Segue between UIViewControllers without Storyboard OR XIB - objective-c

I have a nifty project I downloaded from GitHub (here) and I am playing around with it. The project has no storyboard or xibs whatsoever, and only one viewController, which is defined with just a viewController.h file and a viewController.m file.
Perhaps a noob question, but can I have viewController1.h/m programmatically segue to viewController2.h/m without using ANY xibs or storyboards? I found a lot of code on SO and elsewhere allowing one to segue programmatically from one view to another within a Storyboard, from one xib to another or from a scoreboard to a xib (though not the opposite) but nothing on how to segue from one totally code-based vc to another. All the code I found requires that you define the view in terms of the bundle location of the storyboard or xib file, but I want to use neither.
Note: I accepted the answer I did because of its ingenuity/interesting-ness, but for the sake of simplicity I personally ended up opting with this answer to the same question (mine was a duplicate it appears): iOS: present view controller programmaticallly

You can use [viewController presentViewController:anotherController animated:YES completion:nil]; to present the view controller modally.
Another alternative is to use a UINavigationController and do [viewController.navigationController pushViewController:anotherController animated:YES];
The second method will only work if viewController is in the stack of a navigationController

Here is my Context class which changes view controllers. It works with either your own view classes or storyboard view classes.
Specific to your question look at the open function. If there is no root controller when I call open, I assign it as the root view controller. Otherwise I present it from the root view controller.
import Foundation
import UIKit
private let _StoryBoard = UIStoryboard(name: "Main", bundle: nil)
private let _RootWindow = UIWindow(frame: UIScreen.mainScreen().bounds)
public var ROOT_VIEW_CONTROLLER:UIViewController = C_RootViewController()
//abstract base of context classes
class Context:NSObject
{
class var STORYBOARD:UIStoryboard
{
return _StoryBoard
}
class var ROOTWINDOW:UIWindow
{
return _RootWindow
}
var _currentController:Controller!
class func reassignRootViewController(controller:UIViewController)
{
Context.ROOTWINDOW.rootViewController = controller
ROOT_VIEW_CONTROLLER = controller
}
func initController(controllerName:String)->Controller
{
return Context.STORYBOARD.instantiateViewControllerWithIdentifier(controllerName) as Controller
}
func initControllerFromStoryboard(storyboardName:String,controllerName:String)->Controller
{
var storyboard:UIStoryboard = UIStoryboard(name: storyboardName, bundle: nil)
return storyboard.instantiateViewControllerWithIdentifier(controllerName) as Controller
}
func open(controller:UIViewController)
{
if(Context.ROOTWINDOW.rootViewController == nil)
{
Context.ROOTWINDOW.rootViewController = ROOT_VIEW_CONTROLLER
Context.ROOTWINDOW.makeKeyAndVisible()
}
ROOT_VIEW_CONTROLLER.presentViewController(controller, animated: true, completion: {})
}
func close(controller:UIViewController)
{
ROOT_VIEW_CONTROLLER.dismissViewControllerAnimated(true, completion: nil)
}
}

Related

Change navigation root view programmatically objective c

I want to change the navigation's root view controller programmatically using objective c. I am using a storyboard. I tried to change it in-app delegate but didn't work, there's something new introduced called scene delegate where I believe I can change the root from there, but I found no resources for that matter. Can anyone help?
Use below code:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = MyRootViewController()
self.window = window
window.makeKeyAndVisible()
}
}
Reference: https://www.andrewcbancroft.com/blog/ios-development/ui-work/accessing-root-view-controller-ios13-scenedelegate/
And: How set rootViewController in Scene Delegate iOS 13

Load View from XIb crashes

I am loading a view using xib but it crashes always. I have tried different approaches available on net but still it crashes.I don't find any solution to deal with. I have attached code for reference Click here
I have checked your code and the problem is you would give your view class to file owner and according to it change IBOutlets and IBActions.
So first thing is Give 'CartView'to FileOwner and Remove it from View.
Then remove IBAction from gesture and IBOutlets from lbl and img then again assign which will be refer to filesowner.
What you are doing is wrong. You have set the class as Cart for both xib and the view in storyboard.
Try loading the nib from the viewController and add it as subview. If you need the small view in storyboard as a container, just create an outlet and add the card view as subview to container.
You are trying to load cart like this
private func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "CartView", bundle: bundle)
let nibView = nib.instantiate(withOwner: self, options: nil).first as! UIView
return nibView
}
There is absolutely no problem with loading a view with that. The problem is You should not load a view inside the view's class.
you have to do it inside a ViewController. like this:
func loadCartView() -> UIView! {
if let customView = NSBundle.mainBundle().loadNibNamed("CartView", owner: self, options: nil).first as? CartView {
customView.translatesAutoresizingMaskIntoConstraints = false
return customView
} else {
return nil
}
}
SWIFT3:
func loadCartView() -> UIView! {
if let customView = Bundle.main.loadNibNamed("CartView", owner: self, options: nil)?.first as? CartView {
customView.translatesAutoresizingMaskIntoConstraints = false
return customView
} else {
return nil
}
}

Functional testing in Swift. Simulate the Application flow

I'm trying to perform some really simple feature/functional testing in Swift but I have some doubts that I need to resolve to be able to create useful tests.
I want to verify that a Controller presented by another Controller exists into the Application Navigation Hierarchy (it doesn't matter if the Controller has been presented into a NavigationController, as Modal or whatever).
If I instantiate and show controllers programmatically, directly into the test functions, when I check the On Top controller I always get the Storyboard root controller instead of the controller that I have just instantiated, as if the controllers that I've manually created are never added into the Application Hierarchy.
Here an example of pseudo-code:
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let rootController = UIApplication.sharedApplication().keyWindow?.rootViewController
XCTAssert(rootController.self == TheExpectedClass, "Controller is not what we expect")
}
If I instantiate and show controllers programmatically, directly into the test functions, when I check the On Top controller I always get the Storyboard root controller instead of the controller that I have just instantiated, as if the controllers that I've manually created are never added into the Application Hierarchy.
From the code you wrote you are not checking the On Top controller but you are checking root view controller itself (which holds all view controllers in hierarchy including navigation controllers) so thats why you get always storyboard root view controller back. To get top most controller from view controller you can use the following recursive function which takes root view controller and return its top most controller
func topMostController(rootViewController:UIViewController)->UIViewController{
if let viewController = rootViewController as? UINavigationController{
return topMostController(viewController.visibleViewController)
}
if let viewController = rootViewController.presentedViewController{
return topMostController(viewController)
}
return rootViewController
}
and then in your testing function check the controller that this function returns
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let rootController = UIApplication.sharedApplication().keyWindow?.rootViewController
XCTAssert(topMostController(rootController) == TheExpectedClass, "Controller is not what we expect")
}
First you stated that it does not matter if the view is presented by a navigation controller. So i created a empty application with a navigationcontroller as initial controller and two ViewControllers, the first is just namend ViewController the second is in my case ViewControllerSecond which is your TheExpectedClass Controller.
First thing to note: If using a NavigationController obviously the rootController will always be the navigationController. So then lets check what happens if we first load the the ViewController and then within this push the ViewControllerSecond:
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controllerSecond = storyBoard.instantiateViewControllerWithIdentifier("ViewControllerSecond") as? ViewControllerSecond
controllerSecond?.loadView()
self.navigationController?.pushViewController(controllerSecond!, animated: false)
let navigationController = UIApplication.sharedApplication().keyWindow?.rootViewController as UINavigationController
let currentController: AnyObject = navigationController.viewControllers[0]
println(navigationController.viewControllers)
You will see that ViewControllerSecond was pushed to the navigationController as it should.
If I instantiate and show controllers programmatically, directly into
the test functions, when I check the On Top controller I always get
the Storyboard root controller instead of the controller that I have
just instantiated, as if the controllers that I've manually created
are never added into the Application Hierarchy.
What you're saying here is true, they aren't added. In your pseudo code all you did was instantiate some view controllers and push them onto each other.
Why are you expecting them to be in the Application Hierarchy? You never added them there.
There are actually two issues here, and that is only the first one.
The second issue:
UIApplication.sharedApplication().keyWindow?.rootViewController
This code grabs the root view controller, which is actually the one that is on the very "bottom" (assuming "top" means more visible). When you are using a Storyboard this will almost always be the initial view controller.
So even if you did add your newly instantiated view controllers to the hierarchy, the test you do will still not pass.
Suggested solution
As a simple test, you don't need to test that your new View Controller is on top the visual hierarchy. To do so you would need to add it there.
All you really need to test is - "If I push my view controller onto this newly created navigation stack, it should be at the top of that stack (visible)"
This way your test is not dependent on the application state or any other controllers that are in the hierarchy.
Pseudo code:
func testController(){
// Instantiate a controller
let storyBoard:UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let controller1 = storyBoard.instantiateViewControllerWithIdentifier("Controller1") as? ControllerOneViewController
controller1.loadView()
// Call a function that instantiates another controller
controller1.pushAnotherController()
// Test that the current shown controller is what we expect...
let nav = controller1.navigationController! //Assuming it's embedded in one
XCTAssert(nav.visibleViewController.self == TheExpectedClass, "Controller is not what we expect")
}

NSStoryboardSegue sample code (Yosemite Storyboard)

OS X Yosemite introduced NSStoryboardSegue
“A storyboard segue specifies a transition or containment relationship between two scenes in a storyboard…”
Update:
• If I attempt to use a NSStoryboardSegue subclass in a Storyboard with Yosemite., it crashes with SIGABRT.
• If I ignore segues, and manually present a view controller using a specified, custom animator for presentation and dismissal,
func presentViewController(_ viewController: NSViewController,
animator animator: NSViewControllerPresentationAnimator)
it works as expected.
This post provides additional insight: Animate custom presentation of ViewController in OS X Yosemite
Using that as a reference, here's my attempt so far:
class FadeSegue: NSStoryboardSegue {
override func perform() {
super.perform()
sourceController.presentViewController(destinationController as NSViewController,
animator: FadeTransitionAnimator())
}
}
class FadeTransitionAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentationOfViewController(toViewController: NSViewController, fromViewController: NSViewController) {
toViewController.view.wantsLayer = true
toViewController.view.layerContentsRedrawPolicy = .OnSetNeedsDisplay
toViewController.view.alphaValue = 0
fromViewController.view.addSubview(toViewController.view)
toViewController.view.frame = fromViewController.view.frame
NSAnimationContext.runAnimationGroup({ context in
context.duration = 2
toViewController.view.animator().alphaValue = 1
}, completionHandler: nil)
}
func animateDismissalOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
viewController.view.wantsLayer = true
viewController.view.layerContentsRedrawPolicy = .OnSetNeedsDisplay
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = 2
viewController.view.animator().alphaValue = 0
}, completionHandler: {
viewController.view.removeFromSuperview()
})
}
}
The problem appears to be with the Swift 'subclassing' of NSStoryboardSegue. If you implement the same functionality using Objective-C, everything works as expected. The problem is specifically with your FadeSeque class. The animator object works fine in either Objective-C or Swift.
So this:
class FadeSegue: NSStoryboardSegue {
override func perform() {
super.perform()
sourceController.presentViewController(destinationController as NSViewController,
animator: FadeTransitionAnimator())
}
}
Will work if provided as an Objective-C class:
#interface MyCustomSegue : NSStoryboardSegue
#end
#implementation FadeSegue
- (void)perform {
id animator = [[FadeTransitionAnimator alloc] init];
[self.sourceController presentViewController:self.destinationController
animator:animator];
}
#end
(I don't think you need to call super )
As this doesn't seem to be documented much anywhere, I have made a small project on github to demonstrate:
NSStoryboardSegue transitions from one NSViewController to another in the same Storyboard
NSViewController present: methods to achieve the same affect to a separate Xib-based NSViewController without using a Storyboard Segue
presentViewController:asPopoverRelativeToRect:ofView:preferredEdge:behavior:
presentViewControllerAsSheet:
presentViewControllerAsModalWindow:
presentViewController:animator:
animator and segue objects in Objective-C and Swift
edit
OK I've tracked down the EXC_BAD_ACCESS issue. Looking in the stack trace it seemed to have something to do with (Objective-C) NSString to (Swift) String conversion.
That made wonder about the identifier property of NSStoryboardSegue. This is used when setting up segues in the Storyboard, and is not so useful in Custom segues created in code. However, it turns out that if you set an identifier in the storyboard to any string value, even "", the crash disappears.
The identifier property is an NSString* in Objective-C
#property(readonly, copy) NSString *identifier
and an optional String in Swift:
var identifier: String? { get }
Note the read-only status. You can only set the identifier on initialising the object.
The designator initialiser for NSStoryboardSegue looks like this in Objective-C:
- (instancetype)initWithIdentifier:(NSString *)identifier
source:(id)sourceController
destination:(id)destinationController
and in Swift:
init(identifier identifier: String,
source sourceController: AnyObject,
destination destinationController: AnyObject)
Note the non-optional requirement in the Swift initialiser. Therein lies the problem and the crash. If you don't deliberately set an identifier in the storyboard, the Custom segue's designated initialiser will be called using a nil value for the identifier. Not a problem in Objective-C, but bad news for Swift.
The quick solution is to ensure you set an identifier string in Storyboard. For a more robust solution, it turns out that you can override the designated initialiser in your custom subclass to intercept a nil-valued string. Then you can fill it in with a default value before passing on to super's designated initialiser:
override init(identifier: String?,
source sourceController: AnyObject,
destination destinationController: AnyObject) {
var myIdentifier : String
if identifier == nil {
myIdentifier = ""
} else {
myIdentifier = identifier!
}
super.init(identifier: myIdentifier,
source: sourceController,
destination: destinationController)
}
I have updated the sample project to reflect this solution
The same issue comes to me since I forgot make Identity to the segue.
After that, my segue subclass could worked fine.
Highly recommend you take a look at the Apple documentation. If you dig into it a bit, you'll notice in the perform method, you can override animations and such:
SWIFT
func perform()
OBJECTIVE-C
- (void)perform
"You can override this method in your NSStoryboardSegue subclass to perform custom animation between the starting/containing controller and the ending/contained controller for a storyboard segue. Typically, you would use Core Animation to set up an animation from one set of views to the next. For more complex animations, you might take a snapshot image of the two view hierarchies and manipulate the images instead of the view objects.*
Regardless of how you perform the animation, you are responsible for installing the destination view controller o window controller (and its contained views) in the right place so that it can handle events. Typically, this entails calling one of the presentation methods in the NSViewController class."
What you might do as well is have a look at some of the iOS UIStoryboardSegue examples out there in the wild and you should find they're quite similar.

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.
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
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
}
}
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.
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.
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.
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).
try this, after instantiateViewControllerWithIdentifier.
- (void)setClass:(Class)c {
object_setClass(self, c);
}
like :
SubViewController *vc = [sb instantiateViewControllerWithIdentifier:#"MainViewController"];
[vc setClass:[SubViewController class]];
Although it's not strictly a subclass, you can:
option-drag the base class view controller in the Document Outline to make a copy
Move the new view controller copy to a separate place on the storyboard
Change Class to the subclass view controller in the Identity Inspector
Here's an example from a Bloc tutorial I wrote, subclassing ViewController with WhiskeyViewController:
This allows you to create subclasses of view controller subclasses in the storyboard. You can then use instantiateViewControllerWithIdentifier: to create specific subclasses.
This approach is a bit inflexible: later modifications within the storyboard to the base class controller don't propagate to the subclass. If you have a lot of subclasses you may be better off with one of the other solutions, but this will do in a pinch.
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
}
}
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")
}
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)!
}
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
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)
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.
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);
}
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()