Skip to Content
Course content

187: UIViewController Lifecycle Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a few years ago I was reviewing a PR for a junior developer who was building a custom profile header. He was absolutely pulling his hair out because his center-aligned profile image kept jumping to the left side of the screen every time the app launched, despite his math being "perfect." He had written the layout logic inside viewDidLoad. He was calculating the center of the screen based on self.view.frame.width, but in viewDidLoad, the view hasn't actually been placed in the window hierarchy yet. He was performing calculations against a default storyboard size or a zero-width frame, and the layout only "fixed" itself once the system performed its own layout pass. It was a classic case of trusting the wrong lifecycle hook.

The Gap Between Loading and Layout

We all lean on viewDidLoad because it's the most intuitive place to put setup code. It runs once, it's predictable, and it's where we usually initialize our data. But you have to remember that "loaded" doesn't mean "laid out." When viewDidLoad fires, the view exists in memory, but its final dimensions aren't guaranteed. If you're trying to calculate a frame, set a corner radius based on a view's height, or position a subview precisely, viewDidLoad is the wrong place.

That's where viewDidLayoutSubviews comes in. This method is called after the view has adjusted the frames of its subviews. If you need to do something that depends on the actual size of the view on the user's specific device—like an iPhone SE versus a 15 Pro Max—this is where that logic belongs. Just be careful: this method can be called multiple times (for example, during a screen rotation). If you put a heavy API call or a complex database query here, you're going to tank your frame rate.

Managing the Appearance Cycle

Then we have the "appearance" methods: viewWillAppear and viewDidAppear. I often see developers treat these as interchangeable, but they serve very different purposes. viewWillAppear is your last chance to make changes before the user actually sees the screen. It's the perfect spot to hide the navigation bar or refresh a small piece of state that might have changed while the user was on a different screen.

viewDidAppear, on the other hand, is the place for things that must happen after the view is visible. Think of animations, starting a video player, or presenting an alert. If you try to present a modal or trigger a "Welcome" animation in viewWillAppear, iOS will often ignore it or throw a warning in the console because the view hierarchy isn't fully "settled" yet. I've spent way too many hours debugging "invisible" alerts that were simply called too early in the lifecycle.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // Update the UI to reflect data changes from other screens
    self.updateUserStatus() 
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    // Trigger a smooth fade-in animation for the profile picture
    self.animateProfileEntrance()
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    // Now we have the actual width, so we can make the image a perfect circle
    profileImageView.layer.cornerRadius = profileImageView.frame.width / 2
}

One last thing: never forget to call super. It seems like a formality, but the underlying UIViewController class does a lot of housekeeping in these methods. If you omit super.viewWillAppear(animated), you might find that your view doesn't behave correctly with the navigation controller or that certain system events just stop firing. It's a small line of code that saves you from some very strange, hard-to-track bugs.




📋 Practical Task

Exercise: The Frame-Aware Profile Header

Build a simple UIViewController that demonstrates the difference between the loading and layout phases. Follow these requirements:

  • Add a UIView (the "Header") to the center of the screen with a background color.
  • Create a UILabel that displays the current width of the Header view.
  • In viewDidLoad, update the label to show the Header's width.
  • In viewDidLayoutSubviews, update the label again to show the Header's width.
  • Run the app and observe the label. You should see the width change (or start as a default value and then snap to the correct value) as the lifecycle progresses.
  • As a bonus, add a print statement to viewWillAppear and viewDidAppear to verify the order in which they execute in the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.