Skip to content

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
  • self refers to the current instance (usually implicit).
  • mutating marks struct methods that modify self.
  • precondition / assert / fatalError document and enforce programmer assumptions. assert is removed in release builds; precondition is 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 lazy property.
  • Observer ping-pong: assigning to the same property inside its own didSet does not re-trigger it, but two properties whose observers set each other will loop forever.
  • Forgetting mutating on struct methods.
  • Making everything public — expose the minimum.
  • Using static var for global mutable state — hard to test, and a compile error under Swift 6 strict concurrency.

Exercises

  1. Add computed fahrenheit (get/set) to a Temperature struct that stores Celsius.
  2. Create a Player class with health and a didSet that prints "defeated!" when it reaches 0 and clamps to 0...100.
  3. Build a Board struct for tic-tac-toe with a subscript(row:col:) that returns Character? and a setter that rejects out-of-range positions.
  4. Make a BankAccount whose balance is private(set) and whose withdraw can fail. Verify outside code can't set balance directly.
  5. Create a type with a lazy property that reads a file the first time it's accessed. Print a message inside to prove it runs once.
  6. Use callAsFunction to make a Polynomial struct that you can call like a function.

Recap

  • Stored properties hold data; computed properties derive it; lazy defers it; observers react to change.
  • static members belong to the type; prefer static let.
  • private(set) is the key to safe encapsulation.
  • mutating marks struct methods that change self.
  • Subscripts, callAsFunction, and static factories make types pleasant to use.

Next: Enums & pattern matching →