How to use single storyboard uiviewcontroller for multiple subclass

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.

26256 次浏览

I just tried with with Eclipse Neon.1 and Gradle:

------------------------------------------------------------
Gradle 3.2.1
------------------------------------------------------------


Build time:   2016-11-22 15:19:54 UTC
Revision:     83b485b914fd4f335ad0e66af9d14aad458d2cc5


Groovy:       2.4.7
Ant:          Apache Ant(TM) version 1.9.6 compiled on June 29 2015
JVM:          1.8.0_112 (Oracle Corporation 25.112-b15)
OS:           Windows 10 10.0 amd64

enter image description here

or with (Enide) Gradle for Eclipse, Jetty, Android alternative to Gradle Integration for Eclipse

editbox

On windows 10 with Java Version:

C:\FDriveKambiz\repo\gradle-gen-project>java -version
java version "1.8.0_112"
Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)

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

And it failed miserably as you can see in Eclipse. But sailed like a soaring eagle in Intellij...I dont know Intellij, and a huge fan of eclipse, but common dudes, this means NO ONE teste Neon.1 for the simplest of use cases...to import a gradle project.

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.

Create fat jar : gradle fatJar
Copy dependencies : gradle copyDepends

I have a curios issue.

I have a project that I've worked on and always built from the XCode IDE, and it worked fine. Now I'm setting up Bamboo to build the project and as such am building it from the command line.

Create runnable jar with dependencies : gradle createJar

The issue is, if I check my code out of GIT and then use xcodebuild to build it it says that the scheme cannot be found, but if I open the project, it builds and if I then try to build it again from the command line with the same command, it works.

More details can be read here : https://jafarmlp.medium.com/a-simple-java-project-with-gradle-2c323ae0e43d

What magic is XCode doing when I open the project or am I doing something dumb, maybe excluding a file in my .gitignore that I shouldn't?

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.

pps 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).

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

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):

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];
}
}
}

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:

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";
}

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";
}

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"];
}

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.

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.

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
}
}

Probably most flexible way is to use reusable views.

(Create a View in separate XIB file or Container view and add it to each subclass view controller scene in storyboard)

try this, after instantiateViewControllerWithIdentifier.

- (void)setClass:(Class)c {
object_setClass(self, c);
}

like :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];

Objc_setclass method doesn't create an instance of childvc. But while popping out of childvc, deinit of childvc is being call. Since there is no memory allocated separetely for childvc, app crashes. Basecontroller has an instance , whereas child vc doesn't have.

If you are not too reliant on storyboards, you can create a separate .xib file for the controller.

Set the appropriate File's Owner and outlets to the MainViewController and override init(nibName:bundle:) in the Main VC so that its children can access the same Nib and its outlets.

Your code should look like this:

class MainViewController: UIViewController {
@IBOutlet weak var button: UIButton!


override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: "MainViewController", bundle: nil)
}


required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}


override func viewDidLoad() {
super.viewDidLoad()
button.tintColor = .red
}
}

And your Child VC will be able to reuse its parent's nib:

class ChildViewController: MainViewController {
override func viewDidLoad() {
super.viewDidLoad()
button.tintColor = .blue
}
}

Taking answers from here and there, I came up with this neat solution.

Create a parent view controller with this function.

class ParentViewController: UIViewController {




func convert<T: ParentViewController>(to _: T.Type) {


object_setClass(self, T.self)


}


}

This allows the compiler to ensure that the child view controller inherits from the parent view controller.

Then whenever you want to segue to this controller using a sub class you can do:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)


if let parentViewController = segue.destination as? ParentViewController {
ParentViewController.convert(to: ChildViewController.self)
}


}

The cool part is that you can add a storyboard reference to itself, and then keep calling the "next" child view controller.

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).

There is a simple, obvious, everyday solution.

Simply put the existing storyboard/controller inside the new storyobard/controller. I.E. as a container view.

This is the exactly analogous concept to "subclassing", for, view controllers.

Everything works exactly as in a subclass.

Just as you commonly put a view subview inside another view, naturally you commonly put a view controller inside another view controller.

How else can could you do it?

It's a basic part of iOS, as simple as the concept "subview".

It's this easy ...

/*


Search screen is just a modification of our List screen.


*/


import UIKit


class Search: UIViewController {
    

var list: List!
    

override func viewDidLoad() {
super.viewDidLoad()


list = (_sb("List") as! List
addChild(list)
view.addSubview(list.view)
list.view.bindEdgesToSuperview()
list.didMove(toParent: self)
}
}

You now obviously have list to do whatever you want with

list.mode = .blah
list.tableview.reloadData()
list.heading = 'Search!'
list.searchBar.isHidden = false

etc etc.

Container views are "just like" subclassing in the same way that "subviews" are "just like" subclassing.

Of course obviously, you can't "sublcass a layout" - what would that even mean?

("Subclassing" relates to OO software and has no connection to "layouts".)

Obviously when you want to re-use a view, you just subview it inside another view.

When you want to re-use a controller layout, you just container view it inside another controller.

This is like the most basic mechanism of iOS!!


Note - for years now it's been trivial to dynamically load another view controller as a container view. Explained in the last section: https://stackoverflow.com/a/23403979/294884

Note - "_sb" is just an obvious macro we use to save typing,

func _sb(_ s: String)->UIViewController {
// by convention, for a screen "SomeScreen.storyboard" the
// storyboardID must be SomeScreenID
return UIStoryboard(name: s, bundle: nil)
.instantiateViewController(withIdentifier: s + "ID")
}

Cocoabob's comment from Jiří Zahálka answer helped me to get this solution and it worked well.

func openChildA() {
let storyboard = UIStoryboard(name: "Main", bundle: nil);
let parentController = storyboard
.instantiateViewController(withIdentifier: "ParentStoryboardID")
as! ParentClass;
object_setClass(parentController, ChildA.self)
self.present(parentController, animated: true, completion: nil);
}

Thanks for @Jiří Zahálka's inspiring answer, I replied my solution 4 years ago here, but @Sayka suggested me to post it as an answer, so here it is.

In my projects, normally, if I'm using Storyboard for a UIViewController subclass, I always prepare a static method called instantiate() in that subclass, to create an instance from Storyboard easily. So for solve OP's question, if we want to share the same Storyboard for different subclasses, we can simply setClass() to that instance before returning it.

class func instantiate() -> SubClass {
let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)!
object_setClass(instance, SubClass.self)
return (instance as? SubClass)!
}

It is plain simple. Just define the BaseViewController in a xib and then use it like this:

let baseVC: BaseViewController = BaseViewController(nibName: "BaseViewController", bundle: nil)
let subclassVC: ChildViewController = ChildViewController(nibName: "BaseViewController", bundle: nil)

To make is simple you can extract the identifier to a field and the loading to a method like:

public static var baseNibIdentifier: String {
return "BaseViewController"
}


public static func loadFromBaseNib<T>() -> T where T : UIViewController {
return T(nibName: self.baseNibIdentifier, bundle: nil)
}

Then you can use it like this:

let baseVC: BaseViewController = BaseViewController.loadFromBaseNib()
let subclassVC: ChildViewController = ChildViewController.loadFromBaseNib()

Here is a Swift solution which does not rely on Objective-C class swapping hacks. It uses instantiateViewController(identifier:creator:) (iOS 13+). I assume you have the view controller in a storyboard, with identifier template. The class assigned to the view controller in the storyboard should be the superclass:

let storyboard = UIStoryboard(name: "main", bundle: nil)


let viewController = storyboard.instantiateViewController(identifier: "template") { coder in
// The coder provides access to the storyboard data.
// We can now init the preferred UIViewController subclass.


if useSubclass {
return SpecialViewController(coder: coder)
} else {
return BaseViewController(coder: coder)
}
}

Here is the documentation