UIView
iOS Development
View Lifecycle
Swift Programming
iOS Notifications

UIView - How to get notified when the view is loaded?

Master System Design with Codemia

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

Introduction

UIView itself does not expose a single universal event named “view loaded,” so developers often need to pick the right lifecycle hook for the job. In practice, the correct callback depends on whether you are working in a UIViewController, a custom UIView, or a view loaded from a nib. Choosing the proper hook avoids duplicate work, layout bugs, and timing issues.

Understand the Lifecycle Boundaries

A common source of confusion is mixing controller lifecycle with view lifecycle. For controllers, viewDidLoad is called once after the root view is loaded into memory. For views, methods such as init, awakeFromNib, didMoveToSuperview, didMoveToWindow, and layoutSubviews are the key points.

Use this rule of thumb:

  • Use viewDidLoad for one-time controller setup
  • Use awakeFromNib for nib-backed view setup
  • Use didMoveToWindow when you need to know a view became visible in a window
  • Use layoutSubviews for frame-dependent updates

Controller Example for One-Time Setup

If your question is really about screen initialization, start in UIViewController.

swift
1import UIKit
2
3final class ProfileViewController: UIViewController {
4    private let titleLabel = UILabel()
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8        view.backgroundColor = .systemBackground
9        titleLabel.text = "Profile"
10        titleLabel.translatesAutoresizingMaskIntoConstraints = false
11        view.addSubview(titleLabel)
12
13        NSLayoutConstraint.activate([
14            titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
15            titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
16        ])
17    }
18}

This callback runs once per controller instance, so it is safe for expensive setup.

Custom UIView Notification Pattern

For reusable custom views, create a small callback API that fires when the view is attached to a window. This pattern gives parent controllers a clear event without relying on guesswork.

swift
1import UIKit
2
3final class StatusCardView: UIView {
4    var onFirstAttachToWindow: (() -> Void)?
5    private var didNotify = false
6
7    override func didMoveToWindow() {
8        super.didMoveToWindow()
9        guard window != nil, !didNotify else { return }
10        didNotify = true
11        onFirstAttachToWindow?()
12    }
13}

Usage in a controller:

swift
1import UIKit
2
3final class DashboardViewController: UIViewController {
4    private let card = StatusCardView()
5
6    override func viewDidLoad() {
7        super.viewDidLoad()
8        card.onFirstAttachToWindow = {
9            print("StatusCardView is now in a window")
10        }
11        view.addSubview(card)
12    }
13}

This is explicit and testable, and it avoids repeatedly firing logic during every layout pass.

Nib and Storyboard Cases

If the view is loaded from Interface Builder, awakeFromNib is typically the earliest safe point for outlet wiring and local initialization.

swift
1import UIKit
2
3final class BannerView: UIView {
4    @IBOutlet private weak var messageLabel: UILabel!
5
6    override func awakeFromNib() {
7        super.awakeFromNib()
8        messageLabel.text = "Ready"
9    }
10}

Avoid assumptions about frame size in awakeFromNib; geometry may still change later.

When You Need Final Geometry

If setup depends on final sizes, use layoutSubviews with an idempotent guard:

swift
1import UIKit
2
3final class RingView: UIView {
4    private var didCreateLayer = false
5
6    override func layoutSubviews() {
7        super.layoutSubviews()
8        guard !didCreateLayer else { return }
9        didCreateLayer = true
10
11        let circle = CAShapeLayer()
12        circle.frame = bounds
13        circle.path = UIBezierPath(ovalIn: bounds.insetBy(dx: 4, dy: 4)).cgPath
14        circle.lineWidth = 3
15        circle.fillColor = UIColor.clear.cgColor
16        circle.strokeColor = UIColor.systemBlue.cgColor
17        layer.addSublayer(circle)
18    }
19}

Without the guard, repeated layout passes can create duplicate layers and memory growth.

Practical Decision Matrix

Pick the callback based on intent:

  • One-time screen setup: viewDidLoad
  • View loaded from nib: awakeFromNib
  • Need window attachment state: didMoveToWindow
  • Need final frame: layoutSubviews

This mental model reduces lifecycle mistakes in large UIKit codebases.

Common Pitfalls

  • Running network requests from layoutSubviews, which may execute many times
  • Expecting viewDidLoad to run each time a screen appears
  • Doing frame-dependent calculations in init or awakeFromNib
  • Forgetting guards in didMoveToWindow, causing repeated side effects
  • Treating UIView and UIViewController lifecycles as interchangeable

Most bugs are caused by choosing a hook that does not match the desired lifecycle moment.

Summary

  • There is no single UIView event that universally means “fully loaded.”
  • Use controller callbacks for screen setup and view callbacks for reusable components.
  • didMoveToWindow is a solid signal when a view first becomes visible.
  • Reserve layoutSubviews for geometry-sensitive work, guarded for idempotence.
  • Explicit callback patterns make lifecycle behavior easier to test and maintain.

Course illustration
Course illustration

All Rights Reserved.