Segues link ViewControllers together, they don’t pass along any data (except layout data).
ViewControllers are given a few chances to set up each other in methods like prepareForSegue:sender:
. However we don’t want to tightly couple our ViewControllers.
It is debatable that not much is gained from Segues if when in a prepare method, the viewController cast it to a known type and initialises it anyway. Especially if the segue is performed in code.
Here is an approach I’ve found successful at addressing this problem by using a protocol.
First define a protocol to indicate an object can be assigned a Model.
@protocol ModelAssignable <NSObject>
@property (nonatomic) Model *model;
@end
You could stop there. In prepareForSegue:sender:
you can now simply check if the destination conforms to this protocol and if it does, assign the model object. Do this before any segueIdentifier
checking – if you still have any.
I prefer to move this logic into a category on UIStoryboardSegue
like so:
@implementation UIStoryboardSegue (ModelAssignable)
- (void)assignModel:(Model *)model
{
// another category method for finding the right vc
id<ModelAssignable> destination = [self destinationViewControllerConformingToProtocol:@protocol(ModelAssignable)];
destination.model = model;
}
// be lazy and assign from source to destination automatically
- (void)assignModel
{
if (NO == [self.sourceViewController conformsToProtocol:@protocol(ModelAssignable)]) {
return;
}
id<ModelAssignable> source = self.sourceViewController;
[self assignModel:source.model];
}
@end
You can avoid having to subclass container ViewControllers by also checking if the destination viewController is a UINavigationController
, UISplitViewController
, etc.
@implementation UIStoryboardSegue (FindProtocol)
- (id)destinationViewControllerConformingToProtocol:(Protocol *)protocol
{
id destinationViewController = self.destinationViewController;
if ([destinationViewController conformsToProtocol:protocol]) {
return destinationViewController;
}
// check for container viewControllers
if ([destinationViewController isKindOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = destinationViewController;
UIViewController *topViewController = navigationController.topViewController;
if ([topViewController conformsToProtocol:protocol]) {
return topViewController;
}
}
if ([destinationViewController isKindOfClass:[UISplitViewController class]]) {
UISplitViewController *splitViewController = destinationViewController;
for (UIViewController *viewController in splitViewController.viewControllers) {
if ([viewController conformsToProtocol:protocol]) {
return viewController;
}
}
}
return nil;
}
@end
Now prepareForSegue:sender:
methods will look something like this:
// screen to screen
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
[segue assignModel:self.model];
}
// screen to more specific screen
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
[segue assignModel:self.giantModel.subModel];
}
// going from a table view to a detail view
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
Model *selectedModel = [self.modelCollection modelAtIndex:self.tableView.indexPathForSelectedRow.row];
[segue assignModel:selectedModel];
}
This technique can be used to provide Dependency Injection for other objects too. NSManagedObjectContext
, NSUserDefaults
, networking, etc.
You can find my example project here Ashton-W/Example-SegueInjection
I wrote about 'Dependency Injection Segues'. #iOSDev
http://t.co/wO8git7Hvw
— Ashton (@AshtonDev) April 21, 2015