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.

importFoundation

classDataManager {
lazyvardata: [Int]= {
print("Loading data...")
returnArray(1...5)
    }()
}

You can use the same pattern for other common cases:

importFoundation

classPerson {
letfirstName:String
letlastName:String

lazyvarfullName:String= {
"\(self.firstName) \(self.lastName)"
    }()

lazyvardateFormatter:DateFormatter= {
letformatter=DateFormatter()
formatter.dateStyle= .medium
formatter.timeStyle= .short
returnformatter
    }()

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

classNumbers {
lazyvarvalues: [Int]= {
Array(1...10_000)
    }()
}

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

Example 2: Build a value that needs self

classGreeting {
letname:String

lazyvarmessage:String= {
"Hello, \(self.name)!"
    }()

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

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

Example 3: Create an expensive formatter only if used

importFoundation

classLogger {
lazyvarformatter:DateFormatter= {
letf=DateFormatter()
f.dateStyle= .long
f.timeStyle= .short
returnf
    }()

funclog(_message:String) {
letstamp=formatter.string(from:Date())
print("[\(stamp)] \(message)")
    }
}

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

Common mistakes and how to fix them

Mistake 1: Using lazy with let

What you might do:

classExample {
lazyletvalue:Int=10
}

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

Correct approach:

classExample {
lazyvarvalue:Int=10
}

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

What you might do:

classCounter {
varcount=0

lazyvarnext:Int= {
count+=1
returncount
    }()
}

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.

classCounter {
varcount=0

varnext:Int {
count+=1
returncount
    }
}

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.