3

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.

PostCodeism
  • 1,070
  • 1
  • 12
  • 20
  • 4
    Consider the possibility of changing your interpretation of the Strategy design pattern to better fit the structure of the application that you are given with Storyboards: instead of using inheritance to change the behavior of your view controller, use aggregation. In other words, have a property on `WatchStateController` that references another object of some base class or protocol type, which can provide the desired behavior as a sort of delegate. – Mike Mertsock Jun 29 '13 at 12:30
  • Thanks. I'm very familiar with aggregation and composition - I use it 95% of the time. Unfortunately using it here won't make sense, it'll result in repetitive work, copying and pasting the same code, which is what I'm wanting to avoid. Redundancy is the enemy. If this is a limitation in using storyboards, you're right, a variation to the pattern will have to be found. Question is, is the state/strategy pattern compatible at all, when using storyboards? Or should I just give up and litter the code with a bunch of IF/ELSE statements (what the strategy pattern is actually designed to avoid) – PostCodeism Jun 29 '13 at 13:49
  • maybe i read it wrong or to fast but the image you included of storyboard showing watchstatecontroller, and you want to change it...shouldnt that be WatchStateTimeController since you subclasses it and this is the view you want of the subclass ? maybe i could use a simpler description..like you have class a for view a and you sublcassed class a for class b for view b but your issue is view b is running with class a? – rezand Jun 29 '13 at 14:12
  • 1
    @PostCodeism the alternative I propose should not result in more duplicate code/logic than subclassing the view controller. How would you determine which subclass of `WatchStateController` to use in a particular situation? Instead of changing the subclass of the controller, that same code could just assign a different subclass of some helper object to a property on the controller. No conditional logic needed in `WatchStateController`. – Mike Mertsock Jun 29 '13 at 15:39
  • @rezand yes it should be WatchStateTimeController. It's set as WatchStateController in the storyboard, but I want to change it to WatchStateTimeController dynamically in code. The storyboard has a problem with this, when instantiating a class using the storyboard...even though the class you're instantiating is a valid subclasses. It should support subclass instantiation, and it seems weird to me that it doesn't. – PostCodeism Jul 01 '13 at 14:26
  • @esker I don't see how that would modify UIKit elements, in the context of UIKit's child view controllers and container framework. You're talking about modifying behaviour, I'm talking about modifying state. What I mean by state specifically is - having a UIButton with a title of "start". When start is pressed, the title should be changed to "stop", and the "target" should be changed to a different IBAction method.... this is just 1 example. I don't want it to execute some behaviour, I want the SAME UIKit elements in the same UIViewController (read inheritance) to change state. – PostCodeism Jul 01 '13 at 15:14
  • 1
    ok sorry if im stating the obvious but you replied to me that what i was asking was correct earlier so since apparently i do understand my next question is if you subclassed it why can't you just use the pull down from the class selection you are showing there on the storyboard image you posted showing WatchStateController. if you subclassed a for class b then you should be able to use the pull down and select class b for view b. – rezand Jul 01 '13 at 15:43
  • @rezand Runtime. Runtime is the answer to your question. You can't pull down the dropdown option at Runtime. At Runtime, you can only change things in code. So the question is, how do you do change the class name in code? – PostCodeism Jul 01 '13 at 19:17

2 Answers2

6

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.

user2216794
  • 151
  • 2
  • 2
5

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
Mike Mertsock
  • 11,825
  • 7
  • 42
  • 75
  • This is digressing away from the original question, but I do like where this is going. I love discussing software patterns and how we might implement them in Objective-C. Sometimes we need to reshape our thinking for Xcode. But my understanding of the Strategy pattern is similar to the one detailed in this book, "Pro Objective-C Design Patterns for iOS". A CONTEXT class, which "has a" CONCRETE class, which "is a" kind of ABSTRACT class. Limited space here, see the chapter 19 sample: http://books.google.ca/books?id=rLZcD4hkDy4C&printsec=frontcover#v=onepage&q=Strategy%20Pattern&f=false – PostCodeism Jul 01 '13 at 14:05
  • Using a protocol similar to the example you gave could be workable solution, and is similar to the solution I had before, which was getting convoluted to manage. But it's not the Strategy pattern - you've still got the problem of managing convoluted IF/ELSE statements to maintain logic. The solution to this problem is the specific reason why the Strategy pattern even exists. Read the sample I provided in the link, and think about how we can implement that within the constraints of UIViewControllers and UIStoryboards. If it's not possible, fine. I'll have to re-implement a protocol solution :) – PostCodeism Jul 01 '13 at 14:11
  • 1
    How would @esker's solution require if/else for logic? The protocol specified only encapsulates the dynamic behaviors. One could also have an abstract class provide some common implementations, and concrete strategies inherit it (although in that case why not put the common impl in your `WatchStateController` and use a protocol for the rest). As for the last bit of if/else for strategy initialization, I'd use factory methods to encapsulate the `-instantiateViewControllerWithIdentifier:` & `vc.strategy` setting, or set it in -`performSegueWithIdentifier:sender:` if using segues to switch logic. – qix Dec 17 '13 at 20:12
  • @qix did you not see the code of `if (useTimeStrategy)` ...? The strategy pattern is supposed to do away with ALL conditionals to decide on what class to instantiate. That's the whole point of it. I know that it's not very common, so not a lot of people understand it. But it's the best suited pattern to what I was aiming to implement – PostCodeism Nov 27 '14 at 22:15