What is a lazy property in Swift, and when should you use it?
TL;DR
A lazy var is not created when the object is created. It is created the first time you read it. After that, the value is stored and reused.
Use it for expensive setup that may not be needed, or for values that need self.
It must be a var. It is not thread-safe.
Basic example
final class ReportViewController: UIViewController {
lazy var formatter: DateFormatter = {
print("Creating formatter")
let f = DateFormatter()
f.dateStyle = .medium
return f
}()
}
let vc = ReportViewController() // nothing printed yet
vc.formatter.string(from: .now) // prints "Creating formatter"
vc.formatter.string(from: .now) // no print, reuses the valueWhy use it
1. Save expensive work
Create heavy objects only if the user really needs them.
final class PhotoEditor {
lazy var filterEngine = FilterEngine() // big setup, maybe never used
}2. Use self during setup
Normal stored properties cannot use self in their default value. A lazy one can, because it runs after init.
final class ProfileView: UIView {
lazy var tapGesture = UITapGestureRecognizer(
target: self,
action: #selector(handleTap)
)
@objc private func handleTap() { }
}Rules
- Must be
var. The value changes from "not yet set" to "set".lazy letis not allowed. - Needs an initial value (an expression or a closure call
{ ... }()). - Cannot be a computed property. Lazy is about stored values.
Lazy vs computed property
lazy var | Computed property | |
|---|---|---|
| When it runs | First read, once | Every read |
| Stores the result | Yes | No |
| Can change later | Yes, you can assign it | Only via a setter |
| Good for | Expensive, stable values | Values that depend on other state |
struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height } // computed: always up to date
}If width changes, a lazy area would be stale. Use a computed property for derived values.
Lazy in structs
Reading a lazy property may write to self. So the getter is mutating.
struct Report {
lazy var summary = buildSummary()
func buildSummary() -> String { "..." }
}
let r = Report()
// r.summary // error: cannot use mutating getter on a let constant
var r2 = Report()
_ = r2.summary // OKThis is a common surprise. It is one reason lazy is more common in classes.
Not thread-safe
If two threads read a lazy property for the first time at the same moment, it may be created twice. This is a data race.
final class Cache {
lazy var storage = [String: Data]() // not safe from many threads
}Fixes: create it in init, protect it with a lock or an actor, or use a static let.
static let and globals are lazy and safe
enum Formatters {
static let shortDate: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .short
return f
}()
}Global constants and static properties are created lazily on first use. Swift guarantees they are created once, even with many threads. This is the standard way to write a singleton.
final class Analytics {
static let shared = Analytics()
private init() { }
}Lazy sequences
A different feature with a similar name. .lazy delays work on collections until you need each element.
let firstBig = (1...1_000_000)
.lazy
.map { $0 * $0 }
.first { $0 > 1_000 } // only computes 32 squaresCommon follow-up questions
- Why can't
lazybe used withlet? Aletmust have its value wheninitfinishes. A lazy value is set later. - Can you reset a lazy property? You can assign a new value. But if you set it to
nil(for an optional), it will not run the initializer again. - Is
lazyallowed in Swift 6 strict concurrency? Yes, but the type must still be safe. In an actor or a@MainActorclass, lazy properties are protected by that isolation. - Is a
@Statedefault value lazy? No. The expression runs every time the view struct is created, but SwiftUI keeps only the first value. Avoid heavy work there. - Does
lazywork with@objc? You can expose it, but KVO on lazy properties is unusual. Keep lazy for internal setup.