Swift & Objective-C

[Swift Basics #3] Swift Properties: Stored, Computed, lazy, didSet—4 Selection Rules

This is part 3 of the Swift basics series. It organizes five property types around selection criteria rather than syntax lists. The key question is simple: is this value stored, computed, and when is it created?

7 min read
Cover image for [Swift Basics #3] Swift Properties: Stored, Computed, lazy, didSet—4 Selection Rules

var name = "". This is one of the most common lines in Swift, but more options can go here than you might expect: stored and computed properties, lazy, willSet and didSet, and type properties. You may know all the syntax, yet lose your bearings when asking, “Should this value be lazy or computed?”

This is part 3 of the Swift basics series. It organizes five property types around selection criteria rather than syntax lists. The key question is simple: is this value stored, computed, and when is it created?

Before choosing a type property because shared state is needed, also check The testing cost of a Swift singleton; it makes the boundary between static let and static var clearer.

Stored vs. computed — Values in memory and values created on demand

The first property choice is whether it is stored or computed.

A stored property is a value that occupies actual space inside an instance. If you write var name = "Kim", space for name is allocated in the instance’s memory. A struct’s size is determined by the sum of its stored properties.

A computed property occupies no space. It runs code whenever accessed to produce a value—in effect, a function.

struct Rectangle {
    var width: Double   // stored
    var height: Double  // stored

    var area: Double {  // computed — no storage
        width * height
    }
}

You could make area a stored property. But then area must be updated whenever width changes, and any mismatch becomes a bug. This gives us the first rule: compute values derived from other values instead of storing them. Keeping one source of truth eliminates synchronization bugs at the root.

Adding set to a computed property makes it writable. Store celsius and compute fahrenheit; assigning to fahrenheit can reverse-calculate and update celsius. There is still only one stored truth; everything else is a window onto it.

Here is also when to use a computed property instead of a function. Apple’s API Design Guidelines follow this convention: use a property when computation is cheap, generally near O(1), has no side effects, and conceptually describes an object’s property. Use a method when computation is expensive or has side effects. That is why array.count is a property while array.sorted() is a method.

lazy — A stored property created on first use

lazy is the third option. It is a stored property, but it is initialized on first access rather than when the instance is created.

class ImageProcessor {
    lazy var filters: [Filter] = loadExpensiveFilters()
}

There are two common uses. First, a value with expensive initialization that may never be used; preparing a heavy resource for every instance would waste work. Second, a property that must reference self. A regular stored property’s default value is computed before init finishes, so it cannot use self. With lazy, initialization is deferred until first access, so self is allowed. That is why immediately invoked closures (= { ... }()) often pair with lazy.

Two cautions. lazy must be var. Because its value changes from an uninitialized, nil-like state to a real value later, it cannot be let. It is also not thread-safe. Concurrent first access from multiple threads can run initialization twice. If a value must be created only once in a multithreaded environment, use another mechanism.

The distinction from computed properties is clear: lazy computes once, stores the result, and returns it thereafter; computed properties recalculate every time. Use lazy for “expensive but stable” values and computed for “cheap but always current” values.

Stored values are boxes on a shelf; computed values are made to order
Stored values are boxes on a shelf; computed values are made to order

willSet and didSet — Responding to value changes

Stored properties can have observers. They run immediately before (willSet) and after (didSet) a value changes.

class ProgressBar {
    var progress: Double = 0 {
        didSet {
            // oldValueis provided automatically
            guard progress != oldValue else { return }
            updateUI()
        }
    }
}

A typical use is automating work that must follow a value change: UI updates, logging, and validation such as reverting out-of-range values. You can add side effects while keeping assignment syntax, without writing a setter yourself.

Two practical rules often cause confusion. First, observers are not called when assigning a value in init. Initialization is designed as “configuration,” not “change,” so a UI update in didSet does not apply to the initial value. Second, changing a property of a value type also calls didSet on the property containing it. As in person.name = "Lee", changing only a struct’s internals calls person’s didSet. The value-type semantics that “mutation is replacement with a new value” apply here too.

Beware of overusing didSet. Once logic accumulates there, it becomes hard to trace what one assignment does. If changing one value sends a network request, that violates the principle of least surprise. Keep didSet limited to lightweight synchronization and move heavy logic into explicit methods.

Type properties — Values attached to a type, not an instance

Adding static attaches a property to the type itself rather than to an instance.

struct APIConfig {
    static let baseURL = URL(string: "https://api.example.com")!
    static var requestCount = 0
}

No matter how many instances you create, there is one type property. It fits collections of constants, such as configuration values and shared formatters, and is widely used by the standard library in places like Int.max and Double.pi.

One useful fact: static let guarantees thread-safe lazy initialization. It initializes exactly once on first access and is safe under concurrent access. static provides a guarantee that lazy var does not. It is also why static let shared in a singleton works without a separate lock. However, mutable static var state is effectively global state, harms test isolation, and can become a data-race candidate. A separate article covers why singletons are called an antipattern; here, remember only that static var is a last resort.

Four questions are enough to make the choice
Four questions are enough to make the choice

Selection flowchart — Summarized in four questions

The five types can be reduced to these one-line questions.

  1. Is it derived from another value? → Computed property. Store only one source of truth.
  2. Is initialization expensive or does it require self? → lazy var. Be careful with concurrent first access.
  3. Is there behavior that follows a value change? → Stored property + didSet. Keep it lightweight.
  4. Should there be one value independent of instances? → static. If let, you also get safe lazy initialization.
  5. If none apply → an ordinary stored property. In most cases, that is the answer.

Property wrappers such as SwiftUI’s @State and @Published are ultimately syntax built on this property system. Once you understand stored properties, computed properties, and observers, you can see wrappers as “stored properties wrapped to automate didSet-like behavior.” Creating property wrappers will be covered in the intermediate series.

Results confirmed by direct execution

In Apple Swift 6.3.3, I recorded lazy initialization counts, the old and new values of didSet, and computed-property results in one object. The output shows lazy read twice and a stored property changed from 0 to 7.

properties=lazy-builds:1,didSet:0->7,computed:14

Because of this small result, I do not rely on the lazy keyword alone when a cache must truly be created only once. If concurrent access is possible, I test the initialization count and check whether separate synchronization is needed. Conversely, I keep didSet limited to lightweight state-observation behavior like the output above, and move failure-prone I/O into explicit methods.

Summary

  • The first property rule is stored versus computed. Compute values derived from others to keep one source of truth.
  • lazy is a stored property initialized on first access, solving expensive initialization and self-reference issues. The tradeoffs are mandatory var and lack of thread safety.
  • willSet/didSet respond to value changes, but do not run in init, and become difficult to trace when loaded with heavy logic.
  • static let is a type property with guaranteed thread-safe lazy initialization; static var is global state and a last resort.

The next article is Basics #4: guard. We will properly examine the early exit briefly introduced in the optionals article and explain why the Swift community wages war on indentation.

Sources and verification