Why does SwiftUI favor structs over classes, and when do you still need a class?
SwiftUI views are cheap, disposable value types by design — understanding why unlocks how diffing, state, and identity actually work under the hood.
LEARN BY DOINGPut your intuition to work3 steps
Why does SwiftUI favor structs over classes, and when do you still need a class?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Structs by default
SwiftUI's core View protocol conforms almost universally through structs, not classes. Every Text, HStack, and custom view you write in day-to-day work is a value type. That's not a stylistic accident — it's foundational to how the framework decides when and what to redraw. When an interviewer asks this question, they're really asking whether you understand the mental model swap SwiftUI demands compared to UIKit's reference-based view controllers and views.
#Value semantics remove a whole class of bugs
A struct is copied on assignment and on being passed around; two variables holding "the same" struct are independent after the copy (Swift optimizes this with copy-on-write for backing storage, so the copy is cheap until one side mutates). That means a View struct captured by SwiftUI to compute one frame can't be mutated out from under it by some other part of the app holding a reference — there is no shared, mutable reference to protect. Classes, by contrast, hand out shared references: if two objects hold a reference to the same instance, a mutation from one is visible to the other, which is a common source of stale-state bugs when a view controller and a background task both hold the same object.
#Structs are why the render cycle is cheap
SwiftUI doesn't mutate a persistent view hierarchy the way UIKit mutates a view controller's view property. Instead, every time state changes, SwiftUI calls body again and builds a brand-new tree of View structs describing what the UI should look like, then diffs that tree against the previous one to figure out what actually needs to change on screen. Structs make that workable: creating and discarding thousands of small value types every frame is close to free — no heap allocation, no retain/release traffic, no dangling references to clean up. If View were a class hierarchy, every re-render would allocate and eventually deallocate a tree of objects, and ARC's retain/release calls would become a real cost multiplied by every pixel-driving state change.
#Identity is a separate concern from the type
Interviewers sometimes push further: "but doesn't SwiftUI need to know which view is which across renders?" It does — but that's handled by structural identity (a view's position and type in the tree) or explicit identity via .id(_:), not by the view struct's own reference identity. A class instance has identity for free — === compares whether two references point at the same object in memory — but SwiftUI deliberately doesn't lean on that, because the whole point is that the struct describing a screen state today is a disposable value, not a persistent object to compare pointers against.
#Where a class is still the correct tool
None of this means classes are wrong in SwiftUI code — it means you reach for one when you actually need reference semantics: shared mutable state that multiple views must observe and mutate in place (a view model backing several screens), objects with a real identity and lifecycle independent of any one view's re-render (a network session manager, an audio player, a Core Bluetooth central), or anything that must interoperate with Objective-C/UIKit APIs that require NSObject subclassing (delegates, or a UIViewControllerRepresentable wrapping a real UIViewController). Since iOS 17, the @Observable macro lets a plain class opt into fine-grained SwiftUI observation without the ObservableObject + @Published boilerplate, but it's still a class underneath, chosen specifically because the state needs to be shared by reference.
#In code
// A SwiftUI View is a struct — cheap to create and discard on every render.struct ProfileBadge: View {let name: Stringlet isOnline: Boolvar body: some View {HStack {Circle().fill(isOnline ? .green : .gray).frame(width: 8, height: 8)Text(name)}}}// A class is still the right tool when state must be shared by reference.@Observablefinal class SessionStore {var currentUser: User?var isAuthenticated: Bool { currentUser != nil }func signIn(as user: User) {currentUser = user}}struct RootView: View {@State private var session = SessionStore()var body: some View {if let user = session.currentUser {ProfileBadge(name: user.name, isOnline: true)} else {LoginView(session: session)}}}