Friday, August 12, 2011

An improved UIViewController subclass

While I am mostly happy with the concept of UIViewController, I still felt the need to add some improvements to it, namely:

  • To fix what I consider to be minor inconsistencies in the way we subclass view controllers
  • To ease access to view lifecycle information

  • This is why I wrote a lightweight UIViewController subclass, called HLSViewController, which implements very small additions making my life easier every day.

    Separate object cleanup

    In a view controller dealloc method, you must release any strong references to instance variables, in particular to view outlets. These outlets must also be released in the viewDidUnload implementation method to cope with memory warnings. If you care about maintainability, you most probably always factor out the outlet cleanup code into a separate hidden method which you call from both your dealloc and viewDidUnload methods:
    
    // MyViewController.h
    
    @class MyViewController : UIViewController {
    
    }
    
    // ...
    
    @end
    
    // MyViewController.m
    
    @interface MyViewController ()
    
    - (void)releaseViews;
    
    @end
    
    @implementation MyViewController
    
    - (void)releaseViews
    {
        self.nameLabel = nil;
        self.ageLabel = nil;
        self.closeButton = nil;
    }
    
    - (void)dealloc
    {
        [self releaseViews];
    
        // Other cleanup code
        
        [super dealloc];
    }
    
    - (void)viewDidUnload
    {
        [super viewDidUnload];
    
        [self releaseViews];
    }
    
    // ...
    
    @end
    
    
    Once I got tired of writing this kind of code (which happened pretty early), I moved it to the HLSViewController class:
    
    // HLSViewController.h
    
    @class HLSViewController : UIViewController {
    
    }
    
    // ...
    
    @end
    
    // HLSViewController.m
    
    @interface HLSViewController ()
    
    - (void)releaseViews;
    
    @end
    
    @implementation HLSViewController
    
    - (void)releaseViews
    {}
    
    - (void)dealloc
    {
        [self releaseViews];
        [super dealloc];
    }
    
    - (void)viewDidUnload
    {
        [super viewDidUnload];
        [self releaseViews];
    }
    
    // ...
    
    @end
    
    
    Now implementing a view controller is much cleaner if we subclass HLSViewController instead of UIViewController:
    
    // MyViewController.h
    
    @interface MyViewController : HLSViewController {
    
    }
    
    // ...
    
    @end
    
    // MyViewController.m
    
    @implementation MyViewController
    
    - (void)releaseViews
    {
        [super releaseViews];
    
        self.nameLabel = nil;
        self.ageLabel = nil;
        self.closeButton = nil;
    }
    
    - (void)dealloc
    {
        // Other cleanup code
        
        [super dealloc];
    }
    
    // ...
    
    @end
    
    
    This cleanly separates the cleanup code into two groups:

  • all resources associated with the view controller's view (in general outlets), and which must be released when the view is unloaded, are cleaned up in releaseViews
  • all remaining resources are cleaned up in dealloc

  • In general, I now hardly ever need to implement the viewDidUnload method in my view controller classes. Note that I call [super releaseViews] in my subclass implementation even if it is not strictly necessary here (the HLSViewController implementation is empty), but after all you cannot know how the method implementation might change in the future.

    Let us discuss calling super methods a little bit more in the next section.

    Making inheritance cleaner

    When subclassing a view controller class, I was often asking myself whether or not I had to call a super method when overriding it in the subclass, most notably with lifecycle and orientation methods. While this is very well documented for UIViewController, this might get obscure as soon you inherit from some custom view controller provided by a library or a framework.

    Consider the shouldAutorotateToInterfaceOrientation: method: The UIViewController implementation returns YES for portrait orientation only, locking any view controller in portrait mode by default. If you were to call the super implementation of this method first in a direct subclass, i.e.
    
    // MyViewController.h
    
    @class MyViewController : UIViewController {
    
    }
    
    // ...
    
    @end
    
    // MyViewController.m
    
    @implementation MyViewController
    
    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
    {
        if (! [super shouldAutorotateToInterfaceOrientation:toInterfaceOrientation]) {
            return NO;
        }
        
        // ...
    }
    
    // ...
    
    @end
    
    
    you would always end up getting a view controller in portrait mode. This is of course not what you want in general. The solution is obvious: Do not call the super shouldAutorotateToInterfaceOrientation: method when directly subclassing UIViewController. The problem is, this is not what you usually should do when inheriting from a class: In general, you namely want to call the parent implementation when overriding a method. Wouldn't it be nice if calling the super method was the rule for all methods overridden by a view controller subclass? Well, the solution for shouldAutorotateToInterfaceOrientation: is simple: Allow all orientations in HLSViewController implementation. This is moreover consistent with what you expect from an iPad application (after all, this is how the default plist is configured):
    
    // HLSViewController.m
    
    @implementation HLSViewController
    
    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
    {
        // We do NOT call the super implementation here, we replace it
        return YES;
    }
    
    // ...
    
    @end
    
    
    It is now easy to know what to do when subclassing view controllers ultimately inheriting from HLSViewController: For such view controllers, The super method must always be called for lifecycle, rotation, orientation and cleanup methods in any subclass implementation.

    Getting the view lifecycle

    There is surprisingly no way to ask a view controller which kind of lifecycle state it is currently in. Fortunately this is easy to fix by implementing all view lifecycle methods in HLSViewController, setting an internal status variable to one of the following values in each of them:
    
    typedef enum {
        HLSViewControllerLifeCyclePhaseInitialized = 0,
        HLSViewControllerLifeCyclePhaseViewDidLoad,
        HLSViewControllerLifeCyclePhaseViewWillAppear,
        HLSViewControllerLifeCyclePhaseViewDidAppear,
        HLSViewControllerLifeCyclePhaseViewWillDisappear,
        HLSViewControllerLifeCyclePhaseViewDidDisappear,
        HLSViewControllerLifeCyclePhaseViewDidUnload
    } HLSViewControllerLifeCyclePhase;
    
    
    An accessor then allows to retrieve this information at any time, which can be very useful when implementing non-trivial view controllers (e.g. a container view controller).

    The code

    Those are the general guidelines I followed in my implementation of HLSViewController. The final implementation also includes logging (at the debug level) to easily track view controller lifecycle events, and is available from the CoconutKit framework.

    2 comments:

    1. mmm... two problems with this approach, you tell me if I'm wrong:

      1. Your trick relies on the fact that you can call virtual methods during dealloc. Is it safe to do so?
      2. Calling self.prop usually triggers KVO notifications, is it safe to do so during dealloc?

      ReplyDelete
    2. 1. In -[HLSViewController dealloc], releaseViews is called before [super dealloc]. I cannot see any reason why this should not be safe.
      2. I try to avoid KVO as much as possible in my own code. It is too easy to couple to private implementation details using it, or to write code which is difficult to understand. I agree this could be dangerous in some cases, and especially if you do not stick to the same rules. The sample code above was written under the assumption that KVO is not used. Of course, other programmers might implement dealloc differently by calling release on each retained ivar directly. This creates a different problem since you bypass the encapsulation provided by an explicit setter implementation. This could be dangerous as well. In my own code, I often implement setters, I never use KVO. That's why self.prop = nil; is safe in my case. But depending on your rules, what you might need is [prop_ivar release]

      ReplyDelete