Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to switch between UISplitViewController and other view controllers?

I'm authoring an iPad app. One of the screens in the app is perfectly suited to using a UISplitViewController. However, the top level of the app is a main menu, which I don't want to use a UISplitViewController for. This presents a problem, because Apple state that:

  1. UISplitViewController should be the top level view controller in the app, i.e. its view should be added as the subview of UIWindow

  2. if used, UISplitViewController should be there for the lifetime of the app -- i.e. don't remove its view from UIWindow and put another in place, or vice versa

Having read around and experimented, it seems to only viable option to satisfy Apple's requirements and our own is to use modal dialogs. So our app has a UISplitViewController at the root level (i.e. its view added as the subview of UIWindow), and to show our main menu, we push it as a full-screen modal dialog onto the UISplitViewController. Then by dismissing the main menu view controller modal dialog, we can actually show our split view.

This strategy seems to work fine. But it begs the questions:

1) Is there any better way of structuring this, without modals, that also meets all the requirements mentioned? It seems a bit odd having the main UI appear by virtue of being pushed as a modal dialog. (Modals are supposed to be for focused user tasks.)

2) Am I at risk of app store rejection because of my approach? This modal strategy is probably 'misusing' modal dialogs, as per Apple's human interface guidelines. But what other choice have they given me? Would they know that I'm doing this, anyway?

like image 508
occulus Avatar asked Nov 18 '10 09:11

occulus


People also ask

How do I switch between view controllers?

From the Library, add a button to the first View Controller and name the button, for example: Show Second View . Select the button, press and hold the Control key on the keyboard and drag from the button to the second View Controller.

What is Navigation Controller in IOS?

The navigation controller manages the navigation bar at the top of the interface and an optional toolbar at the bottom of the interface. The navigation bar is always present and is managed by the navigation controller itself, which updates the navigation bar using the content provided by its child view controllers.


1 Answers

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):

  1. create new UIViewController subclass (LoginViewController for example)
  2. add new view controller in storyboard, set it as initial view controller (instead of UISplitViewController) and connect it to LoginViewController
  3. add UIButton to LoginViewController and create modal segue from that button to UISplitViewController
  4. move boilerplate setup code for UISplitViewController from AppDelegate's didFinishLaunchingWithOptions to LoginViewController's prepareForSegue

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):

  1. create new UIViewController subclass (LoginViewController for example)
  2. add new view controller in storyboard, and connect it to LoginViewController (but this time leave UISplitViewController to be initial view controller)
  3. create modal segue from UISplitViewController to LoginViewController
  4. 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:

  1. console outputs: Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
  2. 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)
  3. 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:

  1. Define Storyboard ID for LoginViewController and UISplitViewController, and add some kind of loggedIn property to AppDelegate.
  2. Based on this property, instantiate appropriate view controller and after that set it as rootViewController.
  3. 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.

like image 67
tadija Avatar answered Oct 04 '22 15:10

tadija