avatarRiccardo Cipolleschi

Summary

The article provides an in-depth explanation of the UIViewController lifecycle, detailing the view loading and view lifecycle phases, and how developers can utilize these phases for custom logic execution in iOS app development.

Abstract

The article "The UIViewController Lifecycle Explained" delves into the intricacies of the UIViewController lifecycle, which is crucial for iOS developers to understand when managing the loading and unloading of views and their associated resources. It breaks down the lifecycle into two main phases: the view loading phase, where the view is created and initialized, and the view lifecycle phase, which manages the visibility states of the view. The article highlights the importance of the loadView and viewDidLoad methods during view loading, emphasizing that the view is not automatically loaded when a UIViewController is created. It also explores the methods involved in the view lifecycle, such as viewWillAppear, viewDidAppear, viewWillDisappear, and viewDidDisappear, and how they are triggered during the transition of view states. Practical code examples are provided to illustrate these concepts, and the article concludes by discussing the impact of presentation modes on the lifecycle methods, underscoring the significance of these hooks for resource management and view-related operations.

Opinions

  • The author suggests that understanding the UIViewController lifecycle is beneficial not only for developers working with UIKit but also for those using SwiftUI, as SwiftUI views can be embedded within a UIHostingController.
  • It is implied that the view loading phase is a critical time for initializing the view with necessary data, such as fetching information from a backend.
  • The author emphasizes the importance of the loadView method being called only when the view is first accessed, which is an optimization to avoid unnecessary memory usage.
  • The article conveys that the viewDidLoad method is a key override point for executing one-time setup logic.
  • There is an opinion that developers should be mindful of when and why views are loaded into memory, as demonstrated by the example with secondaryVC not loading its view since it is not presented.
  • The author provides a tip that accessing a view controller's view property can force the view to load, which can be useful in certain scenarios.
  • The article suggests that the view lifecycle methods are essential hooks provided by UIViewController for executing custom logic at specific moments in a view's transition between visible and hidden states.
  • It is noted that forgetting to call super when overriding lifecycle methods can lead to undefined behavior, implying that this is a best practice that should not be overlooked.
  • The author points out subtle differences between the lifecycle methods, which can help developers decide where to place their code for optimal execution timing.
  • The article concludes with the opinion that a thorough understanding of the UIViewController lifecycle methods can significantly enhance app performance and resource management.

The UIViewController Lifecycle Explained

What happens when a VC is created and presented?

Photo by Tolga Ulkan on Unsplash.

When developing an app, we might need to add some custom logic when a UIView is loaded. For example, we may need to perform a network call to fetch some data from the backend. On other occasions, we may need to clear up some resources when the view is about to be dismissed and hidden from the user.

Although these ideas are simple, there are a few nuances in iOS development. It may be helpful to explore them together.

Note: Even if you are developing only with SwiftUI, you may benefit from this article. SwiftUI views may be loaded into a UIHostingController, which inherits from UIViewController, so these ideas work in that context too.

The Lifecycle

The view controller lifecycle can be divided into two big phases: the view loading and the view lifecycle.

The view controller creates its view the first time the view is accessed, loading it with all the data it requires. This process is the view loading.

The second responsibility is to render and hide its view when it is needed. When these events happen, the VC notifies us about those events so that we can run some custom logic. This is the view lifecycle.

The following image is taken from the UIViewController documentation and shows the view lifecycle:

Image from: Apple Developer

From this Finite-State Machine (FSM), we can see what can happen from each state (the white and light blue circles). For example, if the VC is in the appearing state, it can go only to the appeared state, invoking the viewDidAppear(_:) method). Or it can go directly to the disappearing state, invoking the viewWillDisappear(_:) method.

View loading

The first nuance to keep in mind is that when a VC is created, the view is not automatically loaded in memory.

The UIViewController.init method does not invoke the loadView method. This one is invoked the very first time the ViewController is asked to show its view to the screen.

If the VC is presented and the view has never been loaded before, the loadView method is invoked. After that, the VC invokes the viewDidLoad method that is a handy override point to add some custom logic that needs to be executed only once.

The following code shows that:

The AppDelegate creates two view controllers: mainVC and secondaryVC. While the mainVC prints the loadView and the viewDidLoad log, the secondaryVC doesn’t.

The secondaryVC is not being presented and its view is not going to be shown. Therefore, there is no need to occupy the device’s memory with a view that is potentially not going to be used at all.

For the sake of completeness, this is the ViewController’s code:

As you can see, we just created a property to hold the VC’s name and we overrode the loadView and the viewDidLoad method to print a log.

The logMethod method is interesting. It uses the #function meta-parameter. This parameter is valued by the compiler and contains the name of the current method. By passing that parameter to the log function, we can print the name of the method that called the log function itself!

If we run the code, we obtain the following output:

Note: There is another way to force the VC to load the view in memory: by accessing the view to update one of its property. Try to add the line secondaryVC.view.backgroundColor = .systemRed to see that the view gets loaded even though the VC is not presented.

View lifecycle

This is the second responsibility of a view controller. Whenever a view controller is presented, it goes through the viewWillAppear method and then the viewDidAppear method is invoked.

The same happens when the view disappears: First, the viewWillDisappear method is invoked and then the viewDidDisappear method is invoked.

We can easily check this by modifying the code in our ViewController class.

First of all, let’s create a custom View so we can add a button to it.

This view has a button titled Present that the user can tap to present another view controller. The button is laid out in the center of the screen.

Then, we connect this view to our VC. We can modify the loadView method to create the custom View, set its interaction, and use it as the default view for the ViewController.

The interesting bit is line 11, where we present a new ViewController named Modal whenever the user taps on the Present button.

Finally, let’s add some other logs lines for the view lifecycle:

Nothing fancy here: We override the lifecycle methods, adding a log to see when they are invoked. If we now run the code, we can see the following output:

We can tap on the Present button on the simulator to see what happens next.

You can see that the Modal VC has been loaded and presented. However the Main VC is still there behind the Modal. Why is this? If we look at the screen, it’s easy to understand:

The red circle highlights that the old VC’s View is still there and partially visible below the modal view we just presented. Therefore, it has not disappeared yet.

We can now swipe from top to bottom and dismiss the Modal VC. Let’s see how our output changes:

We can see that viewWillDisappear and viewDidDisappear are finally called.

All these methods (viewDidLoad, viewWillAppear, viewDidAppear, viewWillDisappear, viewDidDisappear) are always invoked by the VC, even though we have nothing to run. These are hooks that the VC offers us to run some specific code in those moments.

Note: Always remember to call super when overriding those methods, passing the same parameter we receive when the hook is invoked. If we don’t do that, the behavior is undefined.

Differences between methods

As the names suggest, there are small differences between these methods:

  • viewWillAppear is invoked when the VC is presenting the view, but the view is not yet visible.
  • viewDidAppear is invoked when the view is already rendered on the screen.
  • viewWillDisappear is invoked when the VC is about to dismiss the view, but the view is still rendered on the screen
  • viewDidDisappear is invoked when the view is not rendered anymore.

To verify that, let’s modify the code of the loadView.

The change in this code happens from line 12 to 14. Here, we create the VC in a separate variable and then change the modalPresentationStyle to fullScreen. This forces the Modal view to cover the whole screen so the Main VC won’t be visible anymore.

Note: With this approach, we won’t be able to dismiss the new VC by swiping from top to bottom anymore. If you want to establish that behavior, you need to add a close button.

If we run this new code and tap on the Present button, we obtain the following output:

The interesting part is condensed in the last four lines. After the Modal view is loaded:

  • The Main VC’s view is about to be dismissed.
  • The Modal VC’s view is about to be presented.
  • The Modal VC’s view is presented.
  • The Main VC’s view is finally dismissed.

So, the Modal VC’s viewXXXAppear methods are wrapped by the Main’s viewXXXDisappear methods. This makes sense because the Modal view is about to completely cover the Main view. This process completes only when the Modal’s view is fully rendered on screen.

Conclusion

In this article, we explored how the ViewController lifecycle methods interact with each other and how we can leverage them in our apps.

We explored a few of their optimizations, gotchas, and how they change when playing with the VC’s presentation mode.

The presented hooks are important. We can use them to load and unload resources or to perform operations related to the view lifecycle.

Programming
Swift
iOS
Apple
Software Development
Recommended from ReadMedium