Building a Navigation Framework for iOS6

I recently setup a scalable Navigation Architecture for iOS (targeting 6.1) and wanted to share some of the elegance in an approach using the built-in Message Bus available to application developers.

Code for this tutorial: git@bitbucket.org:deepelement/ios-architectures.git

See the NavigationFramework Project

Picking Xibs over Storyboards

Apple introduced the Storyboard in iOS5. This was a big deal for Creative Developers and Designers, but a nightmare for large-scale teams and multi-view apps.

Some of the pitfalls that show up with Storyboards on large teams:

What we want is a flatter, more manageable architecture that allows for view-transitions, work-flow complexities to be resolved in a way that balances the concerns of Source, Design and Creative-Engineering.

Flatness is Good

We want views organized in a way that will allow growth without increasing the complexity of the source. This aids in adopting team members mid-cycle.

We also want to hide the state-workflows (and all of those details) in the implementation, not the organization of the implementation.

So, we should create a Views top-level folder in our Project and guide the design to allow a ton of shallow view folders to exist with all details for those views encapsulated within topical folders.

Flat Views

In addition, we should create a clearly isolated root for Framework components that allow us to bridge our intended implementation into the traditional iOS visual frameworks called Framework.

Flat Views & Framework

Creating Screens - aka View Topics

When creating a view, we want to allow both the Engineers and Creative Developers to flex their muscles without blockers.

The best way to achieve this to support MVC.

  1. Create a Group under the Views conceptual group that represents a particular screen in the application
  2. Within the View topic folder, we need a series of items:

    • View Controller Header File (*.h)
    • View Controller Implementation (*.m)
    • Visual Templates (*.xib) - Visual templates that represent target resolutions and devices

    View Topics

  3. Next, Link the Xib to the Controller interface using the Xcode Interface Builder

see How to link a .xib file to a class file with Xcode 4

Rinse and repeat using this strategy.

Building an Accessible Navigation API

Since iOS SDK 2.0 apple has allowed developers to utilized a clever Message Bus feature called the NotifcationCenter.

An NSNotificationCenter object (or simply, notification center) provides a mechanism for broadcasting information within a program. An NSNotificationCenter object is essentially a notification dispatch table.

The NotificationCenter is unique in that the SDK already static-scopes the same instance, taking care of threading concerns, which makes the solution event-oriented and cross-cutting the entire architecture. This makes it a perfect candidate for abstracting navigation requests and allowing views to be unaware of other instances when a transition is needed.

To take advantage of the NSNotificationCenter, we first need to build an abstract class representing our views.

Building an abstract UIViewController

First, create a group under Framework and add a Header called FrameworkUIViewController.h that subclasses the UIViewController:

#import <UIKit/UIKit.h>
 	
 	@interface FrameworkUIViewController : UIViewController
 	
 	- (void)onDataset:(NSObject*)data;
 	@property(nonatomic, retain, readonly) NSObject *data;
 	
 	@end
 	

Next, create an implementation of FrameworkUIViewController.m in the same folder

#import "FrameworkUIViewController.h"
 	
 	@interface FrameworkUIViewController ()
 	@property(nonatomic, retain, readwrite) NSObject *data;
 	@end
 	
 	
 	@implementation FrameworkUIViewController
 	@synthesize data;
 	
 	- (void)onDataset:(NSObject*)data{
 	    self.data = data;
 	}
 	
 	@end
 	

Now, we can adapt each of the Views topic screen interfaces to extend the FrameworkUIViewController to fold into the navigation solution.

Creating the ViewTypes lookup type

As a method to allow navigation requests to correctly route to a particular view, we will need a typed way of defining exactly what views are in our architecture.

To do this, create a ViewTypes.m enum within the Views group:

typedef enum {
 	    SPLASH    = 1,
 	    LANDING=2,
 	    LOGIN = 3,
 	    EXIT_CONFIRMATION = 4
 	} viewTypes;
 	

Creating a View Resolver

In order to associate a ViewType to a particular combination of Controllers and Xib templates, we need to create a resolver. We can also encapsulate conceptual strategies that allow intelligent template selection here too (iPad vs. iPhone resolution).

Create a new group under Framework called ViewResolver.
Here, we want to create an interface called ViewResolver.h that gives the navigation architecture a way of querying the strategy for the loaded controller:

#import "FrameworkUIViewController.h"
 	#import "ViewTypes.m"
 	
 	@interface ViewResolver : NSObject
 	
 	-(FrameworkUIViewController*) resolve:(NSString*)name:(viewTypes)type;
 	
 	@end
 	

Next, let’s create an implementation relative to our View directory in a file called ViewResolver.m:

#import "ViewResolver.h"
 	
 	@interface ViewResolver()
 	
 	@end
 	
 	@implementation ViewResolver
 	
 	-(FrameworkUIViewController*) resolve:(NSString*)name:(viewTypes)type{
 	
 	    UIViewController *viewController = nil;
 	
 	    switch(type)
 	    {
 	        case LOGIN:
 	            viewController = [self manufactureViewController:@"Login"];
 	            break;
 	        case SPLASH:
 	            viewController = [self manufactureViewController:@"Splash"];
 	            break;
 	        case LANDING:
 	            viewController = [self manufactureViewController:@"Landing"];
 	            break;
 	        case EXIT_CONFIRMATION:
 	            viewController = [self manufactureViewController:@"ExitConfirmation"];
 	            break;
 	    }
 	
 	    return viewController;
 	}
 	
 	// Used to manufacture type/resource names based on a common view name.
 	// Customize this to the strategy of resource naming conventions.
 	// Note: this also is responsible for nib selection based on device type
 	- (UIViewController*) manufactureViewController:(NSString *) viewName
 	{
 	    NSString *controllerName = [NSString stringWithFormat:@"%@UIViewController", viewName];
 	    NSString *viewNibName;
 	    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
 	        viewNibName = [NSString stringWithFormat:@"%@UIView_iPhone", viewName];
 	    }
 	    else{
 	        viewNibName = [NSString stringWithFormat:@"%@UIView_iPad", viewName];
 	    }
 	    return [[NSClassFromString(controllerName) alloc] initWithNibName:viewNibName bundle:nil];
 	}
 	
 	@end
 	

Building a Notification Responder

When designing a navigation controller, we should take advantage of the ‘stack-based’ UINavigationController available in the SDK. This controller allows for transition animations, forward/backward push/pop instancing and a variety of optimizations around memory management.

Let’s start by creating a group in the Framework root called ReponderNavigationController.

In this folder, we want to create a header for our responder class in the new group called ResponderNavigationController.h:

#import <UIKit/UIKit.h>
 	#import "ViewResolver.h"
 	
 	@interface ResponderNavigationController : UINavigationController
 	
 	@end
 	

Next, create an implementation in the same folder with called ResponderNavigationController.m that we will define our navigation details within:

#import "ResponderNavigationController.h"
 	
 	@interface ResponderNavigationController ()
 	
 	@end
 	
 	@implementation ResponderNavigationController
 	
 	- (id) init {
 	    self = [super init];
 	    if (self != nil) {
 	       // do init stuff here
 	    }
 	    return self;
 	}
 	
 	@end
 	

This is pretty bare-bones, but we get a ton of cool features just by extending the UINavigationController base.

see UINavigationController Class Reference for more details

Within the ResponderNavigationController, lets define two navigation notifications:

In the constructor of our controller, lets subscribe to the messages and listen for them in a handler:

@implementation ResponderNavigationController
 	
 	- (id) init {
 	    self = [super init];
 	    if (self != nil) {
 	
 	        // Subscribe to the notification center for navigation events
 	        [[NSNotificationCenter defaultCenter] removeObserver:self];
 	        [[NSNotificationCenter defaultCenter] addObserver:self
 	                                                 selector:@selector(handleNotification:)
 	                                                     name:@"NavigateToNotification"
 	                                                   object:nil];
 	        [[NSNotificationCenter defaultCenter] addObserver:self
 	                                                 selector:@selector(handleNotification:)
 	                                                     name:@"NavigateBackNotification"
 	                                                   object:nil];
 	    }
 	    return self;
 	}
 	
 	// Called when a navigation notification is available
 	- (void) handleNotification:(NSNotification *) notification
 	{
 	    if ([[notification name] isEqualToString:@"NavigateToNotification"]){
 	
 	        NSLog (@"Navigating forward");
 	    }else
 	        if ([[notification name] isEqualToString:@"NavigateBackNotification"]){
 	            NSLog (@"Navigating back");
 	        }
 	}
 	
 	@end
 	

When a notification is fired, the SDK allows for userInfo to be passed by the caller.

Next, let’s extend our implementation to allow for callers to control the behavior of the navigation event based on their userInfo parameters. We will need for the sender of the notification to tell us what view they want to navigate to with post data:

- (void) handleNotification:(NSNotification *) notification
 	{
 	    NSDictionary *userInfo = [notification userInfo];
 	    if ([[notification name] isEqualToString:@"NavigateToNotification"]){
 	
 	        NSString *viewName = [notification object];
 	        NSObject *postData = [userInfo objectForKey:@"data"];
 	        NSNumber *num = [userInfo objectForKey:@"viewType"];
 	        int viewTypeIntValue = [num intValue];
 	
 	        // Manufacture the View and update the root view on self
 	        UIViewController *viewController = [self.viewResolver resolve:viewName :viewTypeIntValue];
 	
 	
 	    }else
 	        if ([[notification name] isEqualToString:@"NavigateBackNotification"]){
 	            NSLog (@"Navigating back");
 	        }
 	}
 	

Integrating the Navigation Responder

Now that we have a fancy navigation handler, we need to take a few steps to integrate into the core architecture of our application before we can actually starting moving around the app.

First, we need to ‘mount’ the ResponderNavigationController into the app through the pre-existing deAppDelegate.m implementation:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 	{
 	    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 	
 	    // Setup window defaults
 	    self.window.backgroundColor = [UIColor whiteColor];
 	    [self.window setNeedsDisplay];
 	    [self.window makeKeyAndVisible];
 	
 	    // Create the view controller
 	    ViewResolver *viewResolver = [ViewResolver alloc];
 	
 	    // Configure the primary navigation controller
 	    self.responderNavigationController = [[ResponderNavigationController alloc] initWithRootViewController:[viewResolver resolve:@"Splash" :SPLASH]];
 	    self.responderNavigationController.viewResolver = viewResolver;
 	    [self.responderNavigationController init];
 	    [ self.responderNavigationController  setNavigationBarHidden:TRUE];
 	    self.window.rootViewController = self.responderNavigationController;
 	
 	    return YES;
 	}
 	

Note: here we also go-ahead and use initWithRootViewController during the instantiation of the ResponderNavigationController type to make sure the user has something to stare at while things are bootstrapping.

This will allow the ResponderNavigationController full control over the applications navigation workflow.

Next, we need to extend the ResponderNavigationController.m implementation to actually use the ViewResolver and ViewTypes to correctly transition between pages:

- (void) handleNotification:(NSNotification *) notification
 	{
 	    NSDictionary *userInfo = [notification userInfo];
 	
 	    if ([[notification name] isEqualToString:@"NavigateToNotification"] &&
 	        self.viewResolver != nil){
 	
 	        NSString *viewName = [notification object];
 	        NSObject *postData = [userInfo objectForKey:@"data"];
 	        NSNumber *num = [userInfo objectForKey:@"viewType"];
 	        int viewTypeIntValue = [num intValue];
 	
 	        // Call the Resolver
 	        UIViewController *viewController = [self.viewResolver resolve:viewName :viewTypeIntValue];
 	
 	        if(viewController != nil)
 	        {
 	            NSNumber *clearBackstack = [userInfo objectForKey:@"clearBackstack"];
 	            if([clearBackstack isEqualToNumber:[NSNumber numberWithInt:1]])
 	            {
 	                if([[self viewControllers] containsObject:viewController]) {
 	                    [self popToViewController:[NSArray arrayWithObject:viewController] animated:YES];
 	                } else {
 	                    [self setViewControllers:[NSArray arrayWithObject:viewController] animated:YES];
 	                }
 	            }
 	            else
 	                [self pushViewController:viewController animated:YES];
 	
 	
 	            // The post data event can't occur before the controller has become the root.
 	            // Otherwise, we won't be able to bind to outlets
 	            if(postData != nil && [viewController isKindOfClass:[FrameworkUIViewController class]])
 	            {
 	                [((FrameworkUIViewController *)viewController) onDataset:postData];
 	            }
 	        }
 	        else{
 	            NSLog (@"Navigation Failure for ViewName %@", viewName);
 	        }
 	    }else
 	        if ([[notification name] isEqualToString:@"NavigateBackNotification"]){
 	            NSLog (@"Navigating back");
 	            [self popToRootViewControllerAnimated:YES];
 	        }
 	}
 	

Data Posting

We also want the ability for views to communicate state around the Application without creating static providers.

Notice that in our FrameworkUIViewController definition, we included a nifty data property that the view implementor can rely on being called when NSObject data is included in a Navigation Notification.

@interface FrameworkUIViewController : UIViewController
 	
 	- (void)onDataset:(NSObject*)data;
 	@property(nonatomic, retain, readonly) NSObject *data;
 	
 	@end
 	

Each view can override this method and do their relevant binding therein:

// Override the onDataSet method of the FrameworkUIViewController
 	- (void)onDataset:(NSObject*)data{
 	    [super onDataset:data];
 	
 	    NSString *username = data;
 	    [self welcomeTextOutlet].text = [NSString stringWithFormat:@"Welcome %@!",username];
 	}
 	

The onDataset method is called after instantiation & attachment as the root controller. This is because attempts to bind data to outlets will fail if the owner’s UIViewContoller is not attached to the visual tree.

Examples of the API in Action

Now that the ResponderNavigationController is using the NSNotificationCenter to listen for navigation requests, we now have the ability to send notifications from anywhere in our architecture.

To Navigate forward to the Login View:

 NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
 	    [userInfo setObject:[NSNumber numberWithInt: LOGIN] forKey:@"viewType"];
 	
 	    [[NSNotificationCenter defaultCenter]
 	     postNotificationName:@"NavigateToNotification"
 	     object:self
 	     userInfo:userInfo];
 	

To Navigate Forward to the Landing View, posting data:

 NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
 	    [userInfo setObject:[NSNumber numberWithInt: LANDING] forKey:@"viewType"];
 	    [userInfo setObject:[[self usernameOutlet] text] forKey:@"data"];
 	
 	    [[NSNotificationCenter defaultCenter]
 	     postNotificationName:@"NavigateToNotification"
 	     object:self
 	     userInfo:userInfo];
 	

To Navigate backwards, popping the current view from the stack:

[[NSNotificationCenter defaultCenter]
 	     postNotificationName:@"NavigateBackNotification"
 	     object:self
 	 userInfo:nil];
 	

To Navigate to a view, clearing all back-stack history:

NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
 	[userInfo setObject:[NSNumber numberWithInt: LANDING] forKey:@"viewType"];
 	[userInfo setObject:[NSNumber numberWithBool:true ] forKey:@"clearBackstack"];
 	
 	[[NSNotificationCenter defaultCenter]
 	 postNotificationName:@"NavigateToNotification"
 	 object:self
 	 userInfo:userInfo];
 	

Conclusion

As you can tell, the NSNotificationCenter is a perfect Message Bus to orchestration a decoupled navigation framework. Something that MVVM guru’s like me really appreciates.

Final Architecture

Grab the NavigationFramework project source and use this pattern as a bootstrap for your team.

Funnel any feedback/suggestions to todd at deepelement.com