Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Present view controller modally at app launch

My application has a setup screen that should be presented modally on the root view controller if certain conditions are met.

I have looked around on SO and the internet and the closest answer so far to how to go about doing this is here:

AppDelegate, rootViewController and presentViewController

There are 2 problems with this approach however:

  1. In iOS 8, doing it this way makes a log appear in the console, which doesn't seem to be an error, but is probably not good nonetheless:

Unbalanced calls to begin/end appearance transitions for UITabBarController: 0x7fe20058d570.

  1. The root view controller actually shows up very briefly when the app launches, and then fades into the presented view controller (even though I explicitly call animated:NO on my presentViewController method).

I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed. This is to say, I don't want to dynamically change my root view controller to my setup screen, and present my app experience modally when the user is done setting up.

Presenting the view controller on my root view controller viewDidLoad method also leads to a noticeable blink of the UI when the app is launched for the first time.

Is it possible to programmatically present a view controller modally, before the application has rendered anything so that the first view in place is the modal view controller?

UPDATE: Thank you for the comments, adding my current code as suggested:

In my AppDelegate.m:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

    [self.window makeKeyAndVisible];
    [self.window.rootViewController presentViewController:[storyboard instantiateViewControllerWithIdentifier:@"setupViewController"] animated:NO completion:NULL];

    return YES;
}

This does what I need except for the fact that it briefly shows the window's root view controller for a second when the application launches, then fades the setupViewController, which I find odd given that I am presenting it without animation and fading is not how a modal view controller is presented anyway.

The only thing that has gotten me close is manually adding the view in the root view controller's view did load method like so:

- (void)viewDidLoad
{
    [self.view addSubview:setupViewController.view];
    [self addChildViewController:setupViewController];
}

The problem with this approach is that I can no longer "natively" dismiss the setupViewController, and will now need to deal with the view hierarchy and animated it out myself, which is fine if it's the only solution, but I was hoping there was a sanctioned way of adding a view controller modally without animation before the root view controller displays.

UPDATE 2: After trying a lot of things out and waiting for an answer for 2 months, this question proposes the most creative solution:

iOS Present modal view controller on startup without flash

I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears. However the suggestion in that thread is to create an instance of your Launch Screen and leave that on for longer than default until the modal view controller has had a chance to present itself.

like image 585
Ftoledo Avatar asked Jan 31 '15 03:01

Ftoledo


People also ask

How do I remove a presented view controller?

To dismiss a modally presented view controller, call the view controller's dismiss(animated:completion:) method.


3 Answers

I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears.

Before it appears, no, you can't present. But there are multiple valid approaches to solve this visually. I recommend solution A below for its simplicity.

A. add launchScreen as subview, then present, then remove launchscreen

Solution is presented here by ullstrm and does not suffer from Unbalanced calls to begin/end appearance transitions:

let launchScreenView = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!.view!
launchScreenView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
launchScreenView.frame = window!.rootViewController!.view.bounds
window?.rootViewController?.view.addSubview(launchScreenView)
window?.makeKeyAndVisible()
// avoiding: Unbalanced calls to begin/end appearance transitions.
DispatchQueue.global().async {
    DispatchQueue.main.async {
        self.window?.rootViewController?.present(myViewControllerToPresent, animated: false, completion: {
            launchScreenView.removeFromSuperview()
        })
    }
}

B. addChildViewController first, then remove, then present

Solution is presented here by Benedict Cohen.

like image 176
Cœur Avatar answered Oct 12 '22 13:10

Cœur


I was in the same boat as you and found the same answer. I learned that you can get rid of the first problem (unbalanced calls warning) by setting the modalPresentationStyle of your setupViewController to .OverCurrentContext or .OverFullScreen. Problem solved - so I thought.

Only later I noticed the second problem and that was something I couldn't live with... back to square one.

As you, I wanted a solution with a normal view hierarchy and I didn't want to 'fake' something. I think the most elegant solution is switching your windows rootViewController on first dismissal of your setupViewController.

So, at launch, you set the setupViewController as the rootViewController (if needed):

var window: UIWindow?
var tabBarController: UITabBarController!

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    tabBarController = window!.rootViewController as! UITabBarController
    if needsToShowSetup() {
        let setupViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SetupViewController") as! SetupViewController
        window?.rootViewController = setupViewController
    }
    return true
}

When setup is done you call a method in your appDelegate to switch to the 'real' rootViewController:

func switchToTabBarController() {
    let setupUpViewController = window!.rootViewController!

    tabBarController.view.frame = window!.bounds
    window!.insertSubview(tabBarController.view, atIndex: 0)

    let height = setupUpViewController.view.bounds.size.height
    UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1, options: .allZeros, animations: { () -> Void in
        setupUpViewController.view.transform = CGAffineTransformMakeTranslation(0, height)
        }) { (completed) -> Void in
            self.window!.rootViewController = self.tabBarController
    }
}

I was after a 'cover vertical' dismiss animation. For crossfade and others, you could use UIView.transitionFromView(fromView: UIView, toView: UIView...). Hereafter you can present/dismiss your setupController the normal way, so your doneButton action could be something like this:

@IBAction func doneButtonSelected(sender: UIButton) {
    if presentingViewController != nil {
        presentingViewController!.dismissViewControllerAnimated(true, completion: nil)
    } else {
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.switchToTabBarController()
    }
}

Actually, I implemented this through delegation with the appDelegate being the delegate the first time around.

like image 41
Kjellie Avatar answered Oct 12 '22 12:10

Kjellie


I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed.

I have two suggestions. One is to try doing this in viewDidAppear:. I tried it and although you do see the root view controller's view if you look carefully, you barely see it, and sometimes you don't see it at all if you blink:

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self presentViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"setupViewController"] animated:NO completion:NULL];
}

Of course you'd need to add a flag so that you don't do that every time viewDidAppear: is called - otherwise you'll never be able to get back to this view controller at all! But that's trivial and I leave it as an exercise for the reader.

My other suggestion - and you have clearly thought about doing this - is to use a custom embedded (child) view controller instead. That works around the limitations of the whole "presentation" thing.

I'd launch and set up things dynamically, as you say, with the child view controller present if needed, configuring it all during the launch process. The child view controller's view would just cover the root view controller's view. So that's what the user would see as the app launches.

And then when the user's setup procedure is over and the user "dismisses" this view, you tear that view down, with animation, and remove the child view controller - revealing the root view controller's view underneath. The animation will make this all indistinguishable from the dismissal of presented view, even though it isn't really one.

like image 22
matt Avatar answered Oct 12 '22 11:10

matt