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
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
-
Structures and Classes — the value-type vs. reference-type distinction that shapes how a property’s storage is copied or shared.
-
Methods and Subscripts —
mutatingmethods, the method-side counterpart to avarproperty on a value type. -
Enumerations — computed properties and methods on enum cases.
-
Attributes and Compiler Control —
@propertyWrapper,@Observable, and attributes in general. -
Key Paths and Dynamic Member Lookup — referring to a property itself as a value via
\Type.property.