Showing posts with label UIStoryboard. Show all posts
Showing posts with label UIStoryboard. Show all posts

Sunday, June 10, 2012

Linking Storyboards

Summary

Previously I wrote about best practices with UIStoryboards. Since then I have continued to explore the possibilities of storyboards. With today being the eve of WWDC 2012, I wanted to share one of the tricks I have learned.

A key practice of storyboards is to break them down into natural, reusable modules. The downfall to this decomposition is when we need to transition from one storyboard to another. Storyboards were supposed to save us from the tedious transition code, but decomposition brings some of it back. Fortunately, there is a way to remove this pain.

Before I go into any details, I want to start with a disclaimer. I am not going to label this as best practice…at least not yet. All good design patterns need to stand the test of time. I have not used this trick enough to know if it works in all cases. Also, with this week being WWDC, Apple may expand UIStoryboards to include segues between storyboards.

Implementing

You should first download my code from GitHub. Following along will help in understanding how everything works.

The first thing to do is build your storyboards as you normally would. It's best if you break them down into natural modules. The rule of thumb is, if you can give each storyboard a meaningful name, then you have probably decomposed them well.

Now that you have your various storyboards, you can begin connecting them. To do this, you need to identify where you will transition into a different storyboard. At each of these points, create a blank UIViewController. This will represent the view in the other storyboard you will transition to.

Next you need to create a segue to each of these surrogate scenes. You can choose a Push, Modal, or custom segue according to your needs. They should look something like the picture below:



Once you have your surrogate scene in place, we can make the magic happen. We get to use a little-known tool of Interface Builder. It is the User Defined Runtime Attributes tool. This tool is hidden away in the Identity Inspector (3rd tab of the utility view). Up until now, you have probably used this inspector only to set a custom class. It also allows us to set the values of any property of any view we may be using in the storyboard.

First, we need to change the surrogate's class to RBStoryboardLink. Don't forget to include my RBStoryboardLink class in your app. Next, we can add a couple properties instructing the link what to transition to. The first, and required, attribute is "storyboardName" This is the name of the storyboard file you are going to transition to (without the .storyboard extension). The second attribute is optional. It called "sceneIdentifier". This is the UIStoryboard identifier you have given to a scene in the destination storyboard. If you ignore this attribute, then RBStoryboardLink will simply transition to the first scene in the storyboard. This is often what you want. Your Identity Inspector should now look something like the picture below:



Explanation

RBStoryboardLink works much like a symbolic link in your file system. A symbolic link essentially looks and acts like file. Likewise, RBStoryboardLink looks and acts much like a storyboard scene.

The idea behind RBStoryboardLink is simple. At a high level, it does the following:
  1. Creates the destination scene.
  2. Adds the view controller as a child of itself using the iOS 5 container API.
  3. Copies all the destination scene's properties into RBStoryboardLink.
Conclusion

RBStoryboardLink is a clean, easy-to-use technique to link storyboards without leaving Interface Builder. With WWDC this week, Apple may release similar functionality built directly into Interface Builder. If not, expect to use this technique frequently in your projects.

Monday, May 7, 2012

UIStoryboards and UITabBarController

Previously I wrote about decomposing UIStoryboards into modules. One of the most natural modules are the tabs of a tab bar controller. Since segues can't cross storyboards, we can't use a UITabBarController in a storyboard. This means the UITabBarController must be written in code. I have put together an easy pattern for building UITabBarControllers in code.

UIStoryboard Category

The first step is to write a category on UIStoryboard. It will look something like this:

@implementation UIStoryboard (YourExtras)

+ (UIStoryboard *)searchStoryboard {
    return [UIStoryboard storyboardWithName:@"SearchStoryboard" bundle:nil];
}

+ (UIStoryboard *)mapStoryboard {
    return [UIStoryboard storyboardWithName:@"MapStoryboard" bundle:nil];
}

+ (UIStoryboard *)listStoryboard {
    return [UIStoryboard storyboardWithName:@"ListStoryboard" bundle:nil];
}

+ (UIStoryboard *)favoritesStoryboard {
    return [UIStoryboard storyboardWithName:@"FavoritesStoryboard" bundle:nil];
}

+ (UIStoryboard *)contactStoryboard {
    return [UIStoryboard storyboardWithName:@"ContactStoryboard" bundle:nil];
}

+ (UIStoryboard *)homeStoryboard {
    return [UIStoryboard storyboardWithName:@"HomeStoryboard" bundle:nil];
}

@end

The purpose of this category is to abstract away the storyboard file names. Should the storyboard name (or bundle) change, we will only need to change it in one place. Such methods are useful not just tabs but also for any storyboard module. For the purpose of tabs, each storyboard generally should have a UINavigationController as its initial view.

UITabBarController Category

The next step involves a category I wrote. It takes in an array of UIStoryboards, then instantiates each storyboard as a tab. It saves writing a little bit of code between each project.

@implementation UITabBarController (RBExtras)

+ (UITabBarController *)tabBarControllerWithStoryboardTabs:(NSArray *)tabs {

    UITabBarController * tabBarController = [UITabBarController new];
    NSMutableArray * instantiatedTabs = [NSMutableArray arrayWithCapacity:[tabs count]];

    // Instantiates each of the storyboards.
    [tabs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {

        NSAssert([obj isKindOfClass:[UIStoryboard class]],
                          @"Expected UIStoryboard, got %@",
                          NSStringFromClass([obj class]));

        [instantiatedTabs addObject:[obj instantiateInitialViewController]];
    }];

    [tabBarController setViewControllers:instantiatedTabs];

    return tabBarController;
}

@end

This category can be found on my Github.

App Delegate Superclass

Next is an app delegate superclass I wrote. This saves a few more lines of code from each tab-based app.

@implementation RBAppDelegate

@synthesize window = _window;

- (void)setUpTabBasedAppWithTabs:(NSArray *)tabs block:(RBTabBarCustomizationBlock)block {

    NSAssert(tabs, @"No tabs given.");
    NSAssert([tabs count] >= 2, @"Insufficient number of tabs.");

    // Sets up the tab bar controller.
    UITabBarController * tabBarController = [UITabBarController tabBarControllerWithStoryboardTabs:tabs];

    // The block is called to allow customization of the tab bar controller.
    if (block) block(tabBarController);

    // Sets up the window.
    UIWindow * window = [UIWindow new];
    [window setScreen:[UIScreen mainScreen]];
    [window setRootViewController:tabBarController];
    [window makeKeyAndVisible];
    [self setWindow:window];
}

@end

This superclass completes the set up of the tab bar controller. It provides a block for any customizations of the tab bar controller. You can also find this on my Github.

Your App Delegate

The final step is to call the set up method. Don't forget to subclass RBAppDelegate first.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // Creates the tabs.
    NSArray * tabs = [NSArray arrayWithObjects:
                                  [UIStoryboard mapStoryboard],
                                  [UIStoryboard listStoryboard],
                                  [UIStoryboard searchStoryboard],
                                  [UIStoryboard favoritesStoryboard],
                                  [UIStoryboard contactStoryboard],
                                  nil];

    [self setUpTabBasedAppWithTabs:tabs
                                                   block:
     ^(UITabBarController * tabBarController) {
         tabBarController.tabBar.selectedImageTintColor = [UIColor greenColor];
     }];

    return YES;
}

The block for customizing the tab bar controller is optional, but I have shown the potential use of such a block. In this case I am setting the color of the selected tab icons.

Conclusion

Within a few lines of code, we can have a tab-based app constructed from several storyboards. Other UIStoryboard patterns can be created using similar techniques.

Sunday, May 6, 2012

Reverse Segues

UIStoryboards make transitioning between views very easy. However, segues work strictly in one direction. I have been asked several times what is the best way to send information back to the previous view controller. Unfortunately, there is no such thing as a reverse segue. In response, I have found four clean, useful patterns to create the needed behavior:
  1. Delegate
  2. Block
  3. Shared memory
  4. NSNotificationCenter
Each one has its advantages and disadvantages. I will discuss each one in turn.

Delegate

Summary

Cocoa uses delegates everywhere, so why not here too?

Code Sample

Let's say view controller X pushes view controller Y via segue. Y specifies a delegate protocol that it will call to pass the needed information. X conforms to that protocol and sets itself as Y's delegate. When Y is dismissed, it calls the delegate method.

// In view controller X
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    [[segue destinationViewController] setDelegate:self];
}

- (void)objectUpdated:(id)object {
    // Do something with the object.
}

// In view controller Y
@protocol MyDelegateProtocol
- (void)objectUpdated:(id object);
@end

- (void)viewWillDisappear:(bool)animated {
    [super viewWillDisappear:animated];

    [[self delegate] objectUpdated:[self object]];
}

Pros/Cons

Delegation is easy enough to implement. However, in this case, the delegate protocol probably has just one method. This hardly seems worth the effort to create the protocol. Because of this, I do not recommend delegates. I instead favor the next option: blocks. If you need more than one callback, a delegate may be more appropriate.

Blocks

Summary

Blocks are great for making callbacks and can easily replace any delegate.

Code Sample

Now when X pushes Y, it also gives Y a block. When Y finishes, it calls the given block and hands it the information to pass on. The block can then do whatever it needs with that information.

// In view controller X
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    [[segue destinationViewController] setBlock:^(id object) {
        // Do something with the object.
    }];
}

// In view controller Y
typedef void(^MyBlock)(id object);

- (void)viewWillDisappear:(bool)animated {
    [super viewWillDisappear:animated];

    if (self.block)
        self.block(self.object);
}

Pros/Cons

Blocks have two great advantages over delegates. First, they don't require a protocol. For convenience, you may write a one-line block typedef, but that is all. Second, blocks keep code where it is relevant, rather in a different method. For most cases, you should favor blocks.

Shared Memory

Summary

Sometimes you may push a new view so it can edit an object. In this case, you can avoid a callback entirely.

Code Sample

When pushing Y, just pass it a pointer to the object of interest. Any changes Y makes to the shared object will directly affect X.

// In view controller X
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    [[segue destinationViewController] setObject:[self object]];
}

Pros/Cons

This solution is the simplest of the four. However, it may not be suitable in some situations. One place this pattern doesn't do well is if you want view controller Y to have a Done and Cancel button. You would need to undo all the changes made to your object if the Cancel button is pressed. This could be made easier by using an NSUndoManager, but that may be overkill for what you want.

There is one particular place that this pattern is especially useful. When using Core Data with iOS 5, you may want view controller Y to have an NSManagedObjectContext that is a child of view controller X's NSManagedObjectContext. All you need to do is create the MOC in X, set the MOC's parent, and pass it on to Y.

NSNotificationCenter

Summary

Using NSNotificationCenter to pass around information is the least used option and the most work, but it can have some nice advantages worth mentioning.

Code Sample

Rather than have X tell Y it is interested in its information, X lets NSNotificationCenter know it is interested in an event.

// In view controller X
- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                                                          selector:@selector(consumeNotifcation:)
                                                                              name:MyNotification
                                                                             object:nil];
}

- (void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                                                    name:MyNotification
                                                                                   object:nil];
    [super viewDidUnload];
}

- (void)consumeNotification:(NSNotification * notification) {
    // Grab the information from the notification.
}

// In view controller Y
NSString * const MyNotification = @"MyNotification";
NSString * const MyObject = @"MyObject";

- (void)viewWillDisappear:(bool)animated {
    [super viewWillDisappear:animated];

    NSDictionary * userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                                                [self object], MyObject,
                                                nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:MyNotification
                                                                                           object:self
                                                                                       userInfo:userInfo];
}

Pros/Cons

Obviously this is a lot more code than the other options. It comes with two key advantages. First, this pattern makes multiple, unrelated observers very easy. Second, it allows for looser coupling between views. X and Y (and any other views/non-views) essentially don't know anything about each other. They simply send or receive information through NSNotificationCenter. One place I use this pattern is with an app-wide settings view. When the settings are changed, different parts of the app may need to know immediately when the change is made. For example, I may have a setting that can turn off downloading over the cell network. Both the networking code and the UI will need to know about this change. The notification makes sure that they are informed immediately without needing to hard code the interactions.

Conclusion

When passing information to a previous view controller, favor blocks. However, keep the other options in mind. They have their advantages too.

Other Information

For more information on UIStoryboards, check out my best practices.

Friday, January 13, 2012

UIStoryboard Best Practices

UIStoryboard is a hot topic right now in iOS. However, there have been many misconceptions on the topic. The first reaction to storyboards (what I have seen anyway) is that they are a panacea for all situations. Following that reaction I saw many people claim that storyboards are awful and broken. The truth is, both these ideas are wrong. During WWDC 2011 Apple may have over-promoted storyboards, thus giving the wrong impression and leading to unmet expectations. Let me correct this now: UIStoryboard is not an all-purpose solution, it is another tool for your toolbox.

My intent here is to provide the best practices for UIStoryboard. I will list my lessons learned, but there are probably more yet to learn. I expect over time that this list will expand to include other best practices for UIStoryboard.

Break your storyboard into modules

The first mistake developers have been making with storyboarding is producing large, monolithic storyboards. A single storyboard may be suitable for small apps but certainly not for large apps. Don't forget one of the key principles of programming and abstraction: decomposition. We decompose our code into modules to make them more reusable and reduce maintenance costs. The same is true for user interfaces, and it applies whether you are using UIStoryboards, XIBs, or anything else. There is an even deeper principle wanting to emerge: when code and user interfaces are decomposed together, then you have truly useful and reusable code, user interfaces, and products (you can quote me on this).

The first question to ask is "how do I identify the natural modules of my app?" Let's start with something easy. Many apps are based on a tab bar controller. Each tab is a natural module and you hence you can put them in their own storyboard. Next, if you have two tabs (or any two views for that matter) with a segue to a common view, then that common view (and its following hierarchy) is almost certainly another natural module. Once you have pruned your view hierarchy, you will probably find that each module can be given a name. For example, you may have your login storyboard, settings storyboard, about storyboard, main storyboard, etc. If you can give a fitting name to each module, then you have decomposed your hierarchy well.

Keep in mind that having a single view in a storyboard is not a bad thing. This is particularly true with table views. The power of static and prototype table view cells is very useful.

You should also note that breaking storyboards into modules makes them more friendly to version control and sharing among a team.

You don't need to convert everything at once

When you first saw storyboards, you may have had an urge to convert everything over to UIStoryboard. I know I did. Be aware that this does take time and you may not gain any immediate benefits from switching everything. Fortunately, you don't need to do it all at once. You can switch over the parts of your app that will benefit the most now. Then over time you can slowly switch out others.

Make custom UITableViewCell subclasses for prototype cells

Probably the best feature of UIStoryboard is prototype cells. They make custom table view cells easy. Prototype cells can be easily customized; however, the UITableViewCell class may not specify the IBOutlets you need. A custom subclass also gives you another advantage. You can create a -setupWith<#Object#>: method. This takes your setup code out of your table view controller and into the cell itself. This is where you can gain some great reuse.

Not only can you create custom UITableViewCell subclasses, but you can have one subclass service many prototype cells. For example, you may have a cell that takes a Person object. In one table view your cell may have main label on the left and a detail label on the right. A different table view may use the same cell but have a main label above and a detail label below. Your cell subclass handles the content, but the prototype cell handles the look and feel. Again we gain great reuse and flexibility by decomposing code and views together.

Don't forget about IBAction, IBOutlet, and IBOutletCollection

Segues are nice and can do a lot, but don't forget about the other IB fundamentals. Segues can't cross storyboards, but IBActions can. IBActions can also clean up an unnecessary spaghetti segue mess and reduce the need for custom segues. Everything has their place and their use; they are all tools at your disposal.

While on the topic of IBActions and family, I should cover encapsulation. Most, if not all, your actions and outlets do not need to be public. So, rather than put them in your public headers, put them in a class extension. See here for more details. Xcode/Interface Builder didn't handle this well in the past but now there are no problems. This keeps your interfaces much cleaner and easier for others to understand.

Don't forget about XIBs

Even though storyboards are very similar to XIBs, they still have a place. Simple, one-view modules may be better represented as XIBs. Also, XIBs can hold custom views without an associated view controller. UIStoryboard requires that view controllers be the basic unit.

You don't need to have a main storyboard

You may have noticed your app settings now has a "Main Storyboard" setting. To use UIStoryboard you are not required to use this. In fact, my latest project has neither a main storyboard nor a main XIB. It uses a programmatic UITabBarController. From there I load all my tab storyboards. The beauty of it all is that it doesn't matter whether I use UIStoryboards, XIBs, or code. All three options are flexible enough to work with each other in any combination.

Specify the name of your app delegate in main.m

As I mentioned the other day, you may need to change your main.m file if it doesn't specify your app delegate's name. It should look something like this:

UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]))

Put your storyboards in a category

To get one of your storyboards, you need to call +storyboardWithName:bundle:. Be wary any time you need to hard code a string for a key, especially one that can change frequently. Should you ever need to rename your storyboard, you will have to change all references to that storyboard. My favorite way to handle this is with a category. It looks something like this:

@implementation UIStoryboard (MyApp)

+ (UIStoryboard *)mainStoryboard {
    return [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
}

@end

The benefit of the category approach is that if you ever rename a storyboard, you only need to change the hard coded string in one place. It also makes your code more readable.

Summary

If you take anything from this post you should remember these:
  1. Decompose your code and your views.
  2. UIStoryboard is a great tool, but it's not the only tool.
I hope these tips and best practices help promote proper use of UIStoryboard. I invite all my readers to let me know of other best practices they encounter.

Thursday, December 15, 2011

Migrating to UIStoryboard

I’ve just recently started using UIStoryboard and so far I’ve been very pleased with it. Building the UI is faster, easier, and requires less code. It’s also immensely helpful to get a bird’s eye view of your full workflow. However, when migrating an old app from using regular XIBs to UIStoryboard I did encounter some troubles. When launching the app, I would get the following error messages:

“The app delegate must implement the window property if it wants to use a main storyboard file.”
and
“Applications are expected to have a root view controller at the end of application launch.”

I could clearly see that my app delegate was implementing the window property as expected. Fortunately, StackOverflow came to the rescue. It turns out that main.m needs to be modified when switching to UIStoryboard from XIBs (in older projects). The change looks like this:

// Old main.m
UIApplicationMain(argc, argv, nil, nil);

// New main.m
UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]));

The documentation for this parameter reads:

The name of the class from which the application delegate is instantiated. If principalClassName designates a subclass of UIApplication, you may designate the subclass as the delegate; the subclass instance receives the application-delegate messages. Specify nil if you load the delegate object from your application’s main nib file.

Basically, if you aren’t using XIBs, you must let UIApplication know what your app delegate’s name is. You will also notice that any new projects created with the latest version of Xcode will specify the app delegate, even if you are using XIBs. This prevents any problems should you migrate to UIStoryboard later.