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:
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.0Here, 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:
let weeklyRange = TemperatureRange(minTemp: 10.0, span: 15.0)
// Represents 10.0 to 25.0
// weeklyRange.minTemp = 5.0 // Error: Cannot modify constant instanceThis 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:
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 loaderIf 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:
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:
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:
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:
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.0Property 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:
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.0Observers 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:
@propertyWrapper
struct TenOrLess {
private var value = 0
var wrappedValue: Int {
get { value }
set { value = min(newValue, 10) }
}
}Apply with @:
struct CompactShape {
@TenOrLess var sideA: Int
@TenOrLess var sideB: Int
}
var shape = CompactShape()
shape.sideA = 8 // 8
shape.sideA = 15 // 10Compiler synthesizes wrapper storage.
Setting Initial Values for Wrapped Properties
Add initializers:
@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:
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:
@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) // trueIn 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:
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:
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) // 100Stored types need defaults, lazy-init on first access.
Example audio mixer:
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