UIViewController lifecycle
Order of calls
init(coder:)/init(nibName:bundle:)/ a custominit— the object exists, the view does not yet.loadView()— called lazily, on the first access toview(not when the controller is created). Loads the view from a storyboard/XIB or creates an empty one; override it to build the root view in code, and then do not callsuper.loadView().viewDidLoad()— the view is in memory, outlets are connected. Called once. Add subviews, constraints and subscriptions here.boundsexist but the size is not final (from the storyboard or zero) — do not read geometry here.viewWillAppear(_:)— the view is about to be added to the hierarchy. Called on every appearance. Suitable for refreshing data that may have changed while the screen was hidden.viewIsAppearing(_:)(iOS 17, back-deployed to iOS 13) — the view is already in the hierarchy,traitCollectionand the parent's geometry are up to date, but the first frame is not drawn yet. Apple recommends it for UI that depends on sizes and traits (instead ofviewWillAppearandviewDidLayoutSubviews).viewWillLayoutSubviews()/viewDidLayoutSubviews()— around the root view'slayoutSubviews(); called many times (rotation, keyboard, constraint changes). Manual layout and reading finalframes go here.viewDidAppear(_:)— the screen is shown, the transition animation finished. Start animations, request permissions, focus a field. Starting data loading here is too late — the screen would show up empty; kick it off inviewDidLoad.viewWillDisappear(_:)— the view is leaving the screen: pop, dismiss, pushing the next controller, switching a tab. Save the draft, stop timers.viewDidDisappear(_:)— the view has been removed from the hierarchy.deinit— the controller is released. If it is not called after closing, you have a leak (most often a retain cycle in a closure).
The rest of this answer, with code and interview follow-ups, is in the DevPrep app.
Read the full answer in DevPrep