Home   

Avoiding Stringly Typed Code in Storyboard View Controllers

At WWDC, Apple heavily promoted storyboards for both iOS and Mac development. Although storyboards aren’t a good fit for every project, Apple is investing a lot of effort in improving storyboards, so it’s becoming increasingly important for iOS and Mac developers to know how to use them. Segues are a fundamental building block of Storyboards, but the APIs provided to work with segues are some of the most stringly typed constructs in Cocoa and CocoaTouch. However, with a little reflection, we can create a better interface.

When using storyboards, it’s not possible to connect IBOutlets between view controllers. When a view controller needs to store a reference to another view controller, it’s common practice to override -prepareForSegue:sender: and store the segue’s -destinationViewController. Since this one method is called for every segue, and since the segues can only be differentiated by their identifiers, this leads to unwieldy if-else statements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString: @"nextView"]) {
        [self setSegueTarget_nextView: [segue destinationViewController]];
        // Perform any other actions for this segue
    }
    else if ([[segue identifier] isEqualToString: @"segue2"])
    {
        // Perform actions for segue2
    }
    else if ([[segue identifier] isEqualToString: @"segue3"])
    {
        // Perform actions for segue3
    }
    ...
    else if ([[segue identifier] isEqualToString: @"segue12"])
    {
        // Perform actions for segue12
    }
}

Since most view controllers only contain a few segues, I’ve always just gritted my teeth and followed this pattern when working with storyboards. But OS X storyboards will contain many more segues, and this pattern obviously does not scale well.

To solve this problem, we can override -prepareForSegue:sender: to call methods based on the segue’s identifier. The following code does two things. First, if the view controller contains a property named segueTarget_<segue identifier>, it assigns the segue’s -destinationViewController to that property. Second, if the view controller contains a method named -prepareForSegue:<segue identifier>sender:, it calls that method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    NSString *identifier = [segue identifier];
    
    NSString *segueTargetString =
        [NSString stringWithFormat: @"setSegueTarget_%@:", identifier];
    
    SEL segueTargetSelector = NSSelectorFromString(segueTargetString);
    
    if ([self respondsToSelector: segueTargetSelector]) {
        IMP imp = [self methodForSelector:segueTargetSelector];
        void (*segueTargetImp)(id, SEL, id) = (void *)imp;
        
        segueTargetImp(self,
                       segueTargetSelector,
                      [segue destinationViewController]);
    }
    
    NSString *prepareForSegueSelectorString =
        [NSString stringWithFormat:@"prepareForSegue_%@:sender:", identifier];
    
    SEL prepareForSegueSelector =
        NSSelectorFromString(prepareForSegueSelectorString);
    
    if ([self respondsToSelector:prepareForSegueSelector]) {
        IMP imp = [self methodForSelector:prepareForSegueSelector];
        
        void (*prepareForSegueImp)(id, SEL, UIStoryboardSegue *, id) =
            (void *)imp;
        
        prepareForSegueImp(self, prepareForSegueSelector, segue, sender);
    }
}

This behavior is inherited by both Objective-C and Swift subclasses of this view controller.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "ViewController.h"

@interface ViewController ()

@property(nonatomic, readwrite, weak) UIViewController *segueTarget_nextView;

@end

@implementation ViewController

- (void)prepareForSegue_nextView:(UIStoryboardSegue *)segue sender:(id)sender
{
    NSLog(@"Called performSegue_nextView:sender:");
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    [super prepareForSegue:segue sender:sender];
    
    NSLog(@"After prepareForSegue, segueTarget_nextView is %@",
          [self segueTarget_nextView]);
}

@end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import UIKit

class SwiftViewController: BUCAutoSegueViewController
{
    var segueTarget_finalView : UIViewController? = nil;
    
    func prepareForSegue_finalView(segue: UIStoryboardSegue!,
                                   sender: AnyObject!)
    {
        print("Called prepareForSegue_finalView");
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue!,
                                  sender: AnyObject!)
    {
        super.prepareForSegue(segue, sender: sender)
        
        print("After prepareForSegue, segueTarget_finalView is
              \(String(describing: self.segueTarget_finalView))")
    }
}

Of course, this is not a perfect solution. Like the stringly-typed version, this is brittle: It will break if you rename your segue without renaming the associated properties and methods. It would be much better if the segues themselves had targets and actions which could be set independent of their identifier.

I have created a Github repository with a view controller that implements this technique, as well as a similar technique for -shouldPerformSegueWithIdentifier:sender:. It’s MIT-licensed, so please feel free to use it in your own code.