How to Use Lazy Properties in Swift

What you’ll build or solve

You’ll add lazy stored properties to a type, so the expensive setup happens only when needed.

When this approach works best

Lazy properties work best when:

  • A property is expensive to create, like a formatter, parser, or large dataset.
  • The value depends on self, so it cannot be created safely before initialization completes.
  • The property might never be used in some code paths, so delaying creation saves work.

This is a bad idea for cheap values or values that must always exist immediately. In those cases, a normal stored property is simpler.

Prerequisites

  • Xcode or a Swift Playground
  • Basic knowledge of stored properties and initialization in Swift

Step-by-step instructions

Step 1: Declare a lazy stored property

Add lazy before var, then provide a value. A closure initializer works well for setup that takes multiple lines.

import Foundation

class DataManager {
    lazy var data: [Int] = {
        print("Loading data...")
        return Array(1...5)
    }()
}

You can use the same pattern for other common cases:

import Foundation

class Person {
    let firstName: String
    let lastName: String

    lazy var fullName: String = {
        "\(self.firstName) \(self.lastName)"
    }()

    lazy var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter
    }()

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

What to look for

  • lazy works only with var, not let.
  • The closure runs once and Swift stores the result.
  • Later reads reuse the stored value.

Examples you can copy

Example 1: Delay loading a dataset

class Numbers {
    lazy var values: [Int] = {
        Array(1...10_000)
    }()
}

let numbers = Numbers()
print(numbers.values.count)

Example 2: Build a value that needs self

class Greeting {
    let name: String

    lazy var message: String = {
        "Hello, \(self.name)!"
    }()

    init(name: String) {
        self.name = name
    }
}

let greeting = Greeting(name: "Alex")
print(greeting.message)

Example 3: Create an expensive formatter only if used

import Foundation

class Logger {
    lazy var formatter: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .long
        f.timeStyle = .short
        return f
    }()

    func log(_ message: String) {
        let stamp = formatter.string(from: Date())
        print("[\(stamp)] \(message)")
    }
}

let logger = Logger()
logger.log("Started")

Common mistakes and how to fix them

Mistake 1: Using lazy with let

What you might do:

class Example {
    lazy let value: Int = 10
}

Why it breaks: Lazy properties must be var because Swift assigns them after initialization.

Correct approach:

class Example {
    lazy var value: Int = 10
}

Mistake 2: Expecting the lazy initializer to run on every access

What you might do:

class Counter {
    var count = 0

    lazy var next: Int = {
        count += 1
        return count
    }()
}

Why it breaks: The closure runs once. After that, next returns the stored value.

Correct approach: Use a computed property when you need a fresh calculation each time.

class Counter {
    var count = 0

    var next: Int {
        count += 1
        return count
    }
}

Troubleshooting

  • If you see lazy cannot be used on a let, change let to var.
  • If the initializer never runs, confirm something actually reads the property.
  • If you get an error about using self too early, move the logic into a lazy closure.
  • If you expected recalculation, switch from lazy to a computed property.

Quick recap

  • Add lazy before var to delay initialization.
  • Initialize it with a value or a closure that returns a value.
  • Lazy initialization runs once and stores the result.
  • Use lazy for expensive setup or values that depend on self.
  • Use computed properties when you need recalculation on every access.