3

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!

Chen Li Yong
  • 5,459
  • 8
  • 58
  • 124
  • you can get storyboards viewcontroller's navigation stack array – Jitendra Modi Jun 02 '17 at 04:30
  • within this line self.navigationController.viewControllers you can get navigation array of your viewcontrollers – Jitendra Modi Jun 02 '17 at 04:31
  • @JitendraModi do you mean `self.navigationController.viewControllers` ? What I mean is I want to instantiate new viewController from the UIStoryboard, not accessing an already existing viewControllers on navigation stack array. – Chen Li Yong Jun 02 '17 at 04:31
  • mean what you want to do? You want to navigate through new viewcontroller with passing an array? – Jitendra Modi Jun 02 '17 at 04:32
  • If your viewController knows about his storyboard?, this can be the solution for what you need? – Reinier Melian Jun 02 '17 at 04:33
  • @JitendraModi what I want to do is I want to be able to instantiate (create) a new View Controller from a defined template inside any storyboard, without mentioning the storyboard. – Chen Li Yong Jun 02 '17 at 04:33
  • then you can create a new xib it is without storyboard. You can instantiate anytime anywhere in whole project – Jitendra Modi Jun 02 '17 at 04:34
  • just create a constant array that holding the storyboard name? why thats so hard? not like u are dynamically creating new one – Tj3n Jun 02 '17 at 04:34
  • @ReinierMelian no, I want to have a list / an array of *all* the storyboard available to the project, dynamically. Just like how we can get all the `subviews` within any `view`, or all `viewControllers` stack within `navigationController`. – Chen Li Yong Jun 02 '17 at 04:35
  • Another way is loop through `[NSBundle mainBundle]` and get the path that contains `.storyboard` or something, then can extract the name out – Tj3n Jun 02 '17 at 04:38
  • @Tj3n creating the list that is not hard, and I have mentioned that in my question. But that way, I cannot, for instance, wrap this in a plugin in Github code and let other people use the code, without that person also create a list of their storyboard IDs. More over, supplying the static list of storyboards also makes the user need to edit the original source code, which is not what "plugin" is intended. Or I can also make the function accepts array of storyboard ID strings, but that will be very ugly. – Chen Li Yong Jun 02 '17 at 04:38
  • @Tj3n okay, never messing up with `NSBundle` before, but I will try to look into that. Thanks. – Chen Li Yong Jun 02 '17 at 04:39
  • @ChenLiYong You can also use `Bundle.main.urls(forResourcesWithExtension: "storyboardc", subdirectory: "Base.lproj")` check this solution of my for more detail on it https://stackoverflow.com/a/44126152/6433023 – Nirav D Jun 02 '17 at 04:55

4 Answers4

4

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Oh wow this is great! Let me try to incorporate this into the code and will get back to you again. Thanks! – Chen Li Yong Jun 02 '17 at 04:42
  • "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." --> it's ok, I can probably add some workaround and precaution on that, now that I know how `NSBundle` works. – Chen Li Yong Jun 02 '17 at 04:45
  • 1
    Even if it changes, it won't affect you until you rebuild your app with a newer iOS deployment target. – rob mayoff Jun 02 '17 at 04:47
0

Don't do that.

Instead, use Storyboard References to segue to different storyboards as necessary.

  • That also can be done. But I kinda don't like segue. Segue clutters the storyboard if the viewcontrollers are already abundant (hence why I want to separate them to different storyboards). But it will still clutters. Let's just say I want to try out different methods of programming to find out what suited me the best. :) – Chen Li Yong Jun 02 '17 at 06:31
0

In your situation, maybe it would be cleaner if you discard storyboard all together. And create all your view controllers programmatically.

Mikasa
  • 328
  • 2
  • 7
  • That's a great idea... for me. Not for the developers who continue my work. :( – Chen Li Yong Jun 02 '17 at 06:21
  • Teach them how to do it. It's not that complicated. A good iOS developer should be able to do both. (\w storyboard and programmatically) – Mikasa Jun 02 '17 at 06:23
  • It's not complicated, but not too productive, especially in terms of visually designing the layout. The one who can manipulate the design quickly is the one who create the design in the first place. Other people will still need to dig in to the code and search for that one line of code which control the y-position of that label inside that cell inside that tableview inside that container inside that page control inside that... – Chen Li Yong Jun 02 '17 at 06:28
0

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 { /* ... */ }
        })
Robin Macharg
  • 1,468
  • 14
  • 22