Explain the UIViewController lifecycle.
EasyAsked oftenUIKitApp lifecycle 2 min read
TL;DR
A view controller loads its view once (loadView, then viewDidLoad).
After that, it gets appear and disappear callbacks each time it comes on or off screen.
Layout callbacks run whenever the view size changes.
Put one-time setup in viewDidLoad. Put "refresh when shown" work in viewWillAppear or viewIsAppearing.
The order of calls
| Method | When | Called how often |
|---|---|---|
init | Object is created | Once |
loadView | view is accessed for the first time | Once |
viewDidLoad | View is in memory | Once |
viewWillAppear(_:) | Just before it goes on screen | Every time |
viewIsAppearing(_:) | View is in the hierarchy, size and traits are ready | Every time |
viewWillLayoutSubviews / viewDidLayoutSubviews | Around each layout pass | Many times |
viewDidAppear(_:) | On screen, animation finished | Every time |
viewWillDisappear(_:) / viewDidDisappear(_:) | Leaving the screen | Every time |
deinit | Object is freed | Once |
What goes where
final class ProfileViewController: UIViewController {
private let nameLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// One-time setup: add subviews, constraints, targets.
view.addSubview(nameLabel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Runs every time. Good for refreshing data.
nameLabel.text = UserStore.shared.name
}
override func viewIsAppearing(_ animated: Bool) {
super.viewIsAppearing(animated)
// Size and trait collection are correct here.
// Good place for work that depends on view.bounds.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Start animations, analytics "screen viewed" events.
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Stop timers, save drafts.
}
}loadView vs viewDidLoad
- Override
loadViewonly when you want to replace the root view with your own view. Do not callsuperthen. - Use
viewDidLoadfor everything else.
override func loadView() {
view = ProfileView() // custom root view, no super call
}viewIsAppearing (the modern answer)
viewIsAppearing came in iOS 17 and is back-deployed to iOS 13.
In viewWillAppear, the view may not have its final size or traits yet.
In viewIsAppearing, it does. So it is the best place for size-based or trait-based updates.
Common mistakes
- Forgetting to call
super. - Reading
view.boundsinviewDidLoad. The size is not final there. - Doing heavy work in
viewDidLayoutSubviews. It runs many times. - Thinking
viewDidLoadruns every time the screen shows. It runs once per instance. - Expecting
viewWillAppearto run when a sheet is dismissed on iOS 13+. A page sheet does not hide the screen below it, so the presenting controller gets no appear callbacks.
What about viewDidUnload?
It was removed long ago (deprecated in iOS 6). Views are no longer unloaded on memory warnings.
Use didReceiveMemoryWarning to free caches if needed.
Common follow-up questions
- Is
viewDidLoadcalled if the view is never shown? Only if something accessesview. Loading is lazy. - Where do you start a network request? Usually
viewDidLoadfor one-time data, orviewWillAppearwhen the data must refresh on each visit. - Why is
deinitnot called? A retain cycle keeps the controller alive. Check closures that captureselfstrongly. - What changes inside a
UINavigationControllerpush? The old screen getsviewWillDisappear/viewDidDisappear. The new one gets the full appear sequence. - How does SwiftUI compare? SwiftUI has
onAppear,onDisappearand.task. There is noviewDidLoad.