How to use single storyboard uiviewcontroller for multiple subclass

25,617

Solution 1

great question - but unfortunately only a lame answer. I don't believe that it is currently possible to do what you propose because there are no initializers in UIStoryboard that allow overriding the view controller associated with the storyboard as defined in the object details in the storyboard on initialization. It's at initialization that all the UI elements in the stoaryboard are linked up to their properties in the view controller.

It will by default initialize with the view controller that is specified in the storyboard definition.

If you are trying to gain reuse of UI elements you created in the storyboard, they still must be linked or associated to properties in which ever view controller is using them for them to be able to "tell" the view controller about events.

It's not that much of a big deal copying over a storyboard layout especially if you only need a similar design for 3 views, however if you do, you must make sure that all the previous associations are cleared, or it will get crashes when it tries to communicate to the previous view controller. You will be able to recognize them as KVO error messages in the log output.

A couple of approaches you could take:

  • store the UI elements in a UIView - in a xib file and instantiate it from your base class and add it as a sub view in the main view, typically self.view. Then you would simply use the storyboard layout with basically blank view controllers holding their place in the storyboard but with the correct view controller sub class assigned to them. Since they would inherit from the base, they would get that view.

  • create the layout in code and install it from your base view controller. Obviously this approach defeats the purpose of using the storyboard, but may be the way to go in your case. If you have other parts of the app that would benefit from the storyboard approach, it's ok to deviate here and there if appropriate. In this case, like above, you would just use bank view controllers with your subclass assigned and let the base view controller install the UI.

It would be nice if Apple came up with a way to do what you propose, but the issue of having the graphic elements pre-linked with the controller subclass would still be an issue.

have a great New Year!! be well

Solution 2

The code of line we are looking for is:

object_setClass(AnyObject!, AnyClass!)

In Storyboard -> add UIViewController give it a ParentVC class name.

class ParentVC: UIViewController {

    var type: Int?

    override func awakeFromNib() {

        if type = 0 {

            object_setClass(self, ChildVC1.self)
        }
        if type = 1 {

            object_setClass(self, ChildVC2.self)
        }  
    }

    override func viewDidLoad() {   }
}

class ChildVC1: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 0
    }
}

class ChildVC2: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 1
    }
}

Solution 3

As the accepted answer states, it doesn't look like it is possible to do with storyboards.

My solution is to use Nib's - just like devs used them before storyboards. If you want to have a reusable, subclassable view controller (or even a view), my recommendation is to use Nibs.

SubclassMyViewController *myViewController = [[SubclassMyViewController alloc] initWithNibName:@"MyViewController" bundle:nil]; 

When you connect all your outlets to the "File Owner" in the MyViewController.xib you are NOT specifying what class the Nib should be loaded as, you are just specifying key-value pairs: "this view should be connected to this instance variable name." When calling [SubclassMyViewController alloc] initWithNibName: the initialization process specifies what view controller will be used to "control" the view you created in the nib.

Solution 4

It is possible to have a storyboard instantiate different subclasses of a custom view controller, though it involves a slightly unorthodox technique: overriding the alloc method for the view controller. When the custom view controller is created, the overridden alloc method in fact returns the result of running alloc on the subclass.

I should preface the answer with the proviso that, although I have tested it in various scenarios and received no errors, I can't ensure that it will cope with more complex set ups (but I see no reason why it shouldn't work). Also, I have not submitted any apps using this method, so there is the outside chance that it might be rejected by Apple's review process (though again I see no reason why it should).

For demonstration purposes, I have a subclass of UIViewController called TestViewController, which has a UILabel IBOutlet, and an IBAction. In my storyboard, I have added a view controller and amended its class to TestViewController, and hooked up the IBOutlet to a UILabel and the IBAction to a UIButton. I present the TestViewController by way of a modal segue triggered by a UIButton on the preceding viewController.

Storyboard image

To control which class is instantiated, I have added a static variable and associated class methods so get/set the subclass to be used (I guess one could adopt other ways of determining which subclass is to be instantiated):

TestViewController.m:

#import "TestViewController.h"

@interface TestViewController ()
@end

@implementation TestViewController

static NSString *_classForStoryboard;

+(NSString *)classForStoryboard {
    return [_classForStoryboard copy];
}

+(void)setClassForStoryBoard:(NSString *)classString {
    if ([NSClassFromString(classString) isSubclassOfClass:[self class]]) {
        _classForStoryboard = [classString copy];
    } else {
        NSLog(@"Warning: %@ is not a subclass of %@, reverting to base class", classString, NSStringFromClass([self class]));
        _classForStoryboard = nil;
    }
}

+(instancetype)alloc {
    if (_classForStoryboard == nil) {
        return [super alloc];
    } else {
        if (NSClassFromString(_classForStoryboard) != [self class]) {
            TestViewController *subclassedVC = [NSClassFromString(_classForStoryboard) alloc];
            return subclassedVC;
        } else {
            return [super alloc];
        }
    }
}

For my test I have two subclasses of TestViewController: RedTestViewController and GreenTestViewController. The subclasses each have additional properties and each override viewDidLoad to change the background colour of the view and update the text of the UILabel IBOutlet:

RedTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor redColor];
    self.testLabel.text = @"Set by RedTestVC";
}

GreenTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor greenColor];
    self.testLabel.text = @"Set by GreenTestVC";
}

On some occasions I might want to instantiate TestViewController itself, on other occasions RedTestViewController or GreenTestViewController. In the preceding view controller, I do this at random as follows:

NSInteger vcIndex = arc4random_uniform(4);
if (vcIndex == 0) {
    NSLog(@"Chose TestVC");
    [TestViewController setClassForStoryBoard:@"TestViewController"];
} else if (vcIndex == 1) {
    NSLog(@"Chose RedVC");
    [TestViewController setClassForStoryBoard:@"RedTestViewController"];
} else if (vcIndex == 2) {
    NSLog(@"Chose BlueVC");
    [TestViewController setClassForStoryBoard:@"BlueTestViewController"];
} else {
    NSLog(@"Chose GreenVC");
    [TestViewController setClassForStoryBoard:@"GreenTestViewController"];
}

Note that the setClassForStoryBoard method checks to ensure that the class name requested is indeed a subclass of TestViewController, to avoid any mix-ups. The reference above to BlueTestViewController is there to test this functionality.

Solution 5

Basing particularly on nickgzzjr and Jiří Zahálka answers plus comment under the second one from CocoaBob I've prepared short generic method doing exactly what OP needs. You need only to check storyboard name and View Controllers storyboard ID

class func instantiate<T: BasicViewController>(as _: T.Type) -> T? {
        let storyboard = UIStoryboard(name: "StoryboardName", bundle: nil)
        guard let instance = storyboard.instantiateViewController(withIdentifier: "Identifier") as? BasicViewController else {
            return nil
        }
        object_setClass(instance, T.self)
        return instance as? T
    }

Optionals are added to avoid force unwrap (swiftlint warnings), but method returns correct objects.

Also: you need to initialize properties existing only in subclass before reading them from casted objects (if subclass has those properties and BasicViewController does not). Those properties won't be initialized automatically and attempt to read them before initialization will lead to crash. Because they are there in effect of casting it's very likely that even weak variables won't be set to nil (will contain garbage).

Share:
25,617
Agustinus Verdy
Author by

Agustinus Verdy

Updated on June 23, 2021

Comments

  • Agustinus Verdy
    Agustinus Verdy about 3 years

    Let say I have a storyboard that contains UINavigationController as initial view controller. Its root view controller is subclass of UITableViewController, which is BasicViewController. It has IBAction which is connected to right navigation button of the navigation bar

    From there I would like to use the storyboard as a template for other views without having to create additional storyboards. Say these views will have exactly the same interface but with root view controller of class SpecificViewController1 and SpecificViewController2 which are subclasses of BasicViewController.
    Those 2 view controllers would have the same functionality and interface except for the IBAction method.
    It would be like the following:

    @interface BasicViewController : UITableViewController
    
    @interface SpecificViewController1 : BasicViewController
    
    @interface SpecificViewController2 : BasicViewController
    

    Can I do something like that?
    Can I just instantiate the storyboard of BasicViewController but have root view controller to subclass SpecificViewController1 and SpecificViewController2?

    Thanks.

  • Agustinus Verdy
    Agustinus Verdy over 11 years
    That was quick. As I thought, it would not be possible. Currently I come up with a solution by having just that BasicViewController class and have additional property to indicate which "class"/"mode" it will be acting as. Thanks anyway.
  • Hlung
    Hlung over 10 years
    too bad :( Guess I have to copy and paste the same view controller and change its class as a workaround.
  • TheEye
    TheEye about 10 years
    And this is why I don't like Storyboards ... somehow they are not really working once you do a bit more than standard views ...
  • Tony
    Tony almost 10 years
    So sad when hearing you said that. I'm looking for solution
  • Tim
    Tim almost 9 years
    We've done something similar in the project, but overriding the UIViewController's alloc method to get a subclass from an external class gathering the full info about all the overrides. Works perfectly.
  • Tim
    Tim almost 9 years
    By the way this method may stop working as fas ar as Apple stops calling alloc on view controllers. For the example NSManagedObject class never receives alloc method. I think Apple could copy the code to another method: maybe +allocManagedObject
  • Raphael Oliveira
    Raphael Oliveira over 8 years
    I'm spending some time trying to intercept the unarchiving process but so far I haven't been lucky. I tried to override classForKeyedUnarchiver on the parent UIViewController but this method is not called.
  • CocoaBob
    CocoaBob about 8 years
    Thanks, it just works, for example: class func instantiate() -> SubClass { let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)! object_setClass(instance, SubClass.self) return (instance as? SubClass)! }
  • user1366265
    user1366265 almost 8 years
    I'm not sure I understand how's this supposed to work. The parent is setting its class as that of a child? How can you have multiple children then?!
  • jere
    jere almost 8 years
    you sir, have made my day
  • DanSkeel
    DanSkeel almost 8 years
    Please comment when you downvote. I know that I don't answer directly to that question but I propose a solution for the root problem.
  • Sherwin Zadeh
    Sherwin Zadeh over 7 years
    Can you expand on how this is a solution? I agree with @user136265 -- how can you have multiple ChildVCs?
  • Jiří Zahálka
    Jiří Zahálka over 7 years
    OK guys, so let me just explain it little bit more in detail: What do we want to achieve? We want to subclass our ParentViewController so that we can use its Storyboard for more classes. So the magic line that does it all is highlighted in my solution and must be used in awakeFromNib in ParentVC. What happens then is that it uses all methods from newly set ChildVC1 which becomes as a subclass. If you want to use it for more ChildVCs? Simply do your logic in awakeFromNib .. if (type = a) { object_setClass(self, ChildVC1.self) } else { object_setClass(self.ChildVC2.self) } Good luck.
  • Charlton Provatas
    Charlton Provatas over 7 years
    where is "type" being set? in user defined runtime attributes? or in code?
  • Jiří Zahálka
    Jiří Zahálka over 7 years
    It is set in code in place where you call ParentVC from. Just set it as yours ParentVC variable attribute.
  • codeforester
    codeforester over 7 years
    Please add some helpful explanation about what your code does.
  • plam4u
    plam4u over 7 years
    There is another approach: Specify the custom logic in different delegates and in prepareForSegue, assign the correct delegate. This way, you create 1 UIViewController + 1 UIViewController in the Storyboard but you have multiple implementation versions.
  • LegendLength
    LegendLength about 7 years
    Thanks plam I've been thinking about this for a couple of days and that seems like the best solution. Storyboards don't really allow for proper subclassing and reusability in a general sense.
  • Ace Green
    Ace Green about 7 years
    This isn't a subclass mate, this is just duplicating a ViewController.
  • Ace Green
    Ace Green about 7 years
    That doesn't awakeFromNib is called before the type is set. Likely need to move the type check login to viewDidLoad().
  • Ace Green
    Ace Green about 7 years
    Also this doesn't seem to trigger viewDidLoad() in the subclass ChildVC2
  • Aaron Brager
    Aaron Brager about 7 years
    That's not right. It becomes a subclass when you change Class to the subclass (step 3). Then you can make whatever changes you want to, and hook up to the outlets/actions in your subclass.
  • Ace Green
    Ace Green about 7 years
    I don't think you get the concept of subclassing.
  • superarts.org
    superarts.org about 7 years
    If "later modifications within the storyboard to the base class controller don't propagate to the subclass", it's not called "subclassing". It's copy & paste.
  • Aaron Brager
    Aaron Brager almost 7 years
    The underlying class, selected in the Identity Inspector, is still a subclass. The object being initialized and controlling the business logic is still a subclass. Only the encoded view data, store as XML in the storyboard file and initialized via initWithCoder:, does not have an inherited relationship. This type of relationship is not supported by storyboard files.
  • Patrik
    Patrik almost 7 years
    Be very careful when using this! Normally this shouldn't be used at all... This simply changes the isa pointer of the given pointer and doesn't reallocate memory to accommodate for e.g. different properties. One indicator for this is that the pointer to self doesn't change. So inspection of the object (e.g. reading _ivar / property values) after object_setClass can cause crashes.
  • Sira Lam
    Sira Lam over 6 years
    I tried this approach, from debugger I know that my subclass has been instantiated; but I cannot init the subclass' xib, getting a Thread1: BAD_ACCESS_EXC. Anyone else experienced this?
  • Legoless
    Legoless over 6 years
    What happens with this, if you use instance variables from subclass? I'm guessing crash, because there is not enough memory allocated to fit that. In my tests, I've been getting EXC_BAD_ACCESS, so not recommending this.
  • Al Zonke
    Al Zonke about 6 years
    This will not work if you will add new variables in child class. And child's init also will not be called. Such constraints makes all approach unusable.
  • avismara
    avismara over 5 years
    Even design wise, I don't think we should be doing this since the parent has a semantic coupling with its children. It would be better just to ditch storyboard and use xibs instead.
  • Adam Tucholski
    Adam Tucholski almost 5 years
    Surprisingly seems that this is possible with storyboards, thanks to ObjC runtime library. Check my answer here: stackoverflow.com/a/57622836/7183675
  • Adam Tucholski
    Adam Tucholski almost 5 years
    Sorry, answer looks nice but seems that it isn't correct. Check my answer here: stackoverflow.com/a/57622836/7183675
  • Abdul Saleem
    Abdul Saleem about 4 years
    @CocoaBob your comment was the exact solution. Why didn't you posted that as an answer ?!!
  • CocoaBob
    CocoaBob about 4 years
    @Sayka Thanks, I didn't know that 😂 it was 4 years ago, many things have changed. I just re-posted it as an answer here stackoverflow.com/a/62316832/886215
  • Abdul Saleem
    Abdul Saleem about 4 years
    well @CocoaBob, I put that as an answer because I thought the same will help some others too.
  • lzl
    lzl almost 3 years
    I tried this and sometimes got Heap buffer overflows when I access attributes off the subclass. It seems like when you set the class this way the memory wasn't reassigned to the subclass properly, so I'm gonna say ....this method is probably not a good idea.