How can I get array of UIStoryboard in Swift? - ios

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 { /* ... */ }
})

Related

Using a variable to type cast in Swift 3

I have two functions that are nearly identical and I want to merge them into one but I cannot find out how to handle the type casting in my if-let statement. There are only two solutions that I can think of but I cannot execute either of them.
Here are the two functions (there's a lot more to them but this is the only part that is causing me trouble in the merge):
func loadNextEventViewController() {
if let nextEventViewController = storyboard?.instantiateViewController(withIdentifier: "EventViewController") as? EventViewController {
// Executed code in here
}
}
func loadFinishViewController() {
if let finishViewController = storyboard?.instantiateViewController(withIdentifier: "FinishViewController") as? FinishViewController {
// Executed code in here
}
}
My first attempt was to make a generic parameter that could accept either the EventViewController OR FinalViewController, but as far as I can tell, there is no logical OR for generic parameters, only logical AND.
My second attempt was to create a computer variable but this didn't work either.
How can I take an argument in my function call that I could cast to be either class type in my if-let block?
Example:
func loadViewController(identifier: String, viewControllerType: UIViewController)
I've solved this issue in a very clunky way by using an in-else statement but I'd like to find a more elegant way of solving this problem.
You can do it this way
func load(_ viewController: UIViewController) {
if viewController is EventViewController {
//do stuff
}
if viewController is FinishViewController {
//do stuff
}
//do stuff that applies to all types of view controllers
}
Then call it like this:
let eventVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("EventViewController")
load(eventVC)
If you want to avoid using if statements you can use a switch statement instead, like so:
func load(_ viewController: UIViewController) {
switch viewController {
case is EventViewController:
//do stuff for EventViewController
case is FinishViewController:
//do stuff for FinishViewControllers
default:
//do stuff for other types, or break
}
//do stuff that applies to all viewControllers
}
This is not a problem of casting.. in your application you have two options, move to one vc or the other. There could be alot of logic that makes this decision or not. but either way, a decision HAS to be made at some point on what VC to show. You just need to decide on the best place to make this decision.
based on what you've shown so far, you could just do:
func showController(identifier: String) {
if let vc = storyboard?.instantiateViewController(withIdentifier: identifier) as? UIViewController {
// Executed code in here
}
}
I presume though, that each route has a different actions that require handling differently. The most common approach here is to use a segue, then you can use prepareForSegue to capture the segue and set properties as required based on the segue identifier. If you can provide some more information I can provide a better answer.
The main thing you need to consider here are.. whats actually different between the two. determine this and you can refactor your code to reduce repetition
You can do it like this
func loadViewController(identifier: String) {
let vc = storyboard?.instantiateViewController(withIdentifier: identifier) as? ViewController
if let eventVC = vc as? EventViewController {
// Executed code in here
} else if let finishVC = vc as? FinishViewController {
// Executed code in here
}
}

Passing data to a specific ViewController

The picture is just an example...
So basically I want to pass data from RED to BLUE but i have to go through GREEN.. is there a way to just pass the data straight to BLUE without it having to go through GREEN?
What I'm doing now is the conventional method of passing the data to GREEN then having GREEN read it and pass it on to BLUE
But my app has a lot of ViewControllers and I wish to be able to pass it straight to the final page (BLUE page) is there a way? Sorry I'm new to the Swift
If you try to pass data from _Red to _Blue, without _Green - it about bad architecture, becouse if something changed in logic of app, and you need to changed flow of controllers - you will feel a lot of headache...
You can wrap your data in object and pass through or you can make DataController which will be manage your data, and your ViewController's will be get\set it by Datacontroller interface
Use a simple swift class e.g Sharedclass to set a variable and then get the value of the variable from the same Sharedclass. Example is given below
import Foundation
class SharedClass {
static let sharedInstance = SharedClass()
var phoneNumber = ""
}
To set the variable
SharedClass.sharedInstance.phoneNumber = "123456"
To get the variable
BLUEViewController.phoneNumber = SharedClass.sharedInstance.phoneNumber
You can push your navigationController directly to one of your viewControllers. Try this;
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let blueController = storyboard.instantiateViewController(withIdentifier: "BlueViewController") as? BluewViewController {
blueController.passedData = passedData
navigationController?.pushViewController(blueController, animated: true)
}
Or you can just present it too. Instead of pushing in navigation controller.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let blueController = storyboard.instantiateViewController(withIdentifier: "BlueViewController") as? BluewViewController {
blueController.passedData = passedData
present(blueController, animated: true, completion: nil)
}

Prevent runtime crash for function instantiateViewController(withIdentifier:)

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

How to override init method and return an storyboard instance in swift

In Objective-C, I can easily do this use the following codes:
- (instancetype)init {
UIStoryboard *sb = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
self = [sb instantiateViewControllerWithIdentifier:#"WMViewController"];
return self;
}
How can I implement this using Swift?
I know it is strange to do this, but for some reasons, I must init a viewController like the following:
let vcClass = WMTableViewController.self
// ...
let vc = vcClass.init()
So in order to support storybord / xib, it will be easy if can override init method and return another instance.
Here is the work I am doing:
I am trying to convert my little lib (WMPageController) to Swift (WMPageController-Swift), then I am stucked here.I would be happy if you have another suggestion or solution to deal with it.
Thanks a lot if you would like to help me.
Nice question. I tried to solve the same problem in the past.
A specific solution
First of all you could just use a class method
class WMViewController: UIViewController {
class func loadFromStoryboard() -> WMViewController? {
return UIStoryboard(name: "main", bundle: nil).instantiateViewControllerWithIdentifier("WMViewController") as? WMViewController
}
}
And use it like this
let controller = WMViewController.loadFromStoryboard()
A general solution
However adding the loadFromStoryboard method to each ViewController you want to make "loadable" is a bad practice. So we could move that code inside a protocol extension.
protocol StoryboardLoadable { }
extension StoryboardLoadable where Self:UIViewController {
private static var identifier: String { return "\(Self.self)" }
static func loadFromStoryboard() -> Self? {
return UIStoryboard(name: "main", bundle: nil).instantiateViewControllerWithIdentifier(identifier) as? Self
}
}
Now, all you need to do to add the loadFromStoryboard method to your custom view controller is this single line
extension WMViewController: StoryboardLoadable {}
That's it. Now you can load your view controller writing
let controller = WMViewController.loadFromStoryboard()

Localize storyboard programatically on the fly

My app allows user to change country, so if they change to Thailand, I am supposed to update my storyboard etc's language.
P/S: I've seen a lot of people say if you relaunch the app you'll be fine, but that's really a user experience issue I wanted to prevent.
The following is my current attempt, it works fine for programatically created/modified/defined text but it doesn't update the text on my storyboard :(
import UIKit
class LocalizationHelper: NSObject {
static var bundle: NSBundle?
static let sharedInstance = LocalizationHelper()
func localizedStringForKey(key: String, comment: String) -> String {
return (LocalizationHelper.bundle?.localizedStringForKey(key, value: comment, table: nil))!
}
func setLanguage(language: String) {
NSUserDefaults.standardUserDefaults().setObject(language, forKey: "LocalizedStringUserDefaultsKey")
NSUserDefaults.standardUserDefaults().synchronize()
if let path = NSBundle.mainBundle().pathForResource(language, ofType: "lproj") {
print(language)
print(language)
LocalizationHelper.bundle = NSBundle(path: path)
print(LocalizationHelper.bundle)
} else {
NSUserDefaults.standardUserDefaults().setObject("Base", forKey: "LocalizedStringUserDefaultsKey")
NSUserDefaults.standardUserDefaults().synchronize()
LocalizationHelper.bundle = NSBundle.mainBundle()
}
}
}
The print statement result is
Optional(NSBundle </var/mobile/Containers/Bundle/Application/91776A02-BADA-4EC9-A451-45A453B84C83/Appname.app/th.lproj> (not yet loaded))
Does this matter anything?
For the record, I referenced this
https://github.com/jslim89/JSLocalizedString
P/S: I have also have my Main.string(Thai) in my Main.storyboard(Base) ready, just that they only update when I change System Language :(
I believe you have outlets defined for each UI component in storyboard. You can simply localize them by calling NSLocalizedString:
self.title = NSLocalizedString("MyTitle", nil);
The other option could be to have multiple storyboard one for each supported language. Once user change the country, based on the supported language pick the right storyboard. Something like this:
let storyboard = UIStoryboard(name: "MainStoryboard_English", bundle: nil)

Resources