UIViewController
iOS development
lifecycle methods
app architecture
Swift programming

Looking to understand the iOS UIViewController lifecycle

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

UIViewController lifecycle methods tell you when a controller is created, when its view is loaded, when that view becomes visible, and when it leaves the screen. Knowing those boundaries is how you decide where setup, refresh, animation, and cleanup code should live.

Object Creation Versus View Loading

A view controller object can exist before its view hierarchy has been loaded. That is why setup work is split across several methods instead of happening in one place.

The most common early lifecycle points are:

  • 'init for dependency injection and object setup'
  • 'loadView when building the view hierarchy manually'
  • 'viewDidLoad after the view hierarchy is in memory'

Most controllers override viewDidLoad, not loadView, because UIKit can usually create the root view for you.

swift
1import UIKit
2
3final class ProfileViewController: UIViewController {
4    private let username: String
5
6    init(username: String) {
7        self.username = username
8        super.init(nibName: nil, bundle: nil)
9    }
10
11    required init?(coder: NSCoder) {
12        fatalError("init(coder:) has not been implemented")
13    }
14
15    override func viewDidLoad() {
16        super.viewDidLoad()
17        title = username
18        view.backgroundColor = .systemBackground
19    }
20}

viewDidLoad is a good home for one-time setup such as building subviews, registering table or collection view cells, assigning delegates, and wiring view model bindings. It is usually the wrong place for work that must happen every time the screen reappears.

Appearance and Disappearance Callbacks

When a controller moves on or off screen, UIKit calls appearance methods in a predictable order. These methods matter because some work depends on visibility, not just existence.

swift
1override func viewWillAppear(_ animated: Bool) {
2    super.viewWillAppear(animated)
3    print("Refresh UI state here")
4}
5
6override func viewDidAppear(_ animated: Bool) {
7    super.viewDidAppear(animated)
8    print("Start animation or analytics here")
9}
10
11override func viewWillDisappear(_ animated: Bool) {
12    super.viewWillDisappear(animated)
13    print("Pause transient work here")
14}
15
16override func viewDidDisappear(_ animated: Bool) {
17    super.viewDidDisappear(animated)
18    print("Finish cleanup here")
19}

Use viewWillAppear for refreshing labels, toggles, or derived UI state that may have changed while the screen was hidden. Use viewDidAppear for work that requires the view to be visible, such as starting a tutorial animation or logging a screen view event. The disappearance callbacks are the right place to stop temporary behavior like timers, editing sessions, or observation tied only to visibility.

Layout Callbacks Are Different

Many lifecycle bugs come from confusing loading with layout. viewDidLoad means the view exists in memory. It does not guarantee final sizes. If you need the actual bounds of subviews, use layout callbacks such as viewDidLayoutSubviews.

swift
1override func viewDidLayoutSubviews() {
2    super.viewDidLayoutSubviews()
3    print("View size: \(view.bounds.size)")
4}

That distinction matters when you set corner radii based on width, create gradient layers, or position custom drawing. Code that depends on geometry often behaves incorrectly if it runs too early.

A Practical Mental Model

The simplest way to reason about the lifecycle is:

  • configure the controller in init
  • do one-time view setup in viewDidLoad
  • refresh screen state in viewWillAppear
  • start visible-only effects in viewDidAppear
  • stop them in viewWillDisappear or viewDidDisappear

Once you follow that split consistently, controllers become easier to test and easier to maintain. It also reduces the temptation to throw unrelated code into viewDidLoad just because that method is familiar.

Common Pitfalls

  • Putting every update in viewDidLoad even though it often runs only once for a loaded view.
  • Reading final view sizes before layout has happened.
  • Starting animations or analytics before the controller is actually visible.
  • Forgetting to call super in overridden lifecycle methods.
  • Treating lifecycle callbacks as a substitute for proper state management.

Summary

  • The lifecycle describes loading, layout, appearance, and disappearance of a view controller.
  • 'viewDidLoad is for one-time setup after the view hierarchy exists.'
  • 'viewWillAppear and viewDidAppear are for work tied to becoming visible.'
  • Layout-dependent code belongs in layout callbacks, not just loading callbacks.
  • A clear separation of responsibilities makes controller code smaller and less fragile.

Course illustration
Course illustration

All Rights Reserved.