Prevent runtime crash for function instantiateViewController(withIdentifier:) - ios

Swift 3.0
From what I found here is UIStoryboard always return non-optional instance in function instantiateViewController(withIdentifier:).
open class UIStoryboard : NSObject {
...
open func instantiateViewController(withIdentifier identifier: String) -> UIViewController
}
The crash happen if we adding wrong identifier value, without noticed.
This case might happen in complex projects which have large number of controller in many storyboards, and a controller StoryboardId is missed.
The only solution I found is making a function to temporary create all controllers when start app delegate (this function should be called in debugging mode only).
func validateControllers() {
guard let _ = xyzStoryboard.instantiateViewController(withIdentifier: "ABC") as? ABCViewController else {
fatalError("fail in init controller ABC from storyboard XYZ")
}
...
}
But I wonder if we could have another way to handle this situation. Or should I raise this issue to Swift team? Thanks!

as far as i know there is no solution for preventing crashes like this. but this is a good thing and it's meant to crash like this to show you there is something wrong with your code and you need to fix them!

Writing this answer to just to let you know how we handled the accidental typo(s) of a View Controller Identifier, which could lead to error when you try to create an ViewController from Storyboard(s) using view controller's identifier.
We had a complex project which had almost 15-20 ViewControllers, we didn't put them in a single storyboard instead we shared these VCs across multiple story boards and then we created an object called StoryBoardManager, which would create an VC from various storyboard(s) and hand it over to us.
We also created couple of enums to represent various storyboard(s) and viewController(s) inside.
It somewhat looks like this,
enum Storyboards : String {
case main = "Main"
case signup = "SignUp"
case discover = "Discover"
case utility = "Utility"
case event = "Event"
}
enum ViewControllers : String {
case login = "login"
case onBoard = "on_board"
case signup = "signup"
case signupNavigation = "signupNavigaitonVC"
case discoverNavigation = "discoverNavigation"
case individualProfileSetUp = "individualProfileSetUp"
case organizationProfileSetUp = "organizationProfileSetUp"
case discover = "discover"
case imagePickerMenuVC = "imagePickerMenuVC"
case eventDiscoverMapNavigation = "eventDiscoverMapNavigationVC"
case eventDiscoverMapVC = "eventDiscoverMapVC"
case event = "event"
}
class StoryboardManager {
static func storyboard(name:Storyboards) -> UIStoryboard {
let aStoryboard = UIStoryboard(name: name.rawValue, bundle: nil)
return aStoryboard
}
static func viewController(storyboardId:Storyboards, viewControllerId:ViewControllers) -> UIViewController {
let storyboard = StoryboardManager.storyboard(storyboardId)
let viewcontroller = storyboard.instantiateViewControllerWithIdentifier(viewControllerId.rawValue)
return viewcontroller
}
}
We mainly did this to avoid the typo mistakes for the ViewController identifier(s), which would lead to an runtime error. You will add all the viewController(s) identifiers to ViewControllers enum and storyboard(s) names to the Storyboards enum as a enum case. We updated both the enums whenever we introduced a new storyboard or a new ViewController in any of the storyboards and this helped all the team members not to make the typo for ViewController identifier.
Then we created ViewController(s) using the below code,
let loginVC = StoryboardManager.viewController(.main, viewControllerId: .login)
HTH :)

Short brief
Currently, I have a complex project that have 5 storyboards, with 20 controllers. In my case, using storyboard segue seems not a good idea. Therefore, creating controller's instance from storyboard and make a transition by code is much better.
But the problem is that instantiateViewController(withIdentifier:) always return non-optional instance, and it will be crashed if we put an invalid identifier (which didn't define in storyboard).
Solution (Swift 3.0)
There are steps to prevent this problem.
In *.storyboard, use controller class name as StoryboardID
Create UIStoryboard+Ext.swift to define all storyboards & controllers belong
Create BaseViewController.swift to define the initialization from storyboard
Create function to init all storyboards & controllers, to prevent crashing in runtime.
Download sample code from if you want to try yourself: https://github.com/nahung89/StoryboardPattern
Detail (TL;DR)
Step 1:
Step 2:
extension UIStoryboard {
enum Identifier: String {
case main = "Main"
case explore = "Explore"
case search = "Search"
case profile = "Profile"
}
static func name(for controller: UIViewController.Type) -> String? {
var identifier: Identifier?
switch controller.className {
case HomeViewController.className:
identifier = .main
case ExploreViewController.className:
identifier = .explore
case SearchViewController.className:
identifier = .search
case ProfileViewController.className:
identifier = .profile
default:
break
}
return identifier?.rawValue
}
}
extension UIStoryboard {
static func instanceFromIdentifier(_ identifier: Identifier) -> UIStoryboard {
return UIStoryboard(name: identifier.rawValue, bundle: Bundle.main)
}
static func instanceFromName(_ name: String) -> UIStoryboard? {
guard let identifier = Identifier(rawValue: name) else { return nil }
return instanceFromIdentifier(identifier)
}
}
Step 3:
extension UIViewController {
var className: String {
return String(describing: type(of: self))
}
class var className: String {
return String(describing: self)
}
}
class BaseViewController: UIViewController {
static var controllerId: String {
return String(describing: self) // return slass name, i.e "ExploreViewController"
}
static func instanceFromStoryboard() -> Self? {
guard let storyboardName = UIStoryboard.name(for: self) else { return nil }
return instantiateFrom(storyboardName: storyboardName)
}
private static func instantiateFrom<VC: UIViewController>(storyboardName: String) -> VC? {
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: controllerId) as? VC
return controller
}
}
Step 4
extension AppDelegate {
func validateStoryboards() {
guard
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.main.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.explore.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.profile.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.search.rawValue)
else {
fatalError("fail to init storyboard by name")
}
guard let _ = HomeViewController.instanceFromStoryboard(),
let _ = ExploreViewController.instanceFromStoryboard(),
let _ = ProfileViewController.instanceFromStoryboard(),
let _ = SearchViewController.instanceFromStoryboard()
else {
fatalError("fail to init controller from storyboard")
}
}
}

Related

UITabBarController Shared Data Model - share & update model from anywhere

I'm using a TabBarcontroller type app and I'm using a shared model of the form:
enum WorkoutState {
case Stopped
case Started
case Paused
}
class BaseTBController: UITabBarController {
var workoutState: WorkoutState? = .Stopped
}
Currently all is working and I can access and update the variable across the different tabs using
let tabbar = tabBarController as! BaseTBController
if tabbar.workoutState = .Stop {
//do something
tabbar.workoutState = .Start
}
Now, the situation is that I seem to need to put this all over the place in my code. eg:
startRun()
resumeRun()
pauseRun()
Is there a better way to do this instead of putting
let tabbar = tabBarController as! BaseTBController
tabbar.workoutState = .Start
in each of the 3 functions?
You can always use protocol and default extension to achieve what you need
protocol HandleWorkStateProtocol where Self: UIViewController {
func updateWorkOutState(to: WorkoutState)
}
extension HandleWorkStateProtocol {
func updateWorkOutState(to state: WorkoutState) {
guard let tabBarController = self.tabBarController as? BaseTBController else { return }
tabBarController.workoutState = state
}
}
In all you view controller's that has these 3 methods (startRun, resumeRun, pauseRun) simply confirm to this protocol and call updateWorkOutState(to: with appropriate value to modify the status
class SomeTestViewController: UIViewController {
func startRun() {
self.updateWorkOutState(to: .Started)
}
func resumeRun() {
}
func pauseRun() {
self.updateWorkOutState(to: .Paused)
}
}
extension SomeTestViewController: HandleWorkStateProtocol {}
P.S
Case values of enum does not follow Pascal casing like Stopped instead it follows Camel casing stopped so change your enum values to
enum WorkoutState {
case stopped
case started
case paused
}

How can I get array of UIStoryboard in Swift?

I find that my Storyboard has become very complex and decided to split it into different Storyboards. But I want to have the freedom to instantiate a UIViewController no matter where I put the view controller in. That way, I can move around View Controller from Storyboard to Storyboard without the need to remember where did I put that View Controller, and I also don't have to update the code at all because they all use the same code to instantiate the same View Controller with that name, no matter where it resides.
Therefore, I want to create an extension of UIViewController like this:
extension UIViewController {
func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
guard let named = named else { return nil; }
if let sbName = fromStoryboard {
let sb = UIStoryboard(name: sbName, bundle: nil);
return sb.instantiateViewController(withIdentifier: named);
}
else {
for sb in UIStoryboard.storyboards {
if let vc = sb.instantiateViewController(withIdentifier: named) {
return vc;
}
}
}
return nil;
}
The problem is, I cannot find the property / method to return the list of storyboard instances like .storyboards anywhere. Is there any workaround on this? I know that I can probably have a static list of storyboard names, but that way, the extension won't be dynamic and independent of the projects.
Can anybody help? Thanks.
EDIT:
Combining the accepted answer and answer from here to safely instantiate viewcontroller (and return nil if not found), this is my code:
UIStoryboard+Storyboards.swift:
extension UIStoryboard {
static var storyboards : [UIStoryboard] {
let directory = Bundle.main.resourcePath! + "/Base.lproj"
let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
let storyboardArray = storyboardNames.map({ UIStoryboard(name: $0, bundle: Bundle.main )})
return storyboardArray;
}
func instantiateViewControllerSafe(withIdentifier identifier: String) -> UIViewController? {
if let availableIdentifiers = self.value(forKey: "identifierToNibNameMap") as? [String: Any] {
if availableIdentifiers[identifier] != nil {
return self.instantiateViewController(withIdentifier: identifier)
}
}
return nil
}
}
UIViewController+Instantiate.swift:
extension UIViewController {
static func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
guard let named = named else { return nil; }
if let sbName = fromStoryboard {
let sb = UIStoryboard(name: sbName, bundle: nil);
return sb.instantiateViewControllerSafe(withIdentifier: named);
}
else {
for sb in UIStoryboard.storyboards {
if let vc = sb.instantiateViewControllerSafe(withIdentifier: named) {
return vc;
}
}
}
return nil;
}
}
With the restriction that all the storyboard files must be located on Base.lproj folder.
It's not the most efficient code in terms of running time, I know. But for now, it's easy enough to be understood and I can live with this. :) Thanks for everybody who helps!
Storyboards are normally set up to be localized, so you should look for them in the Base.lproj subdirectory of your bundle's resource directory. A storyboard is compiled into a file with the extension .storyboardc, so you should look for files with that suffix and strip it.
let directory = Bundle.main.resourcePath! + "/Base.lproj"
let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
Swift.print(storyboardNames)
If you have created device-specific storyboards (by adding ~ipad or ~iphone to the storyboard filenames) then you'll also want to strip off those suffixes and eliminate duplicates.
Note that the compiled storyboard suffix in particular is not a documented part of the SDK, so it could change in a future version of iOS / Xcode.
Don't do that.
Instead, use Storyboard References to segue to different storyboards as necessary.
In your situation, maybe it would be cleaner if you discard storyboard all together. And create all your view controllers programmatically.
As stated elsewhere storyboards are compiled to opaque (i.e. binary, undocumented) .storyboardc files when the app is compiled and run. The UIStoryboard API only allows instantiating the initial View Controller, or a (known) named one, so there's no naive way to interrogate your app for 'unknown' storyboard elements. There may also be side-effects to instantiating UI elements that are not being displayed. However...
If you put your .storyboard files in a Copy Files build phase you can interrogate the XML and recover e.g. UIViewController/UIView identifiers, custom properties etc. IBDecodable seems to do a reasonable job of this. Installation-wise (cocoapods) it's MacOS-only but will run happily if you embed it in your iOS app (install the SWXMLHash prerequisite and clone IBDecodable/git-submodule it, whatever). There are probably efficiency and localisation issues I'm skimping on but for my use case (building onboarding popovers from custom properties without being explicit about it) it worked OK.
To answer the question posed more specifically, interrogating the storyboards for IDs would allow the app to find (via a dict or similar), and instantiate a View Controller, wherever it was visually stored.
For example:
Bundle.main.urls(forResourcesWithExtension: "storyboard", subdirectory: nil)?
.forEach({ (url) in
do {
let file = try StoryboardFile(url: url)
if !file.document.launchScreen {
file.document.scenes?.forEach({ (scene) in
scene.viewController?.viewController.rootView?.subviews?
.forEach({ (view) in
// Do stuff...
})
})
}
} catch { /* ... */ }
})

Custom segue to a different storyboard

Question:
How might one write a custom segue that would allow you to embed view controllers from a different storyboard?
Context:
I am trying to write a custom segue with which I can link from one storyboard to another. A good article on atomicobject.com illustrates how to create a segue that originates from a button / event etc. Translated into swift, and allowing for non UINavigationControllers, the code looks like:
public class SegueToStoryboard : UIStoryboardSegue {
private class func viewControllerInStoryBoard(identifier:String, bundle:NSBundle? = nil)
-> UIViewController?
{
let boardScene = split(identifier, { $0 == "." }, maxSplit: Int.max, allowEmptySlices: false)
switch boardScene.count {
case 2:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateViewControllerWithIdentifier(boardScene[1]) as? UIViewController
case 1:
let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
return sb.instantiateInitialViewController() as? UIViewController
default:
return nil
}
}
override init(identifier: String!,
source: UIViewController,
destination ignore: UIViewController) {
let target = SegueToStoryboard.viewControllerInStoryBoard(identifier, bundle: nil)
super.init(identifier: identifier, source: source,
destination:target != nil ? target! : ignore)
}
public override func perform() {
let source = self.sourceViewController as UIViewController
let dest = self.destinationViewController as UIViewController
source.addChildViewController(dest)
dest.didMoveToParentViewController(source)
source.view.addSubview(dest.view)
// source.navigationController?.pushViewController(dest, animated: true)
}
}
Problem:
The problem that I am having with both their Obj-C and the above Swift code is that when you try to use the via a container view (with semantics of an embed segue - starting with an embed segue, deleting the segue, and then use the above custom segue), it crashes before ever calling the segue code with the following method-not-found error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIStoryboardSegueTemplate 0x7ffc8432a4f0>
setValue:forUndefinedKey:]: this class is not key value
coding-compliant for the key containerView.'
I have tried to inspect the address listed but get no meaningful results. I do the see the bold statement that it expecting the containerView but don't know how one might isolate, satisfy, and/or work around this problem.
Summary:
My end goal is to embed view controllers defined in separate storyboards to facilitate collaboration and testing without having to write additional code (a non invasive solution). Does anyone have any insight into how to accomplish this greater task? I could fall back to hybrid approach of calling performSegue, but would like to find a single, contained, and complete solution. The above code gets there for event driven (buttons etc) segues, but not with the embed segue.
Any input is appreciated, thanks in advance.
Your approach works fine for custom segues to push / display modally other view controllers but not for embed segues. The reason for this is that the "Embed" segue is not a subclass of UIStoryboardSegue but inherits from UIStoryboardSegueTemplate, which is a private API.
Unfortunately I couldn't find a better way to achieve what you want than with the hybrid approach.
My way is to link the containerView and delete the viewDidLoad segue from it. and manually call the segue on viewdidLoad
public protocol EmbeddingContainerView {
var containerView: UIView! { get set }
}
public class CoreSegue: UIStoryboardSegue {
public static func instantiateViewControllerWithIdentifier(identifier: String) -> UIViewController {
let storyboard = UIStoryboard(name: "Core", bundle: NSBundle(forClass: self))
let controller = storyboard.instantiateViewControllerWithIdentifier(identifier) as! UIViewController
return controller
}
var isPresent = false
var isEmbed = false
override init!(identifier: String?, source: UIViewController, destination: UIViewController) {
if var identifier = identifier {
if identifier.hasPrefix("present ") {
isPresent = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("present ")))
}
if identifier.hasPrefix("embed ") {
isEmbed = true
identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("embed ")))
}
let controller = CoreSegue.instantiateViewControllerWithIdentifier(identifier)
super.init(identifier: identifier, source: source, destination: controller)
} else {
super.init(identifier: identifier, source: source, destination: destination)
}
}
public override func perform() {
if let source = sourceViewController as? UIViewController, dest = destinationViewController as? UIViewController {
if isPresent {
let nav = UINavigationController(rootViewController: dest)
nav.navigationBarHidden = true // you might not need this line
source.presentViewController(nav, animated: true, completion: nil)
} else if isEmbed {
if let contentView = (source as? EmbeddingContainerView)?.containerView {
source.addChildViewController(dest)
contentView.addSubview(dest.view)
dest.view.fullDimension() // which comes from one of my lib
}
} else {
source.navigationController?.pushViewController(destinationViewController as! UIViewController, animated: true)
}
}
}
}
and later in your code:
class MeViewController: UIViewController, EmbeddingContainerView {
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
performSegueWithIdentifier("embed Bookings", sender: nil)
}
}

Optional binding succeeds if it shouldn't

This is what I posted as a possible solution to Traverse view controller hierarchy in Swift (slightly modified):
extension UIViewController {
func traverseAndFindClass<T where T : UIViewController>(T.Type) -> T? {
var currentVC = self
while let parentVC = currentVC.parentViewController {
println("comparing \(parentVC) to \(T.description())")
if let result = parentVC as? T { // (XXX)
return result
}
currentVC = parentVC
}
return nil
}
}
The method should traverse up the parent view controller hierarchy and return the first
instance of the given class, or nil if none is found.
But it does not work, and I cannot figure out why. The optional binding
marked with (XXX) always succeeds, so that the first parent view controller is returned
even if it is not an instance of T.
This can easily be reproduced: Create a project from the "iOS Master-Detail Application"
template in Xcode 6 GM, and add the following code to viewDidLoad() of the
MasterViewController class:
if let vc = self.traverseAndFindClass(UICollectionViewController.self) {
println("found: \(vc)")
} else {
println("not found")
}
self is a MasterViewController (a subclass of UITableViewController), and its
parent view controller is a UINavigationController.
There is no UICollectionViewController in the parent view
controllers hierarchy, so I would expect that the method
returns nil and the output is "not found".
But this is what happens:
comparing <UINavigationController: 0x7fbc00c4de10> to UICollectionViewController
found: <UINavigationController: 0x7fbc00c4de10>
This is obviously wrong, because UINavigationController is not a subclass of
UICollectionViewController. Perhaps I made some stupid error, but I could not find it.
In order to isolate the problem, I also tried to reproduce it with my own class
hierarchy, independent of UIKit:
class BaseClass : NSObject {
var parentViewController : BaseClass?
}
class FirstSubClass : BaseClass { }
class SecondSubClass : BaseClass { }
extension BaseClass {
func traverseAndFindClass<T where T : BaseClass>(T.Type) -> T? {
var currentVC = self
while let parentVC = currentVC.parentViewController {
println("comparing \(parentVC) to \(T.description())")
if let result = parentVC as? T { // (XXX)
return result
}
currentVC = parentVC
}
return nil
}
}
let base = BaseClass()
base.parentViewController = FirstSubClass()
if let result = base.traverseAndFindClass(SecondSubClass.self) {
println("found: \(result)")
} else {
println("not found")
}
And guess what? Now it works as expected! The output is
comparing <MyApp.FirstSubClass: 0x7fff38f78c40> to MyApp.SecondSubClass
not found
UPDATE:
Removing the type constraint in the generic method
func traverseAndFindClass<T>(T.Type) -> T?
as suggested by #POB in a comment makes it work as expected.
Replacing the optional binding by a "two-step binding"
if let result = parentVC as Any as? T { // (XXX)
as suggested by #vacawama in his answer also makes it work as expected.
Changing the build configuration from "Debug" to "Release" also makes the
method work as expected. (I have tested this only in the iOS Simulator so far.)
The last point could indicate that this is a Swift compiler or runtime bug. And I still
cannot see why the problem occurs with subclasses of UIViewController, but not
with subclasses of my BaseClass. Therefore I will keep the question open for a
while before accepting an answer.
UPDATE 2: This has been fixed as of Xcode 7.
With the final Xcode 7
release the problem does not occur anymore. The optional binding
if let result = parentVC as? T in the traverseAndFindClass() method
now works (and fails) as expected, both in Release and Debug configuration.
If you try to conditionally cast an object of type UINavigationController to a UICollectionViewController in a Playground:
var nc = UINavigationController()
if let vc = nc as? UICollectionViewController {
println("Yes")
} else {
println("No")
}
You get this error:
Playground execution failed: :33:16: error: 'UICollectionViewController' is not a subtype of 'UINavigationController'
if let vc = nc as? UICollectionViewController {
but if instead you do:
var nc = UINavigationController()
if let vc = (nc as Any) as? UICollectionViewController {
println("Yes")
} else {
println("No")
}
it prints "No".
So I suggest trying:
extension UIViewController {
func traverseAndFindClass<T where T : UIViewController>(T.Type) -> T? {
var currentVC = self
while let parentVC = currentVC.parentViewController {
println("comparing \(parentVC) to \(T.description())")
if let result = (parentVC as Any) as? T { // (XXX)
return result
}
currentVC = parentVC
}
return nil
}
}

UIViewController extension to instantiate from storyboard

I'm trying to write a little extension in Swift to handle instantiation of a UIViewController from a storyboard.
My idea is the following: Since UIStoryboard's method instantiateViewControllerWithIdentifier needs an identifier to instantiate a given storyboard's view controller, why don't assign every view controller in my storyboard an identifier equal to its exact class name (i.e a UserDetailViewController would have an identifier of "UserDetailViewController"), and, create a class method on UIViewController that would:
accept a UIStoryboard instance as a unique parameter
get the current class name as a string
call instantiateViewControllerWithIdentifier on the storyboard instance with the class name as a parameter
get the newly created UIViewController instance, and return it
So, instead of (which repeats the class name as a string, not very nice)
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("UserDetailViewController") as UserDetailViewController
it would be:
let vc = UserDetailViewController.instantiateFromStoryboard(self.storyboard!)
I used to do it in Objective-C with the following category:
+ (instancetype)instantiateFromStoryboard:(UIStoryboard *)storyboard
{
return [storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
}
But I'm completely stuck with the Swift version. I hope is that there is some kind of way to do it.
I tried the following:
extension UIViewController {
class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self {
return storyboard.instantiateViewControllerWithIdentifier(NSStringFromClass(Self))
}
}
Returning Self instead of AnyObject allows the type inference to work. Otherwise, I would have to cast every single return of this method, which is annoying, but maybe you have a better solution?
This gives me the error: Use of unresolved identifier 'Self'
The NSStringFromClass part seems to be the problem.
What do you think?
Is there any way to return Self from class functions?
How would you get this working without the need to cast the return value every time? (i.e keeping -> Self as return value)
How about writing an extension to UIStoryboard instead of UIViewController?
extension UIStoryboard {
func instantiateVC<T: UIViewController>() -> T? {
// get a class name and demangle for classes in Swift
if let name = NSStringFromClass(T.self)?.componentsSeparatedByString(".").last {
return instantiateViewControllerWithIdentifier(name) as? T
}
return nil
}
}
Even adopting this approach, cost of an use side is low as well.
let vc: UserDetailViewController? = aStoryboard.instantiateVC()
Thanks to MartinR and his answer, I know the answer:
UPDATE: rewritten with a protocol.
Instantiable
protocol StringConvertible {
var rawValue: String {get}
}
protocol Instantiable: class {
static var storyboardName: StringConvertible {get}
}
extension Instantiable {
static func instantiateFromStoryboard() -> Self {
return instantiateFromStoryboardHelper()
}
private static func instantiateFromStoryboardHelper<T>() -> T {
let identifier = String(describing: self)
let storyboard = UIStoryboard(name: storyboardName.rawValue, bundle: nil)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
//MARK: -
extension String: StringConvertible { // allow string as storyboard name
var rawValue: String {
return self
}
}
StoryboardName
enum StoryboardName: String, StringConvertible {
case main = "Main"
//...
}
Usage:
class MyViewController: UIViewController, Instantiable {
static var storyboardName: StringConvertible {
return StoryboardName.main //Or you can use string value "Main"
}
}
let viewController = MyController.instantiateFromStoryboard()
you can create UIViewController Instance like this:
Create enum with all your storyboard name.
enum AppStoryboard: String {
case main = "Main"
case profile = "Profile"
}
Then, here is the extension for instantiate UIViewController
extension UIViewController {
class func instantiate<T: UIViewController>(appStoryboard: AppStoryboard) -> T {
let storyboard = UIStoryboard(name: appStoryboard.rawValue, bundle: nil)
let identifier = String(describing: self)
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}
}
Usage:
let profileVC: ProfileVC = ProfileVC.instantiate(appStoryboard: .profile)
self.navigationController?.pushViewController(profileVC,animated:true)
We are porting our objective c project to swift. We have split the project into modules. Modules have their own storyboards. We have extended your(even our's as well) problem's solution to one more level by avoiding explicit storyboard names.
// Add you modules here. Make sure rawValues refer to a stroyboard file name.
enum StoryModule : String {
case SomeModule
case AnotherModule = "AnotherModulesStoryBoardName"
// and so on...
}
extension UIStoryboard {
class func instantiateController<T>(forModule module : StoryModule) -> T {
let storyboard = UIStoryboard.init(name: module.rawValue, bundle: nil);
let name = String(T).componentsSeparatedByString(".").last
return storyboard.instantiateViewControllerWithIdentifier(name!) as! T
}
}
// Some controller whose UI is in a stroyboard named "SomeModule.storyboard",
// and whose storyboardID is the class name itself, ie "MyViewController"
class MyViewController : UIViewController {
// Controller Code
}
// Usage
class AClass
{
// Here we must alwasy provide explicit type
let viewController : MyViewController = UIStoryboard.instantiateController(forModule: StoryModule.SomeModule)
}
Two things:
Class constructors in Objective-C are convenience initializers in Swift. Use convenience init rather than class func.
NSStringFromClass(Self) with NSStringFromClass(self.type).
Here is a modern Swift example, based on #findall's solution:
extension UIStoryboard {
func instantiate<T>() -> T {
return instantiateViewController(withIdentifier: String(describing: T.self)) as! T
}
static let main = UIStoryboard(name: "Main", bundle: nil)
}
Usage:
let userDetailViewController = UIStoryboard.main.instantiate() as UserDetailViewController
I think it is ok to fail when trying to instantiate a view controller from a storyboard as this kind of problem should be detected soon.
Or, you can do so
func instantiateViewControllerWithIdentifier<T>(_ identifier: T.Type) -> T {
let identifier = String(describing: identifier)
return instantiateViewController(withIdentifier: identifier) as! T
}
Use protocol in UIViewController to reach your thoughts
let vc = YourViewController.instantiate(from: .StoryboardName)
You can see the use of my link :D
https://github.com/JavanC/StoryboardDesignable
You can add this extension :-
extension UIStoryboard{
func instantiateViewController<T:UIViewController>(type: T.Type) -> T? {
var fullName: String = NSStringFromClass(T.self)
if let range = fullName.range(of:".", options:.backwards, range:nil, locale: nil){
fullName = fullName.substring(from: range.upperBound)
}
return self.instantiateViewController(withIdentifier:fullName) as? T
}
}
And can instantiate view controller like this :-
self.storyboard?.instantiateViewController(type: VC.self)!
In complement for the version of #ChikabuZ, here mine that takes into account which bundle the storyboard is in (for example, if your storyboads are in another bundle than your app). I added also a small func if you want to use xib instead of storyboad.
extension UIViewController {
static func instantiate<TController: UIViewController>(_ storyboardName: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName)
}
static func instantiate<TController: UIViewController>(_ storyboardName: String, identifier: String) -> TController {
return instantiateFromStoryboardHelper(storyboardName, identifier: identifier)
}
fileprivate static func instantiateFromStoryboardHelper<T: UIViewController>(_ name: String, identifier: String? = nil) -> T {
let storyboard = UIStoryboard(name: name, bundle: Bundle(for: self))
return storyboard.instantiateViewController(withIdentifier: identifier ?? String(describing: self)) as! T
}
static func instantiate<TController: UIViewController>(xibName: String? = nil) -> TController {
return TController(nibName: xibName ?? String(describing: self), bundle: Bundle(for: self))
}
}
I had a similar thought and settled on using the extension below. It still uses the normal instantiation process, but removes reliance on stringly typed Storyboard and View Controller names:
let myVC = UIStoryboard(.main).instantiate(MyViewController.self)
The return type above is pre-cast to MyViewController, not the standard UIViewController.
extension UIStoryboard {
enum Name: String {
case main = "Main"
case launch = "LaunchScreen"
case other = "Other"
}
convenience init(_ name: Name, bundle: Bundle? = nil) {
self.init(name: name.rawValue, bundle: bundle)
}
func instantiate<T: UIViewController>(_ type: T.Type) -> T {
instantiateViewController(withIdentifier: String(describing: type)) as! T
}
}
Note that you must ensure that each VC's Storyboard Identifier exactly matches its class name! Failure to do so will result in the exception:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard (<UIStoryboard: 0x6000035c04e0>) doesn't contain a view controller with identifier 'MyViewController''

Resources