Properties

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Properties associate a value with a particular class, structure, or enumeration. Stored properties store a constant or variable value as part of an instance; computed properties calculate a value rather than storing one. Both kinds may belong to an instance or to the type itself.

Stored Properties and Constant Struct Instances

struct FixedLengthRange {
    var firstValue: Int
    let length: Int                 // a `let` stored property: fixed once set at initialization
}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
rangeOfThreeItems.firstValue = 6    // fine: rangeOfThreeItems is a `var`

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// rangeOfFourItems.firstValue = 6  // error: rangeOfFourItems is a `let` struct instance, so ALL of it is fixed

Because structs are value types, marking a struct instance let — not just its individual properties — freezes every stored property on it, var ones included, since mutating any property would mean mutating the value the constant holds. A class instance has no such restriction: a let class reference still permits mutating the referenced instance’s var properties, since the reference itself (not the instance) is what the let fixes.

Lazy Stored Properties

class DataImporter {
    var filename = "data.txt"
    init() { print("DataImporter instance created") }
}

class DataManager {
    lazy var importer = DataImporter()    // not created until first accessed
    var data: [String] = []
}

let manager = DataManager()
manager.data.append("some data")
manager.data.append("some more data")
// DataImporter hasn't been created yet
print(manager.importer.filename)          // "DataImporter instance created" prints here, right before "data.txt"

A lazy stored property’s initial value is not computed until the first time the property is accessed — useful when that value is expensive, has side effects, or depends on other state not yet available at init time. lazy requires var: a value that isn’t computed until first use can’t also be guaranteed constant.

Computed Properties, Shorthand Getters/Setters, and Read-Only Computed Properties

struct Point { var x = 0.0, y = 0.0 }
struct Size { var width = 0.0, height = 0.0 }

struct Rect {
    var origin = Point()
    var size = Size()

    var center: Point {
        get {
            Point(x: origin.x + size.width / 2, y: origin.y + size.height / 2)
        }
        set(newCenter) {
            origin.x = newCenter.x - size.width / 2
            origin.y = newCenter.y - size.height / 2
        }
    }

    var shorthandCenter: Point {                  // shorthand: `newValue` replaces a named setter parameter,
        get { center }                             // and a single-expression getter may drop `return`
        set { center = newValue }
    }

    var area: Double {                             // read-only: get-only, no `set` -- may drop the `get` keyword too
        size.width * size.height
    }
}

var square = Rect(origin: Point(x: 0, y: 0), size: Size(width: 10, height: 10))
square.center = Point(x: 15, y: 15)                 // invokes the setter, which repositions origin
print(square.origin)
print(square.area)                                   // 100 -- computed fresh each access, never stored

A computed property’s getter runs on every read and its setter (if any) on every write; a read-only computed property omits set entirely and may drop the get keyword too, leaving just the computed expression — as area does above. Neither kind reserves storage: area recomputes from size every time it’s read.

Property Observers: willSet/didSet

flowchart LR A["New value assigned to the property"] --> B["willSet(newValue) runs\n(oldValue still readable inside willSet too)"] B --> C["The stored value is actually updated"] C --> D["didSet(oldValue) runs\n(newValue already in place, readable via the property itself)"]
class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}

let stepCounter = StepCounter()
stepCounter.totalSteps = 200      // About to set totalSteps to 200 / Added 200 steps
stepCounter.totalSteps = 360      // About to set totalSteps to 360 / Added 160 steps

class Base {
    var score = 0 {
        didSet { print("Base saw score become \(score)") }   // observers on an inherited stored property fire too
    }
}
class Derived: Base {}
let derived = Derived()
derived.score = 10                 // Base saw score become 10

class LazyObserved {
    lazy var value: Int = { print("computing initial value"); return 42 }() {
        didSet { print("value changed to \(value)") }    // didSet also fires on a lazy property, after first access
    }
}

willSet runs just before the new value is stored (with the incoming value available, named newValue by default or the label given in parentheses); didSet runs just after (with the old value available as oldValue). Both are skipped for a property’s very first assignment during initialization, but do fire for every assignment afterward — including one made through an inherited property, or one made to a lazy property after its initial value has been computed.

Property Wrappers

@propertyWrapper
struct Clamped<Value: Comparable> {
    private var value: Value
    let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    var projectedValue: Bool {                       // exposed via the `$` prefix
        value == range.lowerBound || value == range.upperBound
    }
}

struct Slider {
    @Clamped(0...100) var progress: Int = 150         // `wrappedValue:` receives 150, which init clamps to 100
}

var slider = Slider()
print(slider.progress)          // 100
print(slider.$progress)         // true -- $progress is the projectedValue: currently at a range bound

@propertyWrapper moves storage-management logic (clamping, validation, persistence, thread-safety) out of the property itself and into a reusable type: the wrapper’s wrappedValue becomes what code reads and writes through the property’s plain name (progress), an initial value written at the declaration site is threaded through init(wrappedValue:), and an optional projectedValue is reachable with a $ prefix ($progress) for exposing wrapper-specific information such as a validity flag, a Binding, or a publisher.

Global and Local Variables, Type Properties

var globalCounter = 0                 // a global variable -- computed lazily, only on first access, like a lazy property

func exampleFunction() {
    var localCounter = 0              // a local variable -- both global and local variables can be stored or computed,
    localCounter += 1                  // and can have observers, exactly like stored properties on a type
    print(localCounter)
}

struct AudioChannel {
    static let thresholdLevel = 10                  // a stored type property
    static var maxLevelForAllChannels = 0            // shared across every instance, not per-instance

    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxLevelForAllChannels {
                AudioChannel.maxLevelForAllChannels = currentLevel
            }
        }
    }
}

class SomeClass {
    static var storedTypeProperty = "shared"        // `static`: not overridable in a subclass
    class var computedTypeProperty: Int { 42 }       // `class`: a computed type property a subclass MAY override
}

static introduces a type property on a struct, enum, or class — shared by every instance rather than stored per-instance — and on a class it cannot be overridden by a subclass; class declares a computed type property on a class specifically that a subclass is allowed to override, which static never permits.

The Observation Module’s @Observable

import Observation

@Observable
class WeatherModel {
    var temperature: Double = 20.0
    var condition: String = "Sunny"
}

// In a SwiftUI view body, reading `model.temperature` automatically registers that view
// to be invalidated only when `temperature` specifically changes -- not on every model mutation,
// unlike the older ObservableObject/@Published approach it replaces.

@Observable (the Observation module, Swift 5.9) macro-generates fine-grained change tracking for a class’s stored properties: observing code (typically a SwiftUI view) is invalidated only when a property it actually read has changed, rather than on any change to the object as a whole — a more precise, lower-overhead alternative to the ObservableObject protocol and @Published property wrapper it supersedes.

See Also