Delegation between View Controllers using Storyboards (for iPhone)

Delegation between UIViewControllers is a common pattern. We might have a class graph in which there is a master view controller (that communicates with the model and all the other view controllers). Each main view in the application will have its own view controller. It is more efficient to have these view controllers delegate up to the master controller than to have each one communicate directly with the model.

When we use Nib files to represent views, the delegate class can name itself the delegate in the initWithNibName: bundle: method, or in viewDidLoad. Unfortunately, this practice does not work with storyboards. Since there is no Nib file, initWithNibName: bundle: is never called. Setting a class to be the delegate of another class in viewDidLoad relies on being able to “lazily instantiate” the other class in the delegate class, then setting its delegate property to self. This will not work because in a storyboard setting, the new class is actually instantiated during the segue itself (with modal segues). If we do instantiate the new class in viewDidLoad, it is not the same object that will be used.

So what do we do? In this blog, we’ll look at how to perform delegation with a modal view controller. Start Xcode and create a new project. Use the Single View Application template, name the project Delegation, and make sure to check the “Use Storyboards” checkbox as below:

Click Next, choose a location to save the project, and click Create. When the project has loaded, create a new UIViewController class named SecondViewController. Here is SecondViewController.h:

#import <UIKit/UIKit.h>

@protocol SecondControllerDelegate <NSObject>

(void) userDidMakeChoice:(NSUInteger)choice;

@end

@interface SecondViewController : UIViewController

@property (nonatomic, assign) id <SecondControllerDelegate> delegate;

(IBAction)choiceMade:(UISegmentedControl *)sender;

@end

We’ve set up a protocol named SecondControllerDelegate to hold a single delegate method, userDidMakeChoice:. When the user selects an option in a segmented control (in the view), the SecondViewController’s choiceMade: action method will fire, calling the delegate method before dismissing the view controller (which will reveal the main view controller once again).

Let’s look at the implementation of SecondViewController.m:

#import "SecondViewController.h"

@interface SecondViewController ()

@end

@implementation SecondViewController

@synthesize delegate;

(IBAction)choiceMade:(UISegmentedControl *)sender
{
    [self.delegate userDidMakeChoice:sender.selectedSegmentIndex];
    [self dismissViewControllerAnimated:YES completion:nil];
}

@end

After synthesizing the delegate property, we implement choiceMade:. First, we call the delegate method, then dismiss this view controller by calling dismissViewControllerAnimated: completion: (with a nil value for completion, since we don’t need any code to run after the controller is dismissed).

Open MainStoryboard.storyboard in Interface Builder by clicking on it in the navigator. Drag a new View Controller to the main view, next to the first view controller. Place a single button labeled Choose… and a UILabel above the button with the text left blank on the first view controller, then CTRL – click and drag from this button to the second view controller to create a segue. Choose Modal when prompted for a segue type.

Now place a UISegmentedControl and a label on the second view controller. When finished, the Interface Builder should look like this (the label is on the first view controller, it is not selected and therefore invisible):

Select the second view controller, and change its class to SecondViewController as shown here:

Now select the segue itself, and give it an identifier a shown:

Now we’re ready to write the code for ViewController. Here is ViewController.h:

#import <UIKit/UIKit.h>
#import "SecondViewController.h"

@interface ViewController : UIViewController
<SecondControllerDelegate>

@property (nonatomic, weak) IBOutlet UILabel *outcome;

@end

We’ve imported SecondViewController for two reasons, to adopt its protocol, and to get its type, which will become important in a moment. The IBOutlet UILabel property “outcome” will be wired to the “textless” label on this view controller’s view.

Now, ViewController.m:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize outcome;

(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"toSecondVC"]) {
        SecondViewController *secondVC =
            (SecondViewController *) segue.destinationViewController;
        [secondVC setDelegate:self];
    }
}

(void)userDidMakeChoice:(NSUInteger)choice
{
    switch (choice) {
        case 0:
            self.outcome.text = @"One chosen";
            break;
        case 1:
            self.outcome.text = @"Two chosen";
            break;
        case 2:
            self.outcome.text = @"Three chosen";
            break;
           
        default:
            break;
    }
}

@end

The important thing here is the prepareForSegue: sender: method, which will be called right before a segue fires. If there is only one segue in an app, there is no need to check the identifier (or even to set an identifier on the segue at all). We do so here to illustrate the more common case, in which there would be multiple segues from a single view controller, and a need to distinguish them.

The key point is the line:

SecondViewController *secondVC = (SecondViewController *) segue.destinationViewController;

segue.destinationViewController is the view controller that will be shown by the segue. We need to get that view controller so we can set its delegate property to self. But segue.destinationViewController is typed as a UIViewController, not a SecondViewController. It must be cast to the proper type so that we can get to its properties!

Now we can assign the delegate:

[secondVC setDelegate:self];

This allows us to respond to the protocol’s method, which simply converts the integer parameter to an English word.

To finish, we must wire up the ViewController’s label outlet to the label (the one with no text), and the SecondViewController’s choiceMade: method to the UISegmentedControl’s ValueChanged event. Note the button on the first view controller does not need an IBAction method: the segue process itself knows what to do with this button press. It calls prepareForSegue: sender: performing the code we specify, then performs the segue by displaying the SecondViewController.

Leave a Reply

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