PostCodeism
PostCodeism

Reputation: 1070

How to change a UIViewController class name in a UIStoryboard at runtime

I have this UIViewController set up in in my storyboard, with all the outlets, views, and constraints I need. Perfect. Let's call this WatchStateController, it'll serve as an abstract parent class.

I then have this subclass of WatchStateController, called WatchStateTimeController, which will have the functionality I need for a particular state of the application.

Because I am trying to use the 1 view controller in the UIStoryboard, I'm having some problems in instantiating a WatchStateTimeController as type WatchStateTimeController - it instantiates as WatchStateController.

UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];

WatchStateTimeController *timeController = (WatchStateTimeController *)[mainStoryboard instantiateViewControllerWithIdentifier:@"WatchStateController"];

This is because the "Class" field in the storyboard's Identity Inspector is set to "WatchStateController". So the question is, how do I merely change this classname set in the Identity Inspector at runtime?

Identity Inspector

NOTE: ignore why I'm trying to do this and concentrate on how. If you really must know why, you can read up on the Strategy design pattern.

Upvotes: 3

Views: 2648

Answers (2)

user2216794
user2216794

Reputation: 151

One slightly dirty workaround I'm using to force storyboard to be compatible with the strategy pattern: I write a custom allocator in the base (abstract) view controller that returns an instance of the desired concrete view controller subclass, before the storyboard mechanism gets over.

For this to work, you have to tell the base class which subclass you want to instantiate.

So, in the base controller:

Class _concreteSubclass = nil;
+ (void) setConcreteSubclassToInstantiate:(Class)c {
    _concreteSubclass = c;
}

+ (id)allocWithZone: (NSZone *)zone {
    Class c = _concreteSubclass ?: [self class];
    void *object = calloc(class_getInstanceSize(c), 1);
    *(Class *)object = c;
    return (id)CFBridgingRelease(object);
}

This instantiates enough memory for the ivars of the subclass too.

The view controller type of "MyViewController" known to the storyboard is just "BaseViewController"; but then, where you ask the storyboard to instantiate the view controller, you do something like this:

[BaseViewController setConcreteSubclassToInstantiate:[SomeSubclassOfBaseViewController class]];
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@"Main" bundle: nil];
SomeSubclassOfBaseViewController *vc = (SomeSubclassOfBaseViewController *)[mainStoryboard instantiateViewControllerWithIdentifier:@"MyViewController"];
[self presentViewController:vc animated:NO completion:^{}];

The concrete view controller is instantiated and shown without a hitch.

Upvotes: 6

Mike Mertsock
Mike Mertsock

Reputation: 12005

Here's an example of the strategy pattern using a helper object, as I described in the comments:

@class WatchStateController;

@protocol WatchStateStrategy <NSObject>
- (void)doSomeBehaviorPolymorphically:(WatchStateController *)controller;
@end

@interface WatchStateController
// or call this a delegate or whatever makes sense.
@property (nonatomic) id <WatchStateStrategy> strategy;
@end

@implementation WatchStateController
- (void)someAction:(id)sender
{
    [self.strategy doSomeBehaviorPolymorphically:self];
}
@end

@interface WatchStateTimeStrategy <WatchStateStrategy>
@end

@implementation WatchStateTimeStrategy
- (void)doSomeBehaviorPolymorphically:(WatchStateController *)controller
{
    // here's one variation of the behavior
}
@end

@interface WatchStateAnotherStrategy <WatchStateStrategy>
@end

@implementation WatchStateAnotherStrategy
- (void)doSomeBehaviorPolymorphically:(WatchStateController *)controller
{
    // here's another variation of the behavior
}
@end

And to set this up when you are presenting your view controller, assign the appropriate helper object (instead of attempting to change the subclass of the view controller itself):

WatchStateController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"WatchStateController"];
if (useTimeStrategy) {
    viewController.strategy = [WatchStateTimeStrategy new];
} else {
    viewController.strategy = [WatchStateAnotherStrategy new];
}

The advantages I see to this approach compared to subclassing the view controller:

  • It more closely aligns with SOLID principles, especially single responsibility principle, open/closed principle, etc.
  • Small, focused helper classes, possibly having few or no UI dependencies depending on what they need to do, make for easier unit testing if you plan to write tests
  • It more closely follows the design patterns and structural patterns already in place in iOS (using delegates, and letting storyboards/xibs instantiate view controllers the normal way)
  • Removes logic from the view controller. With iOS it's so easy to get a large view controllers with too much logic; I think we should always be looking for opportunities to improve this

Upvotes: 5

Related Questions