← All topics/Swift language features

Question 1

Value vs reference semantics: struct, class, actor

When would you choose a struct vs a class vs an actor for a model type in an iOS app? What are the tradeoffs for identity, mutation, and concurrency?

Answer outline

Default to a struct for model data, and reach for a class or an actor only when you need what they add. The three differ on identity, mutation, and concurrency:

  1. 1.Structs: value types, so assignment logically copies, and standard library types such as Array use copy-on-write so a struct that holds one stays cheap. They cannot inherit, and they get shared behavior through protocols and extensions. Only a struct that stores a closure or class reference can take part in a retain cycle.
  2. 2.Classes: reference types with shared identity (===), inheritance, and Objective-C interop. Use them for shared mutable state or a shared lifecycle. ARC applies, so retain cycles through closures and delegates are a real risk.
  3. 3.Actors: reference types with isolated mutable state and compiler-enforced serial access. Use them for shared resources that several tasks touch. Most models never need one.

SwiftUI views are structs, which is why cheap value copies matter there. View models are usually classes, marked @Observable or conforming to ObservableObject. Keep them on the main actor so the UI always reads consistent state.

Struct vs class vs actor: type semantics, thread safety, and typical iOS use cases.

Principles

  • Prefer structs for pure data and predictable copies, and classes when you need identity or a shared lifecycle.
  • Actors solve mutation under concurrency, and every call into one becomes an await at the call site.
  • A class instance is visible through every reference to it, so one mutation shows up everywhere it's held.
  • A struct of pure values cannot form a retain cycle, but a stored closure or class reference can.
Struct: value semantics
struct Point: Equatable {
    var x: Double
    var y: Double
}

var a = Point(x: 0, y: 0)
var b = a
b.x = 1
// a is unchanged, b is an independent copy
Class: shared reference
final class UserSession {
    var token: String
    init(token: String) { self.token = token }
}

let s1 = UserSession(token: "a")
let s2 = s1
s2.token = "b"
// s1.token is also "b" because both point at the same instance
Actor: serialized access
actor Counter {
    private var value = 0
    func increment() { value += 1 }
    func current() -> Int { value }
}

func useCounter() async {
    let counter = Counter()
    await counter.increment()
    _ = await counter.current()
}

Follow-up angles

  • SwiftUI copies a struct into every view that holds it, so a child mutates the parent's state through @State and @Binding instead of its own copy. State that spans unrelated views or outlives one view belongs in an @Observable or ObservableObject class.
  • Bridging to Objective-C needs a class, and in practice one that subclasses NSObject.