Which initialisers run when a view or view controller is created in code, from a storyboard or from an archive, and when does the view of a controller get loaded?

7.10.1 · L2 · Interview

Predict first

Which initialiser prints for code and for an archive, and how often does loadView run?

import UIKit

final class V: UIView {
    override init(frame: CGRect) { super.init(frame: frame); print("V init(frame:)") }
    required init?(coder: NSCoder) { super.init(coder: coder); print("V init(coder:)") }
}
final class VC: UIViewController {
    override func loadView() { print("loadView"); view = UIView() }
    override func viewDidLoad() { super.viewDidLoad(); print("viewDidLoad") }
}
let v = V()
let data = try NSKeyedArchiver.archivedData(withRootObject: v, requiringSecureCoding: false)
let un = try NSKeyedUnarchiver(forReadingFrom: data)
un.requiresSecureCoding = false
_ = un.decodeObject(forKey: NSKeyedArchiveRootObjectKey)
let vc = VC()
print("loaded:", vc.isViewLoaded)
vc.loadViewIfNeeded()
vc.loadViewIfNeeded()
print("loaded:", vc.isViewLoaded)
  • V init(frame:) / V init(coder:) / loaded: false / loadView / viewDidLoad / loaded: true
  • V init(frame:) twice, then loadView twice
  • V init(coder:) only

Mental model. A view is a form with two entrances, the front desk (code) and the mail slot (archive); a clerk only stamps the form at the entrance it came through, so both entrances must call the same stamping routine.

A UIView has two designated initialisers: init(frame:) for code and init(coder:) for anything decoded (storyboards, nibs, archives). UIView() calls init(frame:); decoding an archived view calls init(coder:) only, so setup placed in one initialiser silently misses the other path (the check prints exactly one line per path). The standard pattern is a private commonInit() called from both. A view loaded from a nib then also receives awakeFromNib once outlets are connected, which is the first safe place to touch them. A view controller behaves the same way: VC() goes through init(nibName:bundle:), a storyboard goes through init(coder:), and a subclass with extra stored properties must supply required init?(coder:) even if it never uses storyboards.

A controller's view is lazy: isViewLoaded is false after init, and the first access to view (or loadViewIfNeeded()) runs loadView then viewDidLoad exactly once; a second call does nothing. So never touch view in an initialiser, since it forces loading early and defeats the laziness, and prefer isViewLoaded to test without side effects. The system can also release an unneeded view and reload it later only in very old iOS versions; on current iOS viewDidLoad runs once per controller instance in practice (see Q240 for the rest of the lifecycle).

Code uses init(frame:), decoding uses init(coder:), and loadView plus viewDidLoad run once on the first access.

Common traps

  • Putting setup only in init(frame:). The same view from a storyboard never gets it.
  • Reading view inside a controller initialiser. It loads the view immediately and runs viewDidLoad before your properties are ready.

They might follow up with

  • Why must init(coder:) exist? It is marked required in UIKit, so every subclass that adds an initialiser must provide it.
  • Where do I read outlets? In viewDidLoad or awakeFromNib, never in init.

Related questions

  • 7.3.1 What is the view controller lifecycle, and what belongs in each method?
  • 7.2.2 What is the difference between frame and bounds, and what happens to them under a transform?

What does a UIStackView own, and what is the difference between… →