iosNudge

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

MethodWhenCalled how often
initObject is createdOnce
loadViewview is accessed for the first timeOnce
viewDidLoadView is in memoryOnce
viewWillAppear(_:)Just before it goes on screenEvery time
viewIsAppearing(_:)View is in the hierarchy, size and traits are readyEvery time
viewWillLayoutSubviews / viewDidLayoutSubviewsAround each layout passMany times
viewDidAppear(_:)On screen, animation finishedEvery time
viewWillDisappear(_:) / viewDidDisappear(_:)Leaving the screenEvery time
deinitObject is freedOnce

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 loadView only when you want to replace the root view with your own view. Do not call super then.
  • Use viewDidLoad for 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.bounds in viewDidLoad. The size is not final there.
  • Doing heavy work in viewDidLayoutSubviews. It runs many times.
  • Thinking viewDidLoad runs every time the screen shows. It runs once per instance.
  • Expecting viewWillAppear to 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 viewDidLoad called if the view is never shown? Only if something accesses view. Loading is lazy.
  • Where do you start a network request? Usually viewDidLoad for one-time data, or viewWillAppear when the data must refresh on each visit.
  • Why is deinit not called? A retain cycle keeps the controller alive. Check closures that capture self strongly.
  • What changes inside a UINavigationController push? The old screen gets viewWillDisappear/viewDidDisappear. The new one gets the full appear sequence.
  • How does SwiftUI compare? SwiftUI has onAppear, onDisappear and .task. There is no viewDidLoad.