2. Properties & methods¶
What & why¶
Properties hold a type's data; methods give it behaviour. Swift's property system goes well beyond plain fields: values can be computed, lazy, observed, or wrapped — and access control lets you hide the details.
Stored vs computed properties¶
A stored property keeps a value. A computed property calculates one every time it's read:
struct Circle {
var radius: Double // stored
var diameter: Double { // computed (read-only)
radius * 2
}
var area: Double { // computed, read-only
Double.pi * radius * radius
}
var circumference: Double { // computed with a setter
get { 2 * Double.pi * radius }
set { radius = newValue / (2 * Double.pi) }
}
}
var c = Circle(radius: 1)
c.circumference = 20
print(c.radius) // ≈ 3.18
Computed properties store nothing, so they can never get out of sync. Prefer them to duplicating state. Inside set, the incoming value is called newValue unless you name it: set(centimeters) { ... }.
Property observers¶
React when a stored property changes:
class Thermostat {
var target: Double = 20 {
willSet { print("about to change to \(newValue)") }
didSet { print("changed from \(oldValue) to \(target)") }
}
}
let t = Thermostat()
t.target = 22
// about to change to 22.0
// changed from 20.0 to 22.0
Observers don't run during init of the same type. Use didSet to enforce invariants:
struct Volume {
var level = 50 {
didSet { level = min(max(level, 0), 100) } // clamp
}
}
var v = Volume()
v.level = 500
print(v.level) // 100
Lazy properties¶
lazy var is computed once, the first time it's accessed — useful for expensive setup:
struct Report {
let lines: [String]
lazy var wordCount: Int = {
print("counting...")
return lines.reduce(0) { $0 + $1.split(separator: " ").count }
}()
}
var r = Report(lines: ["a b c", "d e"])
print(r.wordCount) // counting... 5
print(r.wordCount) // 5 (no recount)
Lazy properties must be var, and accessing one on a struct requires a var instance because it mutates on first access.
Type properties and methods (static)¶
Belong to the type itself, not any instance:
struct Config {
static let defaultPort = 8080
static var requestCount = 0
static func reset() { requestCount = 0 }
}
print(Config.defaultPort)
Classes can use class func / class var for overridable type members. A global shared instance is just static let shared = Config() — the "singleton" pattern; use it sparingly because it hides dependencies.
Swift 6 and mutable statics
In Swift 6's strict concurrency mode, static var of a non-isolated mutable global is a compile-time error unless it's isolated to an actor or marked nonisolated(unsafe). Prefer static let. The Advanced level explains why.
Methods¶
struct Account {
private(set) var balance: Int = 0 // readable anywhere, writable only inside
mutating func deposit(_ amount: Int) {
precondition(amount > 0, "deposit must be positive")
balance += amount
}
mutating func withdraw(_ amount: Int) -> Bool {
guard amount <= balance else { return false }
balance -= amount
return true
}
}
var acct = Account()
acct.deposit(100)
print(acct.withdraw(30), acct.balance) // true 70
selfrefers to the current instance (usually implicit).mutatingmarks struct methods that modifyself.precondition/assert/fatalErrordocument and enforce programmer assumptions.assertis removed in release builds;preconditionis not.
Access control¶
| Level | Visible to |
|---|---|
open |
any module; subclassable/overridable outside (classes only) |
public |
any module that imports yours |
package |
all modules in the same Swift package |
internal (default) |
the module |
fileprivate |
the source file |
private |
the enclosing declaration (and its same-file extensions) |
private(set) is the workhorse: readable by outsiders, writable only by you — it keeps invariants safe.
Subscripts¶
Give your type [] access:
struct Matrix {
let rows: Int, cols: Int
private var cells: [Double]
init(rows: Int, cols: Int) {
self.rows = rows; self.cols = cols
cells = Array(repeating: 0, count: rows * cols)
}
subscript(row: Int, col: Int) -> Double {
get {
precondition(row < rows && col < cols, "index out of range")
return cells[row * cols + col]
}
set { cells[row * cols + col] = newValue }
}
}
var m = Matrix(rows: 2, cols: 2)
m[0, 1] = 5
print(m[0, 1]) // 5.0
Methods as functions¶
Methods are functions too, so you can pass them where a function is expected:
struct Greeter {
let prefix: String
func greet(_ name: String) -> String { "\(prefix), \(name)!" }
}
let g = Greeter(prefix: "Hello")
print(["Ada", "Grace"].map(g.greet)) // ["Hello, Ada!", "Hello, Grace!"]
static factory methods and callAsFunction¶
struct Color {
var r, g, b: Double
static let black = Color(r: 0, g: 0, b: 0)
static func gray(_ level: Double) -> Color { Color(r: level, g: level, b: level) }
}
let c1: Color = .black // leading-dot syntax works when the type is known
let c2: Color = .gray(0.5)
struct Doubler {
func callAsFunction(_ x: Int) -> Int { x * 2 }
}
let d = Doubler()
print(d(21)) // 42 — calls callAsFunction
Common mistakes¶
- Storing something you could compute and letting copies fall out of sync.
- Heavy work in a computed property — callers assume it's cheap. Use a method or a
lazyproperty. - Observer ping-pong: assigning to the same property inside its own
didSetdoes not re-trigger it, but two properties whose observers set each other will loop forever. - Forgetting
mutatingon struct methods. - Making everything
public— expose the minimum. - Using
static varfor global mutable state — hard to test, and a compile error under Swift 6 strict concurrency.
Exercises¶
- Add computed
fahrenheit(get/set) to aTemperaturestruct that stores Celsius. - Create a
Playerclass withhealthand adidSetthat prints "defeated!" when it reaches 0 and clamps to0...100. - Build a
Boardstruct for tic-tac-toe with asubscript(row:col:)that returnsCharacter?and a setter that rejects out-of-range positions. - Make a
BankAccountwhosebalanceisprivate(set)and whosewithdrawcan fail. Verify outside code can't setbalancedirectly. - Create a type with a
lazyproperty that reads a file the first time it's accessed. Print a message inside to prove it runs once. - Use
callAsFunctionto make aPolynomialstruct that you can call like a function.
Recap¶
- Stored properties hold data; computed properties derive it;
lazydefers it; observers react to change. staticmembers belong to the type; preferstatic let.private(set)is the key to safe encapsulation.mutatingmarks struct methods that changeself.- Subscripts,
callAsFunction, and static factories make types pleasant to use.