Best way to switch between UISplitViewController and other view controllers?
I seriously didn't believe that this concept of having some UIViewController to show before UISplitViewController (login form for example) turns out to be so complicated, until I had to create that kind of view hiearchy.
My example is based on iOS 8 and XCode 6.0 (Swift), so I'm not sure if this problem existed before in a same way, or it's due to some new bugs introduced with iOS 8, but from all of the similar questions I found, I didn't see complete 'not very hacky' solution to this problem.
I'll guide you through some of the things I have tried before I ended up with a solution (at the end of this post). Each example is based on creating new project from Master-Detail template without CoreData enabled.
First try (modal segue to UISplitViewController):
- create new UIViewController subclass (LoginViewController for example)
- add new view controller in storyboard, set it as initial view controller (instead of UISplitViewController) and connect it to LoginViewController
- add UIButton to LoginViewController and create modal segue from that button to UISplitViewController
- move boilerplate setup code for UISplitViewController from AppDelegate's
didFinishLaunchingWithOptions
to LoginViewController'sprepareForSegue
This almost worked. I say almost, because after the app is started with LoginViewController and you tap button and segue to UISplitViewController, there is a strange bug going on: showing and hiding master view controller on orientation change is no longer animated.
After some time struggling with this problem and without real solution, I thought that it's somehow connected with that weird rule that UISplitViewController must be rootViewController (and in this case it isn't, LoginViewController is) so I gave up from this not so perfect solution.
Second try (modal segue from UISplitViewController):
- create new UIViewController subclass (LoginViewController for example)
- add new view controller in storyboard, and connect it to LoginViewController (but this time leave UISplitViewController to be initial view controller)
- create modal segue from UISplitViewController to LoginViewController
- add UIButton to LoginViewController and create unwind segue from that button
Finally, add this code to AppDelegate's didFinishLaunchingWithOptions
after boilerplate code for setting up UISplitViewController:
window?.makeKeyAndVisible()
splitViewController.performSegueWithIdentifier("segueToLogin", sender: self)
return true
or try with this code instead:
window?.makeKeyAndVisible()
let loginViewController = splitViewController.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
splitViewController.presentViewController(loginViewController, animated: false, completion: nil)
return true
Both of these examples produce same several bad things:
- console outputs:
Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
- UISplitViewController must be shown first before LoginViewController is segued modally (I would rather present only the login form so the user doesn't see UISplitViewController before logged in)
- Unwind segue doesn't get called (this is totally other bug, and I'm not going into that story now)
Solution (update rootViewController)
The only way I found which works properly is if you change window's rootViewController on the fly:
- Define Storyboard ID for LoginViewController and UISplitViewController, and add some kind of loggedIn property to AppDelegate.
- Based on this property, instantiate appropriate view controller and after that set it as rootViewController.
- Do it without animation in
didFinishLaunchingWithOptions
but animated when called from the UI.
Here is sample code from AppDelegate:
var loggedIn = false
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
setupRootViewController(false)
return true
}
func setupRootViewController(animated: Bool) {
if let window = self.window {
var newRootViewController: UIViewController? = nil
var transition: UIViewAnimationOptions
// create and setup appropriate rootViewController
if !loggedIn {
let loginViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
newRootViewController = loginViewController
transition = .TransitionFlipFromLeft
} else {
let splitViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
let controller = masterNavigationController.topViewController as MasterViewController
newRootViewController = splitViewController
transition = .TransitionFlipFromRight
}
// update app's rootViewController
if let rootVC = newRootViewController {
if animated {
UIView.transitionWithView(window, duration: 0.5, options: transition, animations: { () -> Void in
window.rootViewController = rootVC
}, completion: nil)
} else {
window.rootViewController = rootVC
}
}
}
}
And this is sample code from LoginViewController:
@IBAction func login(sender: UIButton) {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
delegate.loggedIn = true
delegate.setupRootViewController(true)
}
I would also like to hear if there is some better/cleaner way for this to work properly in iOS 8.
Touche! Ran in to the same issue and solved it the same way using modals. In my case it was a login view and then the main menu as well to be shown before the splitview. I used the same strategy as thought out by you. I (as well as several other knowledgeable iOS folks I spoke to) could not find a better way out. Works fine for me. User never notices the modal anyway. Present them so. And yes I can also tell you that there are quite a few apps doing the same under the hood tricks on the App store. :) On another note, do let me know if you figure a better way out somehow someway sometime :)
And who said you can have only one window ? :)
See if my answer on this similar question can help.
This approach is working very well for me. As long as you don't have to worry about multiple displays or state restoration, this linked code should be enough to do what you need: you don't have to make your logic look backwards or rewrite existing code, and can still take advantage of the UISplitView in a deeper level within your application - without (AFAIK) breaking Apple guidelines.