Joe Conway

View Controller Containers: Part II

January 6, 2014

In this article, we’ll implement a new view controller container, STKMenuViewController.

Make sure to check out the previous article in the View Controller Containers series here.

Going back to our ‘Big 3’ container concepts, let’s look at how this container will shake out:

  1. The organization and traversal of the STKMenuViewController will be the same as a UITabBarController: there will be a list of view controllers that the user can select from at any time.
  2. The layout of the STKMenuViewController will allow each child view controller’s view to be full-screen. Initially, there will be no animation between view controllers, but we’ll remedy that in a later part of the series.
  3. The built-in interface element of the STKMenuViewController will be a full-screen overlay that appears after a user taps three fingers on the screen.

In the end, the STKMenuViewController will look like this when its built-in interface is visible:

View Controller Containers

A three-finger tap on the screen will bring up this ring menu view. The icon for each contained view controller will appear on this ring. Tapping on an icon will display the associated view controller. Tapping anywhere else will dismiss the menu.

When implementing a view controller container, it is best to start by getting something on the screen. So let’s set our initial goals that we’ll conquer in this installment of the series:

  1. Be able to create an instance of STKMenuViewController and set it as the root view controller of a window.
  2. Be able to supply the STKMenuViewController with a list of view controllers that it can eventually swap between.
  3. Be able to see the view of the first view controller in that list.

By meeting these goals, we’ll cover the majority of the first two items in the ‘Big 3’: organization/traversal and layout. We won’t cover every line of code – although, you can check out the source on Github to see that – and will instead focus on the big pieces and the concepts behind them.

On the Shoulders of Giants

Since we are going to mirror the organization of a tab bar controller, and the layout will be similar, let’s look at the tab bar controller’s view hierarchy:

View Controller Containers

Yes, that is a big view hierarchy. The tab bar controller’s view is an instance of UILayoutContainerView. That view’s subviews are the tab bar itself and a UITransitionView. The transition view’s subview is a UIViewControllerWrapperView, which contains the view of the selected view controller.

This means that the process for a tab bar controller to swap in a new view goes as follows:

  1. The newly selected view controller’s view is “wrapped” as a subview of a new UIViewControllerWrapperView. It’s constraints are set up so the new view fits itself inside its wrapper.
  2. This wrapper view is added as a subview of the UITransitionView and its constraints are also set up to tightly fit.
  3. The old view’s wrapper is removed from the UITransitionView, thus removing the old view as well.

The value in this hierarchy is that it cleanly separates the roles of each of the views, allowing for compartmentalization of the fundamental behaviors of a tab bar controller. This is Programming 101: clean role separation minimizes the chance of failure by reducing how much code has to change when the behavior of an object is changed. (And this kind of failure – where one change creates another problem – is a very bad class of failure.)

The UILayoutContainerView establishes the relative layout of the tab bar and the transition view just once. This means that the rules for what happens when the screen rotates are also established just once. By moving the layout logic into this single layer, no matter what view we swap in, a tab bar controller doesn’t have to reconnect constraints or take any extra precaution when laying out its interface.

Thus, the UITransitionView’s position and size are determined by the constraints it has with the UILayoutContainerView and the UITabBar. This makes transition between views a very easy step: just add the new view and its wrapper to the transition view and make it fit. Should we ever want to change the size or position of the tab bar, we don’t have to touch the code that handles swapping in views.

The role of the UIViewControllerWrapperView is a bit more devious. To understand it, first understand that one of the fundamental rules of a view controller is to never modify anything about another view controller’s view hierarchy. This includes modifying the view hierarchy itself (by adding or removing subviews) and changing properties of the views (like the opacity or frame).

The reason for this rule is simple: mucking with another view controller’s view hierarchy isn’t safe. There are three things that can happen. Consider an example, where a view controller tries to change the frame of another view controller’s button:

[[someOtherViewController loginButton] setFrame:someFrame];

First possible type of failure, escalation level: ‘Aw, shucks.’ The someOtherViewController uses a mechanism to ensure that its loginButton stays in the same place, thus causing this code to simply have no effect.

Second possible type of failure, escalation level: ‘Oh, $#!@.’ The someOtherViewController used the position of the loginButton as an anchor for establishing the constraints of the rest of its interface, thus, the entire interface is either unusable, or the constraints are unsatisfiable and the application crashes.

Third possible type of failure, escalation level: ‘You’re fired.’ You want to change the interface of someOtherViewController, and loginButton will no longer be a part of the view hierarchy. Now, the feature you wrote that accessed the loginButton from the outside has to be rewritten. You have to tell the client or your boss, ‘Yeah, we can make that change, but it breaks some other stuff, and then you have to pay for that, too.’ That’s a catastrophic failure, in my opinion. (Unless you are one of those shops that uses this ‘recurring revenue’ as a business plan, in that case, you probably aren’t reading this article anyway, because you’re currently downloading code off Github and Stackoverflow right now.)

Not messing with another view controller’s view hierarchy applies to container view controllers, too. Container view controllers, however, get one exception: they can change the frame of the view controller’s root view to fit it inside their layout.

The wrapper view, then, gives the tab bar controller some minor control over another view controller’s view without opening itself up to these issues. For example, let’s say that the container view controller decides to use a ‘fade out’ animation on a child view controller, but the child view controller prevents the opacity of its view from being changed for some reason. The container view controller only needs to change the opacity of the wrapper view and we get the expected result without harming the child view controller. Thus, you can consider the wrapper view a proxy to a child view controller’s view.

Implementing STKMenuViewController

Since STKMenuViewController is quite similar to a UITabBarController, it could be argued that subclassing UITabBarController would buy us a lot up front. Unfortunately, all subclassing UITabBarController does is buy us headaches.

A tab bar controller is slightly too opaque to effectively subclass. It doesn’t consider that you might want to use a different interface to replace the UITabBar. It will fight you every step of the way when trying to hide the existing UITabBar. It also automatically wraps some of its child view controllers in a UINavigationController if those view controllers tab bar items wouldn’t fit in the UITabBar. This causes layout issues when trying to show those view controllers in our own layout.

While subclassing UITabBarController can work, it is a constant battle – and the rules of engagement change each time a new iOS version is released. However, tab bar controllers really aren’t that special. They largely depend on the built-in view controller container functionality that every UIViewController has. Therefore, implementing a UIViewController that mimics the functionality of a UITabBarController is incredibly straightforward.

So, in our own STKMenuViewController, we’ll use UIViewController’s view controller container functionality. We’ll take a similar approach to UITabBarController, although we’ll forego the wrapper view, just because we don’t have plans to do anything wicked to our child view controllers. In practice, you’ll have to make that decision depending on how reusable you’ll want your container view controller to be – the wrapper view is an extra precaution that you may need if you plan on reusing the container across projects.

Remembering that we’re going to swap in full-screen views and the interface for initiating that swap is a fullscreen overlay, our view hierarchy will look like this:

View Controller Containers

Notice that both the containerView and menuView are subviews of the STKMenuViewController’s view and that the menuView is at the end of that array. Being at the end of the subviews array means that the menuView will always appear on top of the containerView – and the containerView’s subview, the view of the selected view controller.

The public interface for the menu view controller will initially look like this.

@interface STKMenuViewController : UIViewController

@property (nonatomic) int selectedViewControllerIndex;
@property (nonatomic, copy) NSArray *viewControllers;

// Don't change the properties of the menuView
@property (nonatomic, weak, readonly) STKMenuView *menuView;
@property (nonatomic, getter = isMenuVisible) BOOL menuVisible;

- (void)setMenuVisible:(BOOL)menuVisible animated:(BOOL)animated;

@end

These are all messages that another object can send to STKMenuViewController. Therefore, other objects can set the child view controllers and can select one of those view controllers from the list. They can also toggle the menu view itself – which is a class we’ll write later. We leave the menuView itself as a readonly property just in case someone wants to look at the values of the menuView, but really, we don’t want them changing properties of it so we leave a note there.

Internally, the STKMenuViewController will have two properties that it doesn’t expose to the outside world because it wants firm control over them. Therefore, in the implementation file, the class extension will look like so:

@interface STKMenuViewController ()

@property (nonatomic, weak) UIView *transitionView;
@property (nonatomic, weak) UIViewController *selectedViewController;

@end

The loadView method of STKMenuViewController will be responsible for setting up the view hierarchy. (We’ll implement STKMenuView later.) It looks like this:

- (void)loadView
{
    UIView *layoutView = [[UIView alloc] init];

    UIView *transitionView = [[UIView alloc] initWithFrame:[layoutView bounds]];
    [transitionView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
    [layoutView addSubview:transitionView];

    STKMenuView *menuView = [[STKMenuView alloc] initWithFrame:[layoutView bounds]];
    [menuView setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight];
    [menuView setDelegate:self];
    [layoutView addSubview:menuView];

    [self setView:layoutView];
    [self setTransitionView:transitionView];
    _menuView = menuView;

    [self setMenuVisible:NO animated:NO];
}

This should all be straightforward. Notice we hang onto the two views we will need to access later: the transition view and the menu view.

A container view controller works just like a normal view controller: its view isn’t loaded until it is needed and it is sent appearance and rotation messages. This means we have to do two things. First, we need to establish the parent-child relationship between these view controllers to ensure appearance and rotation messages are automatically sent to the children when necessary.

Therefore, setViewControllers: looks like this:

- (void)setViewControllers:(NSArray *)viewControllers
{
    for(UIViewController *vc in [self viewControllers]) {
        [vc willMoveToParentViewController:nil];
        if([vc isViewLoaded]
        && [[vc view] superview] == [self transitionView]) {
            [[vc view] removeFromSuperview];
        }
        [vc removeFromParentViewController];
    }

    _viewControllers = viewControllers;

    for(UIViewController *vc in [self viewControllers]) {
        [self addChildViewController:vc];
        [vc didMoveToParentViewController:self];
    }

    if([_viewControllers count] > 0) {
        [self setSelectedViewController:[_viewControllers objectAtIndex:0]];
    } else {
        [self setSelectedViewController:nil];
    }
}

Notice, first, that if the STKMenuViewController already has children, it dissolves the parent-child relationship between those view controllers and removes the view of the selected view controller from its hierarchy. When removing a child view controller, you must manually send willMoveToParentViewController: to the child and pass nil before removing it.

The reverse is true when adding the new children to the parent, they are first added and then manually send didMoveToParentViewController:. Once the children have established their relationship with the parent, the first view controller in the array is automatically selected. (But honestly, I have to look up which one has to be manually called in the documentation each time, so as long as you remember that you have to do something, you’ll be good.)

Note that there is no code for managing the view hierarchy here. That process is handled in setSelectedViewController:. The role of setViewControllers: is to establish parent-child relationships. The role of setSelectedViewController: is to manage the view hierarchy. This method gives the menu controller a uniform way of swapping between view controllers on their views, since we will do this from multiple places. It also helps us with the issue of not knowing whether or not the menu view controller has loaded its view.

- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
    if(![[self viewControllers] containsObject:selectedViewController]) {
        return;
    }

    UIViewController *previous = [self selectedViewController];

    _selectedViewController = selectedViewController;

    if([self isViewLoaded]) {
        [[previous view] removeFromSuperview];

        UIView *newView = [[self selectedViewController] view];
        [newView setFrame:[[self transitionView] bounds]];
        [newView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth];
        [[self transitionView] addSubview:newView];
    }
}

This method ensures that the view controller it is selecting is actually one of its children – which is unlikely, since setSelectedViewController: is not exposed to the public, but might as well check. Then, if and only if the menu view controller’s view has been loaded, the views are swapped. Notice we’re adding the selected view controller’s view to the transitionView based on our earlier conversation about separating roles.

Since setViewControllers: and therefore setSelectedViewController: can be called before the menu view controller has loaded its view, it is possible that the selected view won’t appear once the view is loaded. For that reason, we must call setSelectedViewController: again in viewDidLoad.

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self setSelectedViewController:[self selectedViewController]];
}

Once setSelectedViewController: runs again, the instance variable _selectedViewController won’t change, but its view will now be in the menu view controller’s view hierarchy.

The final property to implement before we move into the next article (where we will implement the menu view) is selectedViewControllerIndex. This comes with a valuable lesson. We have a selectedViewController property – a pointer to the selected view controller – and the array of all the view controllers. Instead of storing the index as an integer value and keeping selectedViewController and selectedViewControllerIndex in sync, we will derive selectedViewControllerIndex from the information we already have.

- (int)selectedViewControllerIndex
{
    return (int)[[self viewControllers] indexOfObject:[self selectedViewController]];
}

The value to this approach is that we don’t open up ourselves to the error of the index and the selected view controller becoming out of sync. The setter method for this property then forwards the actual work of selecting a view controller onto our one-stop shop, setSelectedViewController:, after performing some bounds checking:

- (void)setSelectedViewControllerIndex:(int)selectedViewControllerIndex
{
    if(selectedViewControllerIndex < 0
    || selectedViewControllerIndex >= [[self viewControllers] count]
    || selectedViewControllerIndex == [self selectedViewControllerIndex])
        return;

    [self setSelectedViewController:[[self viewControllers] objectAtIndex:selectedViewControllerIndex]];
}

Since we don’t have an instance variable for selectedViewControllerIndex, you can go ahead and make it dynamic in the implementation block:

@dynamic selectedViewControllerIndex;

In the next article, we’ll implement STKMenuView and hook it up to a gesture recognizer. This will allow the user to swap between view controllers.

Published January 6, 2014
Joe Conway

CEO & Founder at Stable Kernel

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *