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.Structs: value types, so assignment logically copies, and standard library types such as
Arrayuse 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.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.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.
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
awaitat 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 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
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 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
@Stateand@Bindinginstead of its own copy. State that spans unrelated views or outlives one view belongs in an@ObservableorObservableObjectclass. - Bridging to Objective-C needs a class, and in practice one that subclasses
NSObject.



