Swift Lazy Properties and Property Wrappers

Delay expensive setup until access and use wrappers for repeated property policies without confusing caching, validation and synchronization.

Lesson 28 of 43Intermediate9 min read

Some stored values are expensive to create and are not needed for every instance. A lazy stored property delays that creation until first access. A property wrapper solves a different problem: it packages a repeated access or storage policy, such as clamping a number or tracking whether an assignment was adjusted.

The structs lesson introduces stored and computed properties. This reference adds two tools that can reduce repeated setup code, while keeping their limits visible. Neither lazy nor a wrapper automatically makes concurrent access safe, and neither replaces a deliberate input-validation policy.

Lazy stores a first-access result

A lazy property must be var because its initial value is not established until after the enclosing instance has been initialized. Its initializer runs on the first access and the result becomes stored state. Later access normally reads that stored result rather than reevaluating the initializer.

A computed property is different: its getter runs when read according to the property's implementation. Use a computed property for a current calculation and lazy for deferred stored setup. If the inputs later change, a lazy result does not automatically refresh itself. Add explicit invalidation or choose a different design when changing inputs must update the result.

For a struct, accessing an uninitialized lazy property can mutate its storage, so a let-bound value cannot generally support that first access. A class reference has different mutation semantics, but its lazy storage still needs an ownership policy. Concurrent first access is not guaranteed to initialize exactly once; synchronize access when that guarantee matters.

Wrappers define a property policy

Mark a wrapper type with propertyWrapper and provide wrappedValue. Applying that type as an attribute to a property lets Swift route access through the wrapper's implementation. The wrapper can use stored state, computed access, initialization parameters and generic types according to its design.

Keep the policy clear and local. A bounded percentage wrapper might clamp assignments, while a validation wrapper might reject them through another interface. Silent clamping is appropriate only when callers expect it. If an order quantity is invalid, changing it silently could order an unintended number of items; a throwing or failable boundary may be better.

The underscore-prefixed backing property exposes the wrapper storage within the permitted declaration context. A projectedValue supplies a separate dollar-prefixed view when the wrapper defines one. Projection is not automatically a Binding or a publisher; those meanings come from particular wrapper implementations and frameworks.

Initialization and observation remain distinct

A wrapper initializer can receive an initial wrappedValue and optional configuration. The resulting storage should be ready before callers use the wrapped property. When several properties need related validation, a wrapper applied independently to each field may be less clear than an initializer validating the whole model invariant.

Property observers such as didSet respond to changes according to their language rules, while wrappers implement access behavior. Do not assume every wrapper is a notification mechanism. A wrapper storing a value does not automatically trigger SwiftUI updates unless it participates in the relevant framework's state system.

Likewise, lazy initialization is not persistence. A lazily created object disappears with its owner unless another persistence mechanism stores it. A wrapper named Cached should document where data lives, when it expires, whether reads can fail and whether it is thread-safe. A convenient attribute should not conceal these important operational contracts.

A struct wrapper's stored state follows value semantics when its own fields do. Copying the enclosing struct copies that state, so a projected flag on one copy need not reflect assignments to another. A wrapper storing a shared class reference has a different ownership story. Include a copy-and-mutate example in its tests when consumers rely on independent values rather than assume the attribute determines semantics by itself.

Official references

The Swift properties guide and its official source cover lazy storage and wrappers. Review when work occurs, what remains stored, how inputs change, and who owns concurrent access. These questions distinguish a useful abstraction from a hidden stale-value or synchronization bug.

Swift Lazy Properties and Property Wrappers Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Trace deferred setup

Creating the Catalog does not print the setup message. The first read triggers it, and the second read uses the stored result. This example assumes sequential access. Do not use its trace as evidence that simultaneous readers on several threads receive a once-only initialization guarantee.

Swift
final class Catalog {
    lazy var summary: String = {
        print("building summary")
        return "3 products"
    }()
}

let catalog = Catalog()
print("created")
print(catalog.summary)
print(catalog.summary)
Output
created
building summary
3 products
3 products

Swift Lazy Properties and Property Wrappers in Real Projects

Worked exercise: Clamp a display percentage

Build a wrapper for a display percentage from zero through one hundred. Record whether the latest initialization or assignment needed clamping. This is intentionally a presentation policy, not validation for a financial or stock operation.

Swift
@propertyWrapper
struct Percentage {
    private var value: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { value }
        set {
            let adjusted = min(max(newValue, 0), 100)
            projectedValue = adjusted != newValue
            value = adjusted
        }
    }

    init(wrappedValue: Int) {
        value = min(max(wrappedValue, 0), 100)
        projectedValue = value != wrappedValue
    }
}

struct ProgressDisplay {
    @Percentage var progress = 0
}

var display = ProgressDisplay()
display.progress = 120
print(display.progress)
print(display.$progress)
display.progress = 40
print(display.progress)
print(display.$progress)
Output
100
true
40
false

The projected Boolean describes the latest assignment rather than a permanent history of every adjustment. Test negative input, both valid endpoints and repeated assignments. If the design needs error messages instead of correction, change the public policy rather than forcing rejection into a property setter that has no ordinary throwing result.

Swift Lazy Properties and Property Wrappers FAQ

When should a value be lazy rather than computed?

Use lazy when setup should happen only if needed and the result should remain stored afterward. Use a computed property when each read should reflect current underlying state or perform a deliberate calculation. A lazy value does not automatically update when inputs change, so it can become stale. Decide whether caching is part of the contract and define invalidation explicitly if the cached result depends on editable data.

Does lazy make initialization thread-safe?

No. Swift does not guarantee that concurrent first access initializes a lazy property only once. The sequential console example demonstrates ordinary deferred behavior, not synchronization. If several tasks can access the object, use an appropriate actor or lock ownership strategy, or initialize the value eagerly under controlled conditions. Also consider whether the initialized value itself is safe to share; protecting construction alone does not protect later mutable operations.

What does a dollar-prefixed wrapped property contain?

It contains whatever the wrapper defines as projectedValue. A custom wrapper can project a Boolean, a helper object or another useful view. Framework wrappers can provide specialized meanings such as a binding, but that is not a universal language rule. Read the wrapper's definition and access contract. In this guide, the projection is only a Boolean reporting whether the last assignment was adjusted, not the underlying percentage or a change stream.

Should wrappers silently fix all invalid values?

Only when correction is an explicit and appropriate policy. Clamping a visual progress percentage can be harmless and expected, while silently altering a requested stock quantity can change the user's intended transaction. For business validation, use an API that reports rejection or a descriptive error. A wrapper should make repeated semantics clear rather than hide important failures behind a concise attribute that callers assume performs ordinary storage.

Swift Lazy Properties and Property Wrappers Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A changing source must affect every read immediately. Which design most directly expresses that requirement?

Swift Lazy Properties and Property Wrappers Quiz

2 questions to check what you learned.

  1. What normally happens on a lazy property's second sequential read?

    • The initializer must execute again.
    • The stored first-access result is read.
    • The enclosing object is recreated.
  2. Which member is required for a basic property wrapper's wrapped access?

    • mainClass
    • hashBucket
    • wrappedValue