body and some ViewThese are the lesson notes for this day. Its build-along tutorial and flash cards have not been written yet.
body and some ViewA SwiftUI view is a struct that conforms to View and has a body property. The struct is not the thing on screen. It is a lightweight description of what should be on screen. SwiftUI calls body to get that description, compares it with the previous one, and changes the real interface to match. Because of this, view structs are created and thrown away constantly, and body may run many times. It must be fast, and it must not cause side effects.
For someone coming from UIKit, the consequences are these. There is no view controller, no viewDidLoad, and no stored reference to a view that you later mutate. A view's init is not a lifecycle event, because the struct is re-created whenever its parent re-renders. You change what is on screen by changing state, and SwiftUI calls body again.
Add let _ = print("body ran") as the first line of a view's body, use the app, and count how often it prints. Add a print to the view's init as well. Explain each line of output.
body?init a mistake, and where should that work go instead?“Product says profile_viewed is being logged dozens of times per visit, and the network inspector shows duplicate requests. Here is the view. What is wrong, and where should this work live?”
struct ProfileView: View {
let userID: String
@State private var profile: Profile?
init(userID: String) {
self.userID = userID
Analytics.log("profile_viewed")
Task { await ProfileStore.shared.prefetch(userID) }
}
var body: some View {
Text(profile?.name ?? "Loading…")
}
}
Where this comes from: Hacking with Swift: iOS interview questions — from Hacking with Swift's question list: “What's the difference between a view's initializer and onAppear()?”
A SwiftUI view is a cheap description, not an object with a life of its own. Its init runs every time a parent's body is re-evaluated — which is constantly. So the event is logged on every re-render, and each run starts an unstructured Task that nobody owns or cancels. init is not viewDidLoad.
struct ProfileViewFixed: View {
let userID: String
@State private var profile: Profile?
var body: some View {
Text(profile?.name ?? "Loading…")
.task(id: userID) {
Analytics.log("profile_viewed")
profile = await ProfileStore.shared.load(userID)
}
}
}.task(id:) ties the work to the time the view is on screen: it starts when the view appears, is cancelled when the view goes away, and restarts if userID changes.
What interviewers listen for: That you separate a view's value (re-created endlessly) from its identity (what actually persists), and that you reach for .task rather than a bare Task.