Skip to content

Properties

Properties link values to classes, structures, or enumerations. Stored properties hold constant or variable values within instances, while computed properties calculate values on demand. Classes, structures, and enumerations support computed properties; only classes and structures support stored properties.

Enumerations cannot have stored properties.

Properties are typically tied to instances but can also be type properties, shared across all instances.

Property observers track value changes and enable custom responses. They work on custom stored properties or inherited ones.

Property wrappers allow reusing getter/setter logic across multiple properties.

Stored Properties

A stored property is a constant (let) or variable (var) embedded in an instance.

You can set default values during definition or initialization, even for constants.

Consider a TemperatureRange structure for a fixed temperature span:

swift
struct TemperatureRange {
    var minTemp: Double
    let span: Double
}
var dailyRange = TemperatureRange(minTemp: 20.0, span: 10.0)
// Represents temperatures from 20.0 to 30.0
dailyRange.minTemp = 15.0
// Now represents 15.0 to 25.0

Here, minTemp is variable, span is constant.

Stored Properties of Constant Structure Instances

Assigning a structure instance to a constant makes all its properties immutable, even variables:

swift
let weeklyRange = TemperatureRange(minTemp: 10.0, span: 15.0)
// Represents 10.0 to 25.0
// weeklyRange.minTemp = 5.0 // Error: Cannot modify constant instance

This stems from structures being value types; constants lock all properties.

Classes, as reference types, allow modifying variable properties even if the instance is constant.

Lazy Stored Properties

A lazy property (lazy var) computes its value only on first access.

Useful for expensive setups or dependencies resolved post-initialization.

Example with a hypothetical FileLoader and ConfigManager:

swift
class FileLoader {
    // Simulates loading from a file, time-consuming
    var configFile = "settings.conf"
}

class ConfigManager {
    lazy var loader = FileLoader()
    var settings: [String] = []
}

let manager = ConfigManager()
manager.settings.append("Theme: Dark")
manager.settings.append("Volume: High")
// loader not created yet
print(manager.loader.configFile) // Now creates loader

If accessed concurrently without initialization, no single-init guarantee.

Stored Properties and Instance Variables

Swift properties unify storage; no separate instance variables. All details (name, type) are in one declaration.

Computed Properties

Computed properties provide getters (required) and optional setters, no storage.

Example with geometric types:

swift
struct Coordinate {
    var x = 0.0, y = 0.0
}
struct Dimensions {
    var w = 0.0, h = 0.0
}
struct Box {
    var start = Coordinate()
    var extent = Dimensions()
    var midpoint: Coordinate {
        get {
            let midX = start.x + (extent.w / 2)
            let midY = start.y + (extent.h / 2)
            return Coordinate(x: midX, y: midY)
        }
        set(newMid) {
            start.x = newMid.x - (extent.w / 2)
            start.y = newMid.y - (extent.h / 2)
        }
    }
}
var container = Box(start: Coordinate(x: 0.0, y: 0.0), extent: Dimensions(w: 20.0, h: 20.0))
let initialMid = container.midpoint // (10.0, 10.0)
container.midpoint = Coordinate(x: 30.0, y: 30.0)
print("start is now at (\(container.start.x), \(container.start.y))") // (20.0, 20.0)

Shorthand Setter Declaration

Omit setter parameter name for default newValue:

swift
struct AltBox {
    var start = Coordinate()
    var extent = Dimensions()
    var midpoint: Coordinate {
        get {
            let midX = start.x + (extent.w / 2)
            let midY = start.y + (extent.h / 2)
            return Coordinate(x: midX, y: midY)
        }
        set {
            start.x = newValue.x - (extent.w / 2)
            start.y = newValue.y - (extent.h / 2)
        }
    }
}

Shorthand Getter Declaration

Single-expression getters imply return:

swift
struct SimpleBox {
    var start = Coordinate()
    var extent = Dimensions()
    var midpoint: Coordinate {
        Coordinate(x: start.x + (extent.w / 2), y: start.y + (extent.h / 2))
        set {
            start.x = newValue.x - (extent.w / 2)
            start.y = newValue.y - (extent.h / 2)
        }
    }
}

Read-Only Computed Properties

Getters only, no setters; declare as var:

swift
struct Prism {
    var base = 0.0, height = 0.0, depth = 0.0
    var capacity: Double {
        base * height * depth
    }
}
let samplePrism = Prism(base: 3.0, height: 4.0, depth: 5.0)
print("Capacity: \(samplePrism.capacity)") // 60.0

Property Observers

willSet (before store) and didSet (after) trigger on value changes, even if unchanged.

Apply to custom stored, inherited stored, or inherited computed properties.

Example tracking distance:

swift
class DistanceTracker {
    var totalDistance: Double = 0 {
        willSet(newDist) {
            print("Setting to \(newDist)")
        }
        didSet {
            if totalDistance > oldValue {
                print("Increased by \(totalDistance - oldValue)")
            }
        }
    }
}
let tracker = DistanceTracker()
tracker.totalDistance = 5.0 // Setting to 5.0; Increased by 5.0
tracker.totalDistance = 8.0 // Setting to 8.0; Increased by 3.0

Observers run in subclass initializers post-superclass init.

In-out parameters always trigger observers on write-back.

Property Wrappers

Wrappers separate storage management from definition.

Example capping to 10:

swift
@propertyWrapper
struct TenOrLess {
    private var value = 0
    var wrappedValue: Int {
        get { value }
        set { value = min(newValue, 10) }
    }
}

Apply with @:

swift
struct CompactShape {
    @TenOrLess var sideA: Int
    @TenOrLess var sideB: Int
}
var shape = CompactShape()
shape.sideA = 8 // 8
shape.sideA = 15 // 10

Compiler synthesizes wrapper storage.

Setting Initial Values for Wrapped Properties

Add initializers:

swift
@propertyWrapper
struct CompactNum {
    private var maxVal: Int
    private var value: Int
    var wrappedValue: Int {
        get { value }
        set { value = min(newValue, maxVal) }
    }
    init() { maxVal = 10; value = 0 }
    init(wrappedValue: Int) { maxVal = 10; value = min(wrappedValue, maxVal) }
    init(wrappedValue: Int, maxVal: Int) { self.maxVal = maxVal; value = min(wrappedValue, self.maxVal) }
}

Usage variations:

swift
struct EmptyShape {
    @CompactNum var sideA: Int
    @CompactNum var sideB: Int // Both init to 0
}
struct UnitShape {
    @CompactNum var sideA: Int = 1
    @CompactNum var sideB: Int = 1 // 1 1
}
struct LimitedShape {
    @CompactNum(wrappedValue: 3, maxVal: 5) var sideA: Int
    @CompactNum(maxVal: 7) var sideB: Int = 4 // 3 4; caps at respective max
}

Projecting a Value From a Property Wrapper

Expose extras via $projectedValue:

swift
@propertyWrapper
struct CompactNum {
    private var value: Int
    private(set) var projectedValue: Bool
    var wrappedValue: Int {
        get { value }
        set {
            value = newValue > 10 ? 10 : newValue
            projectedValue = newValue > 10
        }
    }
    init() { value = 0; projectedValue = false }
}
struct Sample {
    @CompactNum var num: Int
}
var s = Sample()
s.num = 6; print(s.$num) // false
s.num = 12; print(s.$num) // true

In methods, omit self. for $.

Global and Local Variables

Global/local variables support computed and observed behaviors.

Globals are lazy without lazy.

Apply wrappers to local stored vars:

swift
func demo() {
    @CompactNum var localNum: Int = 0
    localNum = 7 // 7
    localNum = 11 // 10
}

Type Properties

Belong to type, not instances; use static (or class for overridable class computed).

Example:

swift
struct SampleStruct {
    static var sharedVal = "Hello"
    static var calcVal: Int { 42 }
}
class SampleClass {
    static var sharedVal = "World"
    class var overridableCalc: Int { 100 }
}
print(SampleStruct.sharedVal) // Hello
SampleStruct.sharedVal = "Hi"
print(SampleClass.overridableCalc) // 100

Stored types need defaults, lazy-init on first access.

Example audio mixer:

swift
struct SoundChannel {
    static let maxVol = 10
    static var peakVolAll = 0
    var vol: Int = 0 {
        didSet {
            if vol > SoundChannel.maxVol { vol = SoundChannel.maxVol }
            if vol > SoundChannel.peakVolAll { SoundChannel.peakVolAll = vol }
        }
    }
}
var chan1 = SoundChannel(), chan2 = SoundChannel()
chan1.vol = 8 // peakVolAll = 8
chan2.vol = 12 // vol=10, peakVolAll=10

Released under the MIT License.