iOS在没有闪存的情况下启动模态视图控制器

时间:2022-09-06 21:09:06

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?

有没有办法在应用程序启动时呈现模态UIViewController,而不至少看到毫秒,后面的rootViewController?

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.

没有运气。我试图移动[self.window makeKeyAndVisible];到[... presentViewController:tutorialViewController ...]语句之前,但是模态甚至没有出现。

8 个解决方案

#1


27  

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.

所有presentViewController方法都要求呈现视图控制器首先出现。为了隐藏根VC,必须呈现覆盖。启动屏幕可以继续显示在窗口上,直到演示文稿完成,然后淡出叠加层。

    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];
    }];
}];

#2


8  

may be your can use the "childViewController"

可能是你可以使用“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:)

当您需要解雇您的导师时,您可以从超级视图中删除其视图。你也可以通过设置alpha属性在视图上添加一些动画。希望有用:)

#3


6  

Bruce's upvoted answer in Swift 3:

布鲁斯在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()
                    })
                })
            }
        }
    }

#4


5  

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

这个问题在iOS 10中仍然存在。我的修复是:

  1. in viewWillAppear add the modal VC as a childVC to the rootVC
  2. 在viewWillAppear中,将modal VC作为childVC添加到rootVC
  3. in the viewDidAppear:
    1. Remove the modalVC as a child of the rootVC
    2. 删除modalVC作为rootVC的子级
    3. Modally present the childVC without animation
    4. 以动态方式呈现childVC
  4. 在viewDidAppear中:将modalVC作为rootVC的子项删除模态显示没有动画的childVC

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

        //....
    }
}

#5


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.

在另一个部分中,我执行以典型的self.window.rootViewController presentViewController:...格式呈现我的登录VC的逻辑,无论是应用程序启动还是其他,我都可以使用。

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:

Aaaaand只是为了完成,这里是那些EnvironmentDevice方法的样子:

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;
}

#6


0  

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

可能是一个糟糕的解决方案,但你可以制作一个包含2个容器的ViewController,其中两个容器都链接到VC。然后你可以控制哪些容器应该在代码中可见,这是一个想法

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;
}

#7


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
    }
}

#8


-1  

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

这是我用故事板做的方式,它适用于多种模态。这个例子有3.底部,中部和顶部。

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

只需确保在界面构建器中正确设置每个viewController的storyboardID。

- (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;
}

#1


27  

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.

所有presentViewController方法都要求呈现视图控制器首先出现。为了隐藏根VC,必须呈现覆盖。启动屏幕可以继续显示在窗口上,直到演示文稿完成,然后淡出叠加层。

    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];
    }];
}];

#2


8  

may be your can use the "childViewController"

可能是你可以使用“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:)

当您需要解雇您的导师时,您可以从超级视图中删除其视图。你也可以通过设置alpha属性在视图上添加一些动画。希望有用:)

#3


6  

Bruce's upvoted answer in Swift 3:

布鲁斯在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()
                    })
                })
            }
        }
    }

#4


5  

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

这个问题在iOS 10中仍然存在。我的修复是:

  1. in viewWillAppear add the modal VC as a childVC to the rootVC
  2. 在viewWillAppear中,将modal VC作为childVC添加到rootVC
  3. in the viewDidAppear:
    1. Remove the modalVC as a child of the rootVC
    2. 删除modalVC作为rootVC的子级
    3. Modally present the childVC without animation
    4. 以动态方式呈现childVC
  4. 在viewDidAppear中:将modalVC作为rootVC的子项删除模态显示没有动画的childVC

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

        //....
    }
}

#5


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.

在另一个部分中,我执行以典型的self.window.rootViewController presentViewController:...格式呈现我的登录VC的逻辑,无论是应用程序启动还是其他,我都可以使用。

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:

Aaaaand只是为了完成,这里是那些EnvironmentDevice方法的样子:

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;
}

#6


0  

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

可能是一个糟糕的解决方案,但你可以制作一个包含2个容器的ViewController,其中两个容器都链接到VC。然后你可以控制哪些容器应该在代码中可见,这是一个想法

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;
}

#7


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
    }
}

#8


-1  

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

这是我用故事板做的方式,它适用于多种模态。这个例子有3.底部,中部和顶部。

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

只需确保在界面构建器中正确设置每个viewController的storyboardID。

- (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;
}