iOS启动时如何无闪现地显示模态视图控制器

36

我希望在第一次启动时以模态方式向用户呈现教程向导。

有没有办法在应用程序启动时呈现一个模态的UIViewController,而不会看到它后面的rootViewController,至少不要让它显示一毫秒?

现在我正在做类似于这样的事情(为了清晰起见省略了第一次启动检查):

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

我没有成功。我尝试将[self.window makeKeyAndVisible];移到[...presentViewController:tutorialViewController...]语句之前,但是模态视图甚至都没有出现。


为什么您不将TutorialViewController设为RootViewController呢? - Ulaş Sancak
7
因为我希望在用户完成教程后以模态方式关闭它(最后一个屏幕有一个“开始”按钮)。 - sonxurxo
这些答案中有没有帮助到您? - Beau Nouvelle
@Pandara的回答解决了主要问题(闪烁),但目前还没有模态VC的解决方案。 - sonxurxo
9个回答

35

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

    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
如果有人遇到了“不平衡的调用开始/结束外观转换”的问题,请查看Spoek的答案,使用DispatchQueue解决。 - Cœur
3
我认为Spoek改名为ullstrm,不管怎样,这就是Cœur所指的解决方案:https://dev59.com/D18d5IYBdhLWcg3w3FVk#41469734 - rob5408

13

Bruce在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
目前最佳解决方案。您甚至可以解释为什么需要执行 DispatchQueue.global().async 以解决“不平衡的调用开始/结束外观转换”的问题。 - Cœur
在某些设备上,我发现这似乎偶尔会导致启动画面无限挂起。还有其他人遇到这个问题吗? - shim
似乎是一个“试图呈现不在视图层次结构中的视图”的问题,我正在尝试通过在切换到主线程之前添加延迟来解决它,虽然我也可以更高级一些,设置等待直到主视图控制器完全加载。 - shim
我在 iOS 13 beta 6 上遇到了问题。我设置了 launch .modalPresentationStyle = .fullScreen,但是 window?.addSubview(launch.view) 导致呈现不是全屏,而是全屏和卡片呈现之间的奇怪混合。还有其他人在 iOS 13 上遇到这个问题吗? - Micky
好的,看起来在iOS 13上使用第二个启动VC实例并将其视图添加到窗口而不是同一实例可以解决问题。与此示例不同,我正在使用要呈现的vc的视图。 - Micky

8
也许你可以使用“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];

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


3
我只是在尝试这种方法。我不能接受这个答案是正确的,因为它没有使用模态表达,但我已经点了赞,谢谢! - sonxurxo
哈哈哈,你可以参考一些关于“childViewController”的文档,这是一种有效管理视图的方式。做一件事情并不只有一种正确的方法,对吧?;) - Pandara
对于任何在这里搜索解决方案的人来说,这可能不是正确的做法(IMHO)。但我仍然要使用这种方法,因为它可以解决根本问题 :) - sonxurxo
在我看来,这是正确的解决方案,因为它不能使用modalviewcontroller来完成 :-) - Carles Estevadeordal

7
这个问题在iOS 10中仍然存在。我的解决办法是:
  1. viewWillAppear 中将模态VC作为子VC添加到根VC中
  2. viewDidAppear 中:
    1. 从根VC中移除模态VC
    2. 无动画地以模态形式呈现子VC

代码:

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

        //....
    }
}

1
这是一个不错的替代方案。但由于我们正在处理创业问题,viewController不应该负责处理解决方法。这就是为什么我更喜欢Spoek的解决方案,它完全在AppDelegate中完成。 - Cœur
1
注意:Spoek的名称现在更改为ullstrm。 - Cœur
问题并不一定只在启动时发生;当视图控制器首次出现时,它随时可能想要呈现一个模态视图。这个解决方案解决了一般情况下的问题,我认为比篡改AppDelegate的启动序列更好。如果显示模态的决定由视图控制器做出,那么让AppDelegate参与其中是不合适的。 - devios1
1
@devios1 啊,我不知道这个问题在启动之外也会发生。我想我需要进行实验。但是我不能再给更多的积分了:我已经在两年前点赞了这个答案。 - Cœur

1

可能不是最好的解决方案,但你可以创建一个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;
}

0
Bruce的回答指引了我正确的方向,但由于我的模态框可能不仅在启动时出现(它是一个登录屏幕,所以如果他们退出登录,它需要出现),我不想将我的覆盖层直接绑定到视图控制器的呈现上。
这是我想出来的逻辑:
    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];
                     }];

在另一个部分,我执行呈现我的登录VC的逻辑,使用典型的self.window.rootViewController presentViewController:...格式,无论是应用程序启动还是其他情况都可以使用。
如果有人关心,这是我创建覆盖视图的方法:
@implementation WSILaunchImageView

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

这里是启动图像本身的逻辑:

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

为了完整起见,这里是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;
}

0

虽然可能有点晚,但在您的AppDelegate中,您可以这样做:

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

这仍然会导致在演示时出现一些闪烁。 - Micky

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

1
你能提供一些上下文吗? - Giulio Caccin

-1

这是我使用storyboards的方法,它适用于多个模态视图。 这个例子有3个。底部、中间和顶部。

只需确保在界面构建器中正确设置每个视图控制器的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;
}

谢谢,但是这种方式仍然会出现短暂的闪光。 - sonxurxo
使用这段代码,即使加载了3个控制器,我仍然看不到rootviewcontroller。虽然我使用的控制器没有太多内容。也许你正在尝试一次性加载太多东西?难道你看到的是启动屏幕吗? - Beau Nouvelle
无论我尝试在根视图控制器中加载什么,有时它会出现。而 有时 对我来说不是一个选项 :( - sonxurxo
你可能需要研究自定义转场。首先加载顶部视图控制器,然后使用自定义转场/segue创建类似于模态解除的动画。 - Beau Nouvelle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接