Mastodon

Wednesday, January 28, 2015

View Controller Lifecycle

New iOS developers are often confused about the life cycle of view controllers. In this post I will walk through the various life cycle methods and explain how to use them and some common mistakes to avoid.

Life Cycle Overview

View controllers have a slightly more complex life cycle than views, and it may not be clear what should be done at each state of that life cycle. Here are some of the stages:

  • Construction
  • LoadView
  • ViewDidLoad
  • ViewWillAppear
  • ViewDidAppear
  • ViewWillDisappear
  • ViewDidDisappear
  • ViewWillLayoutSubviews
  • ViewDidLayoutSubviews
  • UpdateViewConstraints
  • WillMoveToParentViewController
  • DidMoveToParentViewController

The most important thing to note is that the View of a view controller is not created until it is needed. This can make some parts of a view controller's code a bit more complex (more on that later), but it allows for view controllers to be more lightweight until they are actually put on screen. 1

View Loading

LoadView is called when the view controller needs to create its top-level view. Specifically, this method is called when the View property getter is accessed for the first time. The sole responsibility of LoadView is to create a view and assign it to the View property. In most cases there is no need to override this method. The default implementation looks for a .xib using the name supplied in the base UIViewController constructor or the name of the class. If there is no .xib then it constructs a new UIView. If you need to implement this method yourself (perhaps to use a custom view type as the top-level view) then you would do it like this:

public override void LoadView()
{
    View = new MyCustomView();
}

You may optionally assign a temporary size to the view (perhaps based on the screen size), but remember that the size of the view controller's view is ultimately assigned by whatever puts it on screen (see my previous blog post on layout gotchas). You should generally avoid doing too much work in this method aside from creating that top-level view. If you have other properties to set or other views to create then you should defer those until ViewDidLoad so that if in the future you decide to replace LoadView with a .xib instead then you can just delete the LoadView method and leave the rest of the code alone.

ViewDidLoad is called after LoadView finishes, and that method is used to finish initializing the view. You can use this method to set additional properties of the View; create, initialize, and add more subviews; and add gesture recognizers. Remember that the view does not yet have a final size at this point so you should not be doing layout in this method (again, refer to my previous blog post on layout gotchas).

It is important to note that LoadView and ViewDidLoad can be called at any time between constructing the view controller and putting it on screen, and you should not make any assumptions about external state in these functions. One example of an assumption that is easy to make but dangerous is using the NavigationController property. This property walks the view controller hierarchy looking for a UINavigationController, which it will only find if the view controller is pushed into a navigation controller. It is possible that a view controller's view is loaded in response to being pushed into a navigation controller, in which case this property will be non-null and usable. However, it is also possible that something else could trigger loading the view before it is pushed into a view controller, and in that case the NavigationController property will be null. It is best to defer any uses of this property until ViewWillAppear.

View Appearance and Disappearance

Speaking of, the next few life cycle methods relate to appearance and disappearance. When a view controller's View is about to be shown its ViewWillAppear method is called. Specifically, this method is called when the view controller's View is about to be added to a window. Within this method the view is not yet in a window (View.Window will be null), and therefore it is not yet visible to a user. You can use this method to update any views before they are shown to the user.

After the view has been added to the window the ViewDidAppear method is called. In this method View.Window will not be null, and therefore the view will actually be visible to the user. You should avoid making visible changes to views at this point since the user will have already seen the previous state of the view. However, you can use this method to initiate animations or loading actions that shouldn't happen until the user can actually see the view.

Likewise, ViewWillDisappear is called when a view controller's view is about to leave the window, and ViewDidDisappear is called after it has been removed from the window.

View Layout

For layout purposes there are three methods to override. ViewWillLayoutSubviews and ViewDidLayoutSubviews are called before and after the view does its layout, respectively. In between these two methods the auto-layout and/or auto-resizing mechanisms take effect and the view's LayoutSubviews method is called. For doing manual layout the best method to override is ViewDidLayoutSubviews since it happens after any auto-layout or auto-resizing calculations have been applied. If you are using auto-layout then you can use UpdateViewConstraints to modify or replace the constraints before they are applied.

View Controller Hierarchy Changes

Lastly, there are two methods related to notifications about changes to the view controller hierarchy, which is used for container view controllers like UINavigationController. WillMoveToParentViewController is called when a view controller is about to move to a new parent view controller. The current parent view controller is still accessible in the ParentViewController property, and the new parent is passed in as an argument (null means it is being removed from its parent). DidMoveToParentViewController is called after the ParentViewController property has been updated. There are very few uses for these functions, and in most cases I have found that other life cycle methods are more suitable.

Properly Handling Delayed View Loading

When a view controller is first constructed it doesn't have a view. That means you should not try to access the View property from within the constructor2. It also means that you should write your public API carefully

For instance, consider a view controller that has a public property for setting the text of a label. You may be tempted to write it like this:

private UILabel _label; // Created in ViewDidLoad

public string LabelText
{
    get
    {
        return _label.Text;
    }

    set
    {
        // WRONG!
        _label.Text = value;
    }
}

The problem here is that this public property may be used before the view is actually loaded, which means the label might not yet exist. You would crash in that case. The proper way to handle this is to have a separate field to store the property value and synchronize that with the view when it is created. However, you also have to make sure you push the value into the view whenever the property is set after the view is loaded. Here is the proper pattern for handling this:

private UILabel _label;
private string _labelText = DefaultLabelText;

private void UpdateLabelText()
{
    _label.Text = _labelText;
}

public override void ViewDidLoad()
{
    _label = new UILabel();
    UpdateLabelText();
}

public string LabelText
{
    get { return _labelText; }

    set
    {
        if (_labelText != value)
        {
            _labelText = value;

            if (IsViewLoaded)
            {
                UpdateLabelText();
            }
        }
    }
}

Note carefully how the setter is implemented: the new value is stored in a field, and then we check IsViewLoaded to determine whether to push the new value into the view. If the view doesn't exist we just wait until it is created and push the value then. The IsViewLoaded property is important to remember. This alternative approach is both obvious and wrong:

// WRONG!
if (View != null)
{
    UpdateLabelText();
}

Recall that the view is loaded the first time that the View property is accessed. That means the code above actually causes the view to load, and therefore would always execute. You should never try comparing View to null. Instead use IsViewLoaded, which checks without itself triggering a load.

Event Handlers and Cleanup

In order to avoid memory leaks or crashes it may be necessary to do some work when you view controller is about to be used and then clean up when it is no longer in use. For instance, you may register for some notifications, and in order to avoid leaking or crashing you need to unregister those notifications. The best methods to use for this are ViewWillAppear and ViewDidDisappear. Here is an example:

private NSObject _notificationHandle;

public override void ViewWillAppear()
{
    _notificationHandle = NSNotificationCenter.DefaultCenter.AddObserver(NotificationName, HandleNotification);
}

public override void ViewDidDisappear()
{
    if (_notificationHandle != null)
    {
        NSNotificationCenter.DefaultCenter.RemoveObserver(_notificationHandle);
        _notificationHandle = null;
    }
}

You should not use ViewDidLoad or LoadView for anything that needs to be cleaned up in ViewDidDisappear because it is possible (and even likely) that ViewDidDisappear will be called multiple times, but ViewDidLoad and LoadView will only ever be called once.

Some people like to put cleanup code in Dispose(bool), but I think that is generally a bad idea for a few reasons. First, many people mistakenly believe that it will avoid leaks (see this post. If you have to do cleanup in order to avoid leaking your view controller itself then the only way that Dispose(bool) will help is if you explicitly Dispose() your view controller somewhere, which is error prone. If you are cleaning up other resources, like background tasks, then it may not even be safe to wait that long. Personally I think it is wrong to use Dispose(bool) for anything other than cleaning up unmanaged resources. Everything else should be handled using ViewWillAppear and ViewDidDisappear or possibly custom app-specific life cycle methods.

In a future blog post I will cover memory leak avoidance for event handlers in Xamarin.iOS in more detail.


  1. Up until iOS 6 a view controller could also unload a view after it had been loaded, which made the life cycle even more complex. Apple found that unloading a view had little benefit, and was the source of many bugs due to the complexity of handling it properly. Therefore they deprecated the view unloading life cycle methods and stopped calling them. You should never implement ViewWillUnload or ViewDidUnload. In fact, Apple's recommendation was to remove those methods even from apps that supported older versions of iOS. 

  2. Technically you can access the View property in a constructor, but doing so will trigger calls to LoadView and ViewDidLoad, which may assume that the code in the constructor has already run. This is very likely to cause problems and so it is a good idea to avoid it. 

3 comments:

  1. Nice writeup!

    I'm new to Xamarin/iOS dev and was wondering how to properly load/change/return to a view?

    For example, I am working on an app where the user needs to login. Once they are logged in I change their view to a new view changing the TabBarController.SelectedIndex. All this is working a-ok. The issue is, I have a Logout button that returns them to the login screen but right now it's still in its previous 'state' (aka the spinner is up, their username/password is still populated, etc).

    What I wonder is how is this properly handled usually? Should I be 'unloading' the view after I change using TabBarController.SelectedIndex to switch to the new view after login? If so how would I unload the view I'm about to exit?

    Or do you reset the view when you return using ViewWillAppear? That seems unnecessary if I literally want the view to be in the same state it was when I originally loaded it.

    I guess even more generally what is the correct approach here? Aka do you unload views when you move from TabBar item to TabBar item (Views/etc), or do you leave them on the nav stack and just handle redisplaying via something like ViewWillAppear?

    Thanks!

    ReplyDelete
    Replies
    1. These are good questions. I would first recommend reading some of my other posts on related topics because they may give you some new ways of thinking about these problems. First, "Decoupling Views" (http://blog.adamkemp.com/2015/03/decoupling-views.html), and the followup to that "Decoupling Views in Multi-Screen Sequences" (http://blog.adamkemp.com/2015/03/decoupling-views-advanced-ux-flows.html). Those both use Xamarin.Forms examples, but the concepts are not specific to Xamarin.Forms, and I have applied both the same techniques to Xamarin.iOS applications.

      For your specific use case there are two approaches:

      1. Replace the existing login view controller with a fresh one so that it starts with a clean slate. This is often the easiest approach.

      2. Update the state of the existing view controller.

      With either approach you need some code somewhere that knows how to complete this logical task of "going back to log in". Ideally the logic for that would go in one place, which is why the technique described in the second linked blog post is useful. Rather than have some other view controller know how to go "back" or how to interact with the login view controller or even which other view controller it should be interacting with, you would instead just have some kind of higher-level navigation controller class (not a "view controller", but a controller in the more general sense) that manages this sequence. You can just tell that object "go back to login", and it can do whatever it needs to do. It would own the reference to the navigation controller or tab bar controller or whatever you're using for navigation.

      Delete
    2. Thx Adam I’ll read those posts soon as that sounds ideal and I need to fully understand how this works. Appreciate the help with all this, thanks again!

      Delete