Lazy properties in Swift

Swift iOS Properties Feb 14, 2021 · 6 min read

In Swift, we can declare stored, computed, and lazy properties. Each of them has its own purpose. A lazy property is a feature only a few programming languages support because we can achieve the same result with a combination of computed and stored properties. The downside is that we need to write some boilerplate code with optional or implicitly unwrapped optionals. As we initialize more and more properties lazily, it makes our code more and more difficult to understand. Fortunately, Swift removes this hassle by providing this feature and makes us focus on more important things.

Definition

A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.

Eager initialization

When we don't do lazy initialization, we are basically doing eager initialization. The ProfileViewController class below has two UIKit elements named photoImageView and nameLabel as properties. There are two problems with this code. First, we are eagerly initializing these properties even though we don't show them on a screen yet. Second, we are messing our init with a lot of code. When the UI gets more complex with more UI elements, it will both slow down the initialization process and hurts the readability of init.

import UIKit

final class ProfileViewController: UIViewController {
    private var photoImageView: UIImageView // A stored property
    private var nameLabel: UILabel // A stored property

    init(photo: UIImage? = nil, name: String) {
        photoImageView = .init()
        photoImageView.image = photo

        nameLabel = .init()
        nameLabel.backgroundColor = photoImageView.backgroundColor
        nameLabel.font = .systemFont(ofSize: 20)
        nameLabel.textColor = .white
        nameLabel.text = name

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()

        view.addSubview(photoImageView)
        view.addSubview(nameLabel)

        // Set up layout constraints here
    }
}

let viewController = ProfileViewController(
    photo: UIImage(named: "profile.png"),
    name: "Sukhrob Khakimov"
)
// The `photoImageView` and `nameLabel` have already been created after 
// initializing the `ProfileViewController` even though we don't show them 
// on a screen yet.

Lazy initialization without a lazy modifier

What we can do is to initialize them lazily. For that, we can use a stored property in conjunction with a computed property to simulate lazy initialization like below. But, there are still some problems with this approach. One of them is that we are doubling the number of properties. The other one is we are using optionals and implicitly unwrapped optionals for the required properties.

import UIKit

final class ProfileViewController: UIViewController {
    let photo: UIImage?
    let name: String

    // A stored property
    private var _photoImageView: UIImageView?

    // A computed property
    private var photoImageView: UIImageView {
        if _photoImageView == nil {
            _photoImageView = UIImageView()
        }

        return _photoImageView!
    }

    // A stored property
    private var _nameLabel: UILabel?

    // A computed property
    private var nameLabel: UILabel {
        if _nameLabel == nil {
            _nameLabel = UILabel()
            _nameLabel?.backgroundColor = photoImageView.backgroundColor
            _nameLabel?.font = .systemFont(ofSize: 20)
            _nameLabel?.textColor = .white
        }

        return _nameLabel!
    }

    init(photo: UIImage? = nil, name: String) {
        self.photo = photo
        self.name = name

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()

        view.addSubview(photoImageView)
        view.addSubview(nameLabel)

        // Set up layout constraints here
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        photoImageView.image = photo
        nameLabel.text = name
    }
}

let viewController = ProfileViewController(
    photo: UIImage(named: "profile.png"),
    name: "Sukhrob Khakimov"
)
// The `photoImageView` and `nameLabel` haven't been created after
// initializing the `ProfileViewController`.

Lazy initialization with a lazy modifier

Luckily, we can achieve a very neat solution with the help of a lazy modifier in Swift. The example below shows two ways of initializing a lazy property with single-line and closure methods. This is what we need most of the time.

import UIKit

final class ProfileViewController: UIViewController {
    let photo: UIImage?
    let name: String

    // A single-line method
    private lazy var photoImageView: UIImageView = .init()

    // A multi-line method with a closure
    private lazy var nameLabel: UILabel = {
        var label = UILabel()
        // There is no retain-cycle with the `photoImageView` so we don't need
        // to do `[weak self]` because a lazy property automatically handles that
        // for us. Also, we can just avoid using the `self` keyword explicitly
        // before `photoImageView`.
        label.backgroundColor = photoImageView.backgroundColor
        label.font = .systemFont(ofSize: 20)
        label.textColor = .white

        return label
    }()

    init(photo: UIImage? = nil, name: String) {
        self.photo = photo
        self.name = name

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()

        view.addSubview(photoImageView)
        view.addSubview(nameLabel)

        // Set up layout constraints here
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        photoImageView.image = photo
        nameLabel.text = name
    }
}

let viewController = ProfileViewController(
    photo: UIImage(named: "profile.png"),
    name: "Sukhrob Khakimov"
)
// The `photoImageView` and `nameLabel` haven't been created after
// initializing the `ProfileViewController`.

Lazy initialization with a lazy modifier and factory method

We can go even further by creating these properties with factory methods. There are two benefits of it. First, we can reuse the factory methods to create other similar UI elements as properties or local variables. Second, it makes our code more readable especially if you tend to create properties at the top of a class or struct before method declarations which is more common.

import UIKit

final class ProfileViewController: UIViewController {
    let photo: UIImage?
    let name: String

    // A single-line method with a factory method
    private lazy var photoImageView: UIImageView = createPhotoImageView()

    // A single-line method with a factory method
    private lazy var nameLabel: UILabel = createNameLabel()

    init(photo: UIImage? = nil, name: String) {
        self.photo = photo
        self.name = name

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()

        view.addSubview(photoImageView)
        view.addSubview(nameLabel)

        // Set up layout constraints here
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        photoImageView.image = photo
        nameLabel.text = name
    }

    // A factory method
    private func createPhotoImageView() -> UIImageView {
        UIImageView()
    }

    // A factory method
    private func createNameLabel() -> UILabel {
        let label = UILabel()
        label.backgroundColor = photoImageView.backgroundColor
        label.font = .systemFont(ofSize: 20)
        label.textColor = .white

        return label
    }
}

let viewController = ProfileViewController(
    photo: UIImage(named: "profile.png"),
    name: "Sukhrob Khakimov"
)
// The `photoImageView` and `nameLabel` haven't been created after
// initializing the `ProfileViewController`.

Here are my final thoughts about lazy properties based on what is written in the Swift documentation and my experience.

Use a lazy property when

  • Its initialization depends on outside factors that are available after an instance's initialization.
  • You want to defer the initialization of some complex and expensive object until it is used because it may slow down your app.
  • You want to initialize it and encapsulate its properties in one place for convenience.
  • You want to code higher-level or final APIs that glue different low-level libraries in one place to build something for your end-users. Examples can be view controllers, views, etc.
  • There is a guarantee that it is only used in a single-thread such as UI thread or it is safe to use in multiple threads without adversely affecting the logic of your app since it is not thread-safe.

    If a property marked with the lazy modifier is accessed by multiple threads simultaneously and the property has not yet been initialized, there’s no guarantee that the property will be initialized only once.

Don't use a lazy property when

  • It can be initialized and provided as a dependency to your stored property in the definition of init.
  • You are designing low-level APIs such as libraries because you may be hiding it as a dependency hence may lose extendability, flexibility, and testability if you are not careful enough with it.
  • It is not safe to use it in multiple threads and you want to create only one instance of it during the entire lifetime of your app.