36

I'd like to present modally, at first startup, a tutorial wizard to the user.

Is there a way to present a modal UIViewController on application startup, without seeing, at least for a millisecond, the rootViewController behind it?

Now I'm doing something like this (omitting first-launch checks for clarity):

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // ...

    UIStoryboard *storyboard = self.window.rootViewController.storyboard;
    TutorialViewController* tutorialViewController = [storyboard instantiateViewControllerWithIdentifier:@"tutorial"];
    tutorialViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
    [self.window makeKeyAndVisible];
    [self.window.rootViewController presentViewController:tutorialViewController animated:NO completion:NULL];
}

with no luck. I've tried to move [self.window makeKeyAndVisible]; to before the [... presentViewController:tutorialViewController ...] statement, but then the modal doesn't even appear.

sonxurxo
  • 5,648
  • 2
  • 23
  • 33

9 Answers9

35

All presentViewController methods require the presenting view controller to have appeared first. In order to hide the root VC an overlay must be presented. The Launch Screen can continued to be presented on the window until the presentation has completed and then fadeout the overlay.

    UIView* overlayView = [[[UINib nibWithNibName:@"LaunchScreen" bundle:nil] instantiateWithOwner:nil options:nil] firstObject];
overlayView.frame = self.window.rootViewController.view.bounds;
overlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

UIStoryboard *storyboard = self.window.rootViewController.storyboard;
TutorialViewController* tutorialViewController = [storyboard instantiateViewControllerWithIdentifier:@"tutorial"];
tutorialViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self.window makeKeyAndVisible];
[self.window addSubview:overlayView];
[self.window.rootViewController presentViewController:tutorialViewController animated:NO completion:^{
    NSLog(@"displaying");
    [UIView animateWithDuration:0.5 animations:^{
        overlayView.alpha = 0;
    } completion:^(BOOL finished) {
        [overlayView removeFromSuperview];
    }];
}];
Bruce Burnham
  • 351
  • 3
  • 3
  • 2
    If anyone is facing _Unbalanced calls to begin/end appearance transitions_, please see **Spoek** answer for a solution using `DispatchQueue`. – Cœur Mar 15 '17 at 04:24
  • 3
    I think **Spoek** changed his name to **ullstrm**, anyway this is the solution **Cœur** refers to: https://stackoverflow.com/a/41469734/84783 – rob5408 Jun 07 '17 at 16:33
13

Bruce's upvoted answer in Swift 3:

if let vc = window?.rootViewController?.storyboard?.instantiateViewController(withIdentifier: "LOGIN")
    {
        let launch = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
        launch.view.frame = vc.view.bounds
        launch.view.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight]
        window?.makeKeyAndVisible()
        window?.addSubview(launch.view)

        //Using DispatchQueue to prevent "Unbalanced calls to begin/end appearance transitions"
        DispatchQueue.global().async {
            // Bounce back to the main thread to update the UI
            DispatchQueue.main.async {
                self.window?.rootViewController?.present(vc, animated: false, completion: {

                    UIView.animate(withDuration: 0.5, animations: {
                        launch.view.alpha = 0
                    }, completion: { (_) in
                        launch.view.removeFromSuperview()
                    })
                })
            }
        }
    }
ullstrm
  • 9,812
  • 7
  • 52
  • 83
  • 4
    Best solution so far. You could even add an explanation on why you had to do `DispatchQueue.global().async` to solve _Unbalanced calls to begin/end appearance transitions_. – Cœur Mar 15 '17 at 04:28
  • On some devices this seems to occasionally be causing an endless hang on the launch screen for me. Anybody else having this problem? – shim Jun 26 '18 at 17:40
  • Seems to be an "attempt to present on view which isn't in the view hierarchy" issue, which I am attempting to solve simply by adding a delay to the switch to the main thread, although I could be fancier and set it up to wait until the main view controller is fully loaded. – shim Jul 03 '18 at 18:58
  • I'm experiencing problems with this on iOS 13 beta 6. I set `launch .modalPresentationStyle = .fullScreen` but the `window?.addSubview(launch.view)` results in the presentation not being fullscreen but some strange mix between the full screen and the card presentation. Anyone else having problems here on iOS 13? – Micky Aug 22 '19 at 13:02
  • Ok, seems like using a second instance of the launch VC and adding that ones view to the window instead of the same instance fixes the issues on iOS 13. Unlike in this example, I was using the view of the vc to be presented. – Micky Aug 22 '19 at 14:58
8

may be your can use the "childViewController"

UIStoryboard *storyboard = self.window.rootViewController.storyboard;
TutorialViewController* tutorialViewController = [storyboard instantiateViewControllerWithIdentifier:@"tutorial"];

[self.window addSubview: tutorialViewController.view];
[self.window.rootViewController addChildViewController: tutorialViewController];

[self.window makeKeyAndVisible];

When you need to dismiss your tutor, you can remove its view from the superview. Also you can add some animation on the view by setting the alpha property.Hope helpful:)

Pandara
  • 139
  • 7
  • 3
    I was just trying this approach. I can not accept this as correct answer since it does not use modal presentations, but i've upvoted it, thank you! – sonxurxo Oct 14 '14 at 08:40
  • hahaha, you can refer to some doc about "childViewController", which is a efficent way to manager views.There is no only one correct way to do one thing, right?;) – Pandara Oct 14 '14 at 08:44
  • Right, but for anyone stumbling here searching for a solution for this, it wouldn't be correct IMHO. I'm going to use this approach anyway as it solves the root problem :) – sonxurxo Oct 14 '14 at 08:49
  • In my opinion this is the correct solution, since it cannot be done using a modalviewcontroller :-) – Carles Estevadeordal Feb 02 '16 at 21:17
7

This problem still exists in iOS 10. My fix was:

  1. in viewWillAppear add the modal VC as a childVC to the rootVC
  2. in the viewDidAppear:
    1. Remove the modalVC as a child of the rootVC
    2. Modally present the childVC without animation

Code:

extension UIViewController {

    func embed(childViewController: UIViewController) {
        childViewController.willMove(toParentViewController: self)

        view.addSubview(childViewController.view)
        childViewController.view.frame = view.bounds
        childViewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]

        addChildViewController(childViewController)
    }


    func unembed(childViewController: UIViewController) {
        assert(childViewController.parent == self)

        childViewController.willMove(toParentViewController: nil)
        childViewController.view.removeFromSuperview()
        childViewController.removeFromParentViewController()
    }
}


class ViewController: UIViewController {

    let modalViewController = UIViewController()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        //BUG FIX: We have to embed the VC rather than modally presenting it because:
        // - Modal presentation within viewWillAppear(animated: false) is not allowed
        // - Modal presentation within viewDidAppear(animated: false) is not visually glitchy
        //The VC is presented modally in viewDidAppear:
        if self.shouldPresentModalVC {
            embed(childViewController: modalViewController)
        }
        //...
    }


    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //BUG FIX: Move the embedded VC to be a modal VC as is expected. See viewWillAppear
        if modalViewController.parent == self {
            unembed(childViewController: modalViewController)
            present(modalViewController, animated: false, completion: nil)
        }

        //....
    }
}
Benedict Cohen
  • 11,912
  • 7
  • 55
  • 67
  • 1
    It's a nice alternative solution. But since we are dealing with a startup issue, it shouldn't be the viewController's responsibility to deal with workarounds. That's why I prefer **Spoek**'s solution which is purely done in AppDelegate. – Cœur Mar 15 '17 at 04:23
  • 1
    Note: Spoek name is now **ullstrm** – Cœur Aug 14 '17 at 15:55
  • @Cœur The issue does not necessarily only occur on startup; it can happen any time a view controller wants to present a modal view overtop of itself when it first appears. This solution solves it for the general case and is imo better than hacking the AppDelegate's startup sequence. If the decision to show the modal is made by the view controller, it is not appropriate for the AppDelegate to have anything to do with it. – devios1 Feb 15 '19 at 20:55
  • 1
    @devios1 ah, I didn't know the issue could happen outside of startup. I guess I need to experiment. But I can't give more points: I already upvoted this answer two years ago. – Cœur Feb 16 '19 at 13:30
1

May be a bad solution, but you could make a ViewController with 2 containers in it, where both of the containers are linked to a VC each. Then you can control which container should be visible in code, that's an idea

if (!firstRun) {
    // Show normal page
    normalContainer.hidden = NO;
    firstRunContainer.hidden = YES;
} else if (firstRun) {
    // Show first run page or something similar
    normalContainer.hidden = YES;
    firstRunContainer.hidden = NO;
}
Erik B
  • 40,889
  • 25
  • 119
  • 135
Erik
  • 2,500
  • 6
  • 28
  • 49
0

Bruce's answer pointed me in the right direction, but because my modal can appear more often than just on launch (it's a login screen, so it needs to appear if they log out), I didn't want to tie my overlay directly to the presentation of the view controller.

Here is the logic I came up with:

    self.window.rootViewController = _tabBarController;
    [self.window makeKeyAndVisible];

    WSILaunchImageView *launchImage = [WSILaunchImageView new];
    [self.window addSubview:launchImage];

    [UIView animateWithDuration:0.1f
                          delay:0.5f
                        options:0
                     animations:^{
                         launchImage.alpha = 0.0f;
                     } completion:^(BOOL finished) {
                         [launchImage removeFromSuperview];
                     }];

In a different section I perform the logic of presenting my login VC in the typical self.window.rootViewController presentViewController:... format which I can use regardless if it's an app launch or otherwise.

If anyone cares, here is how I created my overlay view:

@implementation WSILaunchImageView

- (instancetype)init
{
    self = [super initWithFrame:[UIScreen mainScreen].bounds];
    if (self) {
        self.image = WSILaunchImage();
    }
    return self;
}

And here's the logic for the launch image itself:

UIImage * WSILaunchImage()
{
    static UIImage *launchImage = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (WSIEnvironmentDeviceHas480hScreen()) launchImage = [UIImage imageNamed:@"LaunchImage-700"];
        else if (WSIEnvironmentDeviceHas568hScreen()) launchImage = [UIImage imageNamed:@"LaunchImage-700-568h"];
        else if (WSIEnvironmentDeviceHas667hScreen()) launchImage = [UIImage imageNamed:@"LaunchImage-800-667h"];
        else if (WSIEnvironmentDeviceHas736hScreen()) launchImage = [UIImage imageNamed:@"LaunchImage-800-Portrait-736h"];
    });
    return launchImage;
}

Aaaaand just for completion's sake, here is what those EnvironmentDevice methods look like:

static CGSize const kIPhone4Size = (CGSize){.width = 320.0f, .height = 480.0f};

BOOL WSIEnvironmentDeviceHas480hScreen(void)
{
    static BOOL result = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        result = CGSizeEqualToSize([UIScreen mainScreen].bounds.size, kIPhone4Size);
    });
    return result;
}
Stakenborg
  • 2,890
  • 2
  • 24
  • 30
0
let vc = UIViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = noFlashTransitionDelegate
present(vc, animated: false, completion: nil)

class NoFlashTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {

    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        if source.view.window == nil,
            let overlayViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController(),
            let overlay = overlayViewController.view {
                source.view.addSubview(overlay)
                UIView.animate(withDuration: 0, animations: {}) { (finished) in
                    overlay.removeFromSuperview()
            }
        }
        return nil
    }
}
Azon
  • 57
  • 4
0

It can be late but in your AppDelegate you can do this :

//Set your rootViewController
self.window.rootViewController=myRootViewController;
//Hide the rootViewController to avoid the flash
self.window.rootViewController.view.hidden=YES;
//Display the window
[self.window makeKeyAndVisible];

if(shouldPresentModal){

    //Present your modal controller
    UIViewController *lc_viewController = [UIViewController new];
    UINavigationController *lc_navigationController = [[UINavigationController alloc] initWithRootViewController:lc_viewController];
    [self.window.rootViewController presentViewController:lc_navigationController animated:NO completion:^{

        //Display the rootViewController to show your modal
        self.window.rootViewController.view.hidden=NO;
    }];
}
else{

    //Otherwise display the rootViewController
    self.window.rootViewController.view.hidden=NO;
}
Bejil
  • 412
  • 5
  • 18
-1

This is how I do it with storyboards and it works with multiple modals. This example has 3. Bottom, middle, and top.

Just be sure to have the storyboardID of each viewController set correctly in interface builder.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    UIStoryboard * storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    BottomViewController *bottomViewController = [storyboard instantiateViewControllerWithIdentifier:@"BottomViewController"];
    UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    [window setRootViewController:bottomViewController];
    [window makeKeyAndVisible];

    if (!_loggedIn) {
        MiddleViewController *middleViewController = [storyboard instantiateViewControllerWithIdentifier:@"middleViewController"];
        TopViewController *topViewController = [storyboard instantiateViewControllerWithIdentifier:@"topViewController"];

        [bottomViewController presentViewController:middleViewController animated:NO completion:nil];
        [middleViewController presentViewController:topViewController animated:NO completion:nil];

    }
    else {
        // setup as you normally would.
    }

    self.window = window;

    return YES;
}
Beau Nouvelle
  • 6,962
  • 3
  • 39
  • 54
  • Thank you but this way the short flash still appears – sonxurxo Feb 07 '15 at 10:11
  • I'm not seeing the rootviewcontroller at all with this code, even with loading 3 controllers. Although the controllers I'm using don't have a whole lot going on in them. Perhaps you're trying to load too much at one time? Could it be the launch screen you're seeing? – Beau Nouvelle Feb 07 '15 at 10:26
  • Whatever I try to load in the root view controller, sometimes it appears. And `sometimes` is not an option for me :( – sonxurxo Feb 07 '15 at 15:00
  • You may have to look into custom transitions. Load the top view controller first, and use a custom transition/segue to create an animation similar to a modal dismissal. – Beau Nouvelle Feb 08 '15 at 03:56