Reusing views in storyboards with Auto Layout

Solving real-life Interface Builder problems

For the past few months, I have been working on a rather big project using storyboards and Auto Layout. This has been a great learning experience, and I think I now have a much better understanding of the pros and cons of using Interface Builder vs. pure code. Half a year ago I still assumed that at some point a clear winner would emerge, or at least I would end up preferring one approach over the other. But today I find myself switching between these approaches depending on the project, or even mixing them within a single project. It usually depends on subtle things like the complexity of your application flow and the inheritance relationships between your view controllers.

In any case, I think being able to work with Interface Builder and Auto Layout is an important skill to have as an iOS developer. The first time you try to set up constraints for a non-trivial view hierarchy can be a frustrating experience, and your productivity will probably first take a hit when you start working with storyboards, but you end up with some valuable new tools in your belt and a much better understanding of UIKit, if nothing else. I plan to write more about this topic in the coming months, but today I would like to focus on reusable views in the context of storyboards and Auto Layout.

Reusing interface components

Just like there are many patterns for code reuse, there are also different ways to reuse interface components. To start with something very basic, consider a view controller that occurs in two different flows in your application, e.g. a settings screen that is shown during the activation flow, but can also be accessed later through a navigation drawer. When both flows are defined in the same storyboard, you can lay out the common view controller inside the story board and use multiple segues to link it to the different flows:

Single storyboard

Keeping the entire flow of your application defined inside a single storyboard is not always a good idea. When the application grows and multiple developers start working on the same project, defining different flows (or subflows) in separate storyboards helps to avoid ugly merge conflicts. In this case you can define the common view controller in its own interface file and add a reference to it in the different storyboards:

Multiple storyboards

Often you don’t want to reuse entire screens but only certain parts of it. One approach is to use the view controller containment feature, i.e. define a view controller for each reusable view and use embed segues to link these view controllers to container views in other view controllers. Again you have the option to either lay out the view controller for the reusable view inside the storyboard itself or in a separate interface file:

View controller containment

This approach works but in many cases feels like overkill because the additional view controllers clutter your code base and storyboards. A more lightweight way of working is to reference views rather than view controllers in your storyboards and define the reusable view in a separate interface file:

View containment

The problem

When building a storyboard, Interface Builder allows you to specify the runtime class of the views and view controllers in it. This is a consistent way to link generic interface components to custom code. When the storyboard is loaded into memory, instances of the corresponding classes will be instantiated through the initWithCoder: initializer. But often you want to link code to interface files in the other direction: you want to specify that an object of a certain class should be instantiated by loading the view or view controller from an interface file.

In the case of a view controller, this is pretty straightforward. For a view controller that is referenced in a storyboard, it suffices to override the getter of the nibName property in your view controller implementation and specify the name of the corresponding nib file:

@implementation MyViewController

    - (NSString *)nibName
    {
        return @"MyViewController";
    }

    @end

You can also override the nibBundle property in case your nib file is not included in the main bundle. Alternatively, if you want to be more explicit, you can override initWithCoder: with the same effect:

@implementation MyViewController

    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        return [self initWithNibName:@"MyViewController" bundle:[NSBundle mainBundle]];
    }

    @end

In fact, you don’t even need to specify the name of the nib file or the bundle here because they will default to the class name and the main bundle when you specify nil.

But achieving something similar for views is much less obvious. There is no nibName property defined on UIView. And even if you know the name of the interface file, there may be multiple views defined inside it and you cannot rely on the File’s Owner to resolve this like you can with view controllers.

Resolving the roadblocks

If you adopt the convention that the first view defined inside the nib is the one you are interested in, you might be tempted to write something like this:

@implementation MyView

    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        NSBundle *mainBundle = [NSBundle mainBundle];
        NSArray *loadedViews = [mainBundle loadNibNamed:@"MyView" owner:nil options:nil];
        return [loadedViews firstObject];
    }

    @end

Unfortunately, the above code results in an infinite loop. When initWithCoder: is first called, the nib is unarchived, but the view inside it is instantiated through the initWithCoder: method, which loads the nib again, etc. I first tried to work around this by calling the super class initializer and checking the amount of subviews afterwards. If there are no subviews, the view is a placeholder view referenced from a storyboard and should be loaded from the nib instead. But if there are subviews, the view has been loaded from the nib already and the recursion should stop:

@implementation MyView

    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super initWithCoder:aDecoder];
        if (![self.subviews count])
        {
            NSBundle *mainBundle = [NSBundle mainBundle];
            NSArray *loadedViews = [mainBundle loadNibNamed:@"MyView" owner:nil options:nil];
            self = [loadedViews firstObject];
        }
        return self;
    }

    @end

When I tried this, the app crashed with an uncaught exception “This coder requires that replaced objects be returned from initWithCoder:”. It took me a while to figure this one out, but eventually I stumbled on a method in NSObject called awakeAfterUsingCoder: (not to be confused with awakeFromNib), which is called after initWithCoder: and allows you to substitute another object in place of the object that was decoded. So this is more or less what we were looking for:

@implementation MyView

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
    {
        if (![self.subviews count])
        {
            NSBundle *mainBundle = [NSBundle mainBundle];
            NSArray *loadedViews = [mainBundle loadNibNamed:@"MyView" owner:nil options:nil];
            return [loadedViews firstObject];
        }
        return self;
    }

    @end

Dealing with constraints

So far, so good. We can now add placeholder views to a storyboard, set their class in the identity inspector, and dynamically replace those placeholders with actual content loaded from nib files, without breaking the view outlets in the storyboard. One problem that remains is that, without any precautions, we lose any view properties that depend on the context inside the storyboard. In order to solve this, we need to transfer the frame and the layout constraints from the placeholder view to the view that was loaded from the nib file, so the end result might look something like this:

@implementation MyView

    - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder
    {
        if (![self.subviews count])
        {
            NSBundle *mainBundle = [NSBundle mainBundle];
            NSArray *loadedViews = [mainBundle loadNibNamed:@"MyView" owner:nil options:nil];
            MyView *loadedView = [loadedViews firstObject];
            
            loadedView.frame = self.frame;
            loadedView.autoresizingMask = self.autoresizingMask;
            loadedView.translatesAutoresizingMaskIntoConstraints =
                self.translatesAutoresizingMaskIntoConstraints;
            
            for (NSLayoutConstraint *constraint in self.constraints)
            {
                id firstItem = constraint.firstItem;
                if (firstItem == self)
                {
                    firstItem = loadedView;
                }
                id secondItem = constraint.secondItem;
                if (secondItem == self)
                {
                    secondItem = loadedView;
                }
                [loadedView addConstraint:
                    [NSLayoutConstraint constraintWithItem:firstItem
                                                 attribute:constraint.firstAttribute
                                                 relatedBy:constraint.relation
                                                    toItem:secondItem
                                                 attribute:constraint.secondAttribute
                                                multiplier:constraint.multiplier
                                                  constant:constraint.constant]];
            }
            
            return loadedView;
        }
        return self;
    }

    @end

Now this is quite a bit of boilerplate code, so you probably want to move it to a category on UIView or something like that so your awakeAfterUsingCoder: implementation can be reduced to a one-liner.

Intrinsic size of placeholder views

For components like labels and image views, the intrinsic size can be determined at runtime based on the content. Because of this, Auto Layout does not require you to fully constrain the width and height of these views. Custom views defined in a nib file may also be able to determine their intrinsic size dynamically, but when you add a placeholder view to a view controller in a storyboard, you need to consider this explicitly or Auto Layout will complain about missing constraints. Fortunately there is a dropdown in the Size inspector that allows you to specify that a view is just a placeholder and the width or height will be determined at runtime, when the placeholder has been replaced with actual content:

Specifying placeholder views

An alternative approach

So far we have assumed that the reusable view is defined as the first top-level object in the interface file. An alternative approach is to set the File’s Owner of the interface file to the reusable view class, and to consider the first top-level object in the interface file to be a subview of the reusable view that contains all the other subviews. Outlets can be linked to the File’s Owner rather than to the top-level (sub)view. This breaks the infinite initWithCoder: loop so you don’t need to rely on awakeAfterUsingCoder:. Instead of replacing the placeholder view, you are adding the subview loaded from the nib file to it. The advantage is that you don’t lose the context dependent view properties of the placeholder. However, in the case of Auto Layout, you still need to ensure that appropriate constraints are applied to the added subview:

@implementation MyView

    - (instancetype)initWithCoder:(NSCoder *)aDecoder
    {
        self = [super initWithCoder:aDecoder];
        if (self)
        {
            NSBundle *mainBundle = [NSBundle mainBundle];
            NSArray *loadedViews = [mainBundle loadNibNamed:@"MyView" owner:self options:nil];
            MyView *loadedSubview = [loadedViews firstObject];
            
            [self addSubview:loadedSubview];
            
            loadedSubview.translatesAutoresizingMaskIntoConstraints = NO;
            
            [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeTop]];
            [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeLeft]];
            [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeBottom]];
            [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeRight]];
        }
        return self;
    }

    - (NSLayoutConstraint *)pin:(id)item attribute:(NSLayoutAttribute)attribute
    {
        return [NSLayoutConstraint constraintWithItem:self
                                            attribute:attribute
                                            relatedBy:NSLayoutRelationEqual
                                               toItem:item
                                            attribute:attribute
                                           multiplier:1.0
                                             constant:0.0];
    }

    @end

Bottom line

As your project grows, being able to reuse interface components becomes gradually more important. Because of this, it is worth investing some effort right at the start to develop some generic reuse strategies that cover the edge cases. I still hope that future versions of Xcode will make it easier to reuse views in storyboards, but for now I manage to get by with some common UIView categories.