Best way to switch between UISplitViewController and other view controllers?
Asked Answered
L

9

29

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?

Leitmotif answered 18/11, 2010 at 9:3 Comment(1)
How you push your menu view as a full-screen modal dialog onto the UISplitViewController? I have the same problem, I define a modal segue from split view to menu view in storyboard and then in my splitviewcontroller code use performSegueWithIdentifier in viewDidApear: but this way user always sees a glimpse of split view before the menu modal? can this problem be solved? where should i call performseguewithidentifier to prevent this problem?Conceptualism
B
6

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

Boyfriend answered 18/11, 2010 at 9:17 Comment(2)
Thanks Bourne! We also have a login screen on top of the rest, but I left that out for brevity. I'm still quite surprised that Apple put all these restricitons on UISplitViewController (amongst things) and then completely fail to tell you how to get around the restrictions, e.g. 'use modals'. I think the Apple docs need more (any?) high level UI design ideas/patterns.Leitmotif
I think you guys answered my question: it is not possible. See #4483026Malapropos
S
20

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.

Shannon answered 22/9, 2014 at 17:36 Comment(7)
i've got this working, thanks for the code, but it still shows the login (with a logout button) if already logged in. how do I have it skip that when you first open the app and you are already logged in?Politico
@naturalc I'm using NSUserDefaults to store a boolean value for me. As far as this solution goes, I wish I had the option to give more than one upvote. This saved my life. Thank you so much!!Volz
this works good, but it fills up the memory with every switch between these... any help for that problem? I need to change the rootViewController more often than only when loggin in...Lavolta
This was great and it worked for me. However, I'm not using a Login screen initially - just "home" screen with a button that takes me to the SplitVC and another button that will take me to another VC. My question is, when I go from my "home" VC to the SplitVC, how can I return back to the "home" VC? Thanks.Ardys
@Ardys sorry, I don't understand what problem do you have, there's probably need for more context in your question.Shannon
@Shannon - yeah, sorry about that. I followed the above and now I'm able to swap out my root view controller with a new "home" page (HomeVC). This page has 2 buttons. One leads to the (original) splitVC. SO when I'm in the SplitVC screens, I need to be able to go BACK to the HomeVC to allow the user to pick a 2nd button that leads to another VC.Ardys
@Ardys well, you just need to change implementation of setupRootViewController: method to include 3 view controllers that you're using, maybe even add it some parameter with which you could select the proper VC to be displayed or something like that.Shannon
B
6

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

Boyfriend answered 18/11, 2010 at 9:17 Comment(2)
Thanks Bourne! We also have a login screen on top of the rest, but I left that out for brevity. I'm still quite surprised that Apple put all these restricitons on UISplitViewController (amongst things) and then completely fail to tell you how to get around the restrictions, e.g. 'use modals'. I think the Apple docs need more (any?) high level UI design ideas/patterns.Leitmotif
I think you guys answered my question: it is not possible. See #4483026Malapropos
N
3

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.

Nyeman answered 14/11, 2013 at 16:49 Comment(0)
M
1

For future iOS developers running into the same issue: here's another answer and explanations. You HAVE to make it root view controller. If it is not, overlay a modal.

UISplitviewcontroller not as a rootview controller

Malapropos answered 19/12, 2010 at 16:49 Comment(1)
Well you may write your own splitview controller. That's what I do whenever I need do.Boyfriend
A
1

Just ran into this problem on a project and thought I'd share my solution. In our case (for iPad), we wanted to start with a UISplitViewController with both view controllers visible (using preferredDisplayMode = .allVisible). At some point in the detail (right) hierarchy (we had a navigation controller for this side, too) we wanted to push a new view controller over the entire split view controller (not use a modal transition).

On iPhone, this behavior comes for free—as only one view controller is visible at any time. But on iPad we had to figure something else out. We ended up going with a root container view controller that adds the split view controller to it as a child view controller. This root view controller is embedded in a navigation controller. When the detail view controller in the split view controller wants to push a new controller over the entire split view controller, the root view controller pushes this new view controller with its navigation controller.

Addict answered 8/3, 2018 at 13:50 Comment(2)
Doesn't this defeat the rule that the UISplitViewController "needs to be the root view controller" and not embedded in any containment?Abundance
Taken from developer.apple.com/documentation/uikit/uisplitviewcontroller: "...the split view controller is typically the root view controller of your app’s window, but it may be embedded in another view controller."Addict
C
0

I'd like to contribute my approach to presenting a UISplitViewController, as you might like to via -presentViewController:animated:completion: (we all know that won't work though). I created a UISplitViewController subclass which responds to:

-presentAsRootViewController
-returnToPreviousViewController

The class, which like other successful approaches, sets the UISplitViewController as the window's rootViewController but does so with an animation similar to what you get (by default) with -presentViewController:animated:completion:

PresentableSplitViewController.h

#import <UIKit/UIKit.h>    
@interface PresentableSplitViewController : UISplitViewController    
- (void) presentAsRootViewController;
@end

PresentableSplitViewController.m

#import "PresentableSplitViewController.h"

@interface PresentableSplitViewController ()
@property (nonatomic, strong) UIViewController *previousViewController;
@end

@implementation PresentableSplitViewController

- (void) presentAsRootViewController {

    UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
    _previousViewController=window.rootViewController;

    UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
    window.rootViewController = self;

    [window insertSubview:windowSnapShot atIndex:0];

    CGRect dstFrame=self.view.frame;

    CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
    offset.width*=self.view.frame.size.width;
    offset.height*=self.view.frame.size.height;
    self.view.frame=CGRectOffset(self.view.frame, offset.width, offset.height);

    [UIView animateWithDuration:0.5
                          delay:0.0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         self.view.frame=dstFrame;
                     } completion:^(BOOL finished) {
                         [windowSnapShot removeFromSuperview];
                     }];
}

- (void) returnToPreviousViewController {
    if(_previousViewController) {

        UIWindow *window=[[[UIApplication sharedApplication] delegate] window];

        UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
        window.rootViewController = _previousViewController;

        [window addSubview:windowSnapShot];

        CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
        offset.width*=windowSnapShot.frame.size.width;
        offset.height*=windowSnapShot.frame.size.height;

        CGRect dstFrame=CGRectOffset(windowSnapShot.frame, offset.width, offset.height);

        [UIView animateWithDuration:0.5
                              delay:0.0
             usingSpringWithDamping:1.0
              initialSpringVelocity:0.0
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             windowSnapShot.frame=dstFrame;
                         } completion:^(BOOL finished) {
                             [windowSnapShot removeFromSuperview];
                             _previousViewController=nil;
                         }];
    }
}

@end
Chabot answered 30/10, 2014 at 18:29 Comment(0)
B
0

I did a UISplitView as initial view, than it goes modally to a fullscreen UIView and back to UISplitView. If you need to go back to the SplitView you have to use a custom segue.

Read this link (translate it from japanese)

UIViewController to UISplitViewController

Biotite answered 12/11, 2014 at 17:32 Comment(0)
D
0

Adding to the answer of @tadija I am in a similar situation:

My app was for phones only, and I am adding a tablet UI. I decided doing it in Swift in the same app - and eventually migrate all the app to use the same storyboard (when I feel the IPad version is stable, using it for phones should be trivial with the new classes from XCode6).

No segues were defined in my scene yet and it still works.

My the code in my app delegate is in ObjectiveC, and is slightly different - but uses the same idea. Note that I am using the default view controller from the scene, unlike previous examples. I feel this will also work on IOS7/IPhone in which the runtime will generate a regular UINavigationController instead of a UISplitViewController. I might even add new code which will push the login view controller on IPhones, instead of changing the rootVC.

- (void) setupRootViewController:(BOOL) animated {
    UIViewController *newController = nil;
    UIStoryboard *board = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewAnimationOptions transition = UIViewAnimationOptionTransitionCrossDissolve;

    if (!loggedIn) {
        newController = [board instantiateViewControllerWithIdentifier:@"LoginViewController"];
    } else {
        newController = [board instantiateInitialViewController];
    }

    if (animated) {
        [UIView transitionWithView: self.window duration:0.5 options:transition animations:^{
            self.window.rootViewController = newController;
            NSLog(@"setup root view controller animated");
        } completion:^(BOOL finished) {
            NSLog(@"setup root view controller finished");
        }];
    } else {
        self.window.rootViewController = newController;
    }
}
Diann answered 22/12, 2014 at 13:44 Comment(0)
D
0

Another option: In the details view controller I display a modal view controller:

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if (!appDelegate.loggedIn) {
    // display the login form
    let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
    let login = storyboard.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
    self.presentViewController(login, animated: false, completion: { () -> Void in
       // user logged in and is valid now
       self.updateDisplay()
    })
} else {
    updateDisplay()
}

Don't dismiss the login controller without setting the login flag. Note that in IPhones the master view controller will come first, so a very similar code will need to be on the master view controller.

Diann answered 22/12, 2014 at 14:19 Comment(2)
where in the file are you actually putting this? in the configure view? in the view did load? somewhere else? - i put this in the viewdidload and it says detailviewcontroller does not have a member named updateDisplayPolitico
This code belongs in the initial controller (after the login). The missing function is the one which fills up the details in the display controller.Diann

© 2022 - 2024 — McMap. All rights reserved.