【问题标题】:iOS Present modal view controller on startup without flashiOS在启动时呈现模态视图控制器而不使用flash
【发布时间】:2014-10-14 08:16:16
【问题描述】:

我想在首次启动时向用户展示一个教程向导。

有没有办法在应用程序启动时呈现模态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?
  • @UlasSancak 因为我希望在用户完成教程后以模态方式将其关闭(最后一个屏幕有一个Let's Start 按钮)。
  • 这些答案有帮助吗?
  • @Pandara 的回答解决了主要问题(闪烁),但目前尚无模态 VC 的解决方案

标签: ios objective-c startup modalviewcontroller


【解决方案1】:

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

【讨论】:

  • 如果有人面临开始/结束外观转换的不平衡调用,请参阅 Spoek 答案以获取使用DispatchQueue 的解决方案。
  • 我认为 Spoek 把他的名字改成了 ullstrm,反正这是解决方案 Cœur 指的是:stackoverflow.com/a/41469734/84783
【解决方案2】:

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

【讨论】:

  • 迄今为止最好的解决方案。您甚至可以添加解释为什么必须使用DispatchQueue.global().async 来解决开始/结束外观转换的不平衡调用
  • 在某些设备上,这似乎偶尔会导致我在启动屏幕上无休止地挂起。其他人有这个问题吗?
  • 似乎是一个“试图在视图上呈现不在视图层次结构中的视图”问题,我试图通过在切换到主线程时添加延迟来解决这个问题,尽管我可以更漂亮,并将其设置为等到主视图控制器完全加载。
  • 我在 iOS 13 beta 6 上遇到了这个问题。我设置了launch .modalPresentationStyle = .fullScreen,但window?.addSubview(launch.view) 导致演示不是全屏,而是全屏和卡片演示之间出现了一些奇怪的混合.还有其他人在 iOS 13 上遇到问题吗?
  • 好的,似乎使用启动 VC 的第二个实例并将那个视图添加到窗口而不是同一个实例修复了 iOS 13 上的问题。与此示例不同,我使用的是视图要提交的 vc 的数量。
【解决方案3】:

也许你可以使用“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 属性在视图上添加一些动画。希望有帮助:)

【讨论】:

  • 我只是在尝试这种方法。我不能接受这是正确的答案,因为它不使用模态演示,但我赞成它,谢谢!
  • 哈哈哈,你可以参考一些关于“childViewController”的文档,这是一种管理视图的有效方法。做一件事没有唯一正确的方法,对吧?;)
  • 是的,但是对于在这里寻找解决方案的人来说,恕我直言,这不是正确的。无论如何我都会使用这种方法,因为它解决了根本问题:)
  • 在我看来这是正确的解决方案,因为它不能使用 modalviewcontroller :-)
【解决方案4】:

这个问题在 iOS 10 中仍然存在。我的解决方法是:

  1. viewWillAppear 中将模态VC 作为childVC 添加到rootVC
  2. viewDidAppear
    1. 将 modalVC 作为 rootVC 的子项移除
    2. 以模态方式呈现不带动画的 childVC

代码:

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

        //....
    }
}

【讨论】:

  • 这是一个不错的替代解决方案。但是由于我们正在处理启动问题,因此处理变通方法不应该是 viewController 的责任。这就是为什么我更喜欢 Spoek 的解决方案,它完全在 AppDelegate 中完成。
  • 注意:Spoek 名称现在是 ullstrm
  • @Cœur 这个问题不一定只发生在启动时;当视图控制器第一次出现时,它可能会在视图控制器想要在其自身之上呈现模态视图时发生。该解决方案解决了一般情况,并且比破解 AppDelegate 的启动序列更好。如果显示模式的决定是由视图控制器做出的,那么 AppDelegate 与它有任何关系是不合适的。
  • @devios1 啊,我不知道这个问题可能发生在启动之外。我想我需要尝试一下。但我不能给出更多的分数:两年前我已经对这个答案投了赞成票。
【解决方案5】:

可能是一个糟糕的解决方案,但您可以制作一个包含 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;
}

【讨论】:

    【解决方案6】:

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

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

    如果有人关心,我是这样创建叠加视图的:

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

    Aaaa 只是为了完成,下面是那些 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;
    }
    

    【讨论】:

      【解决方案7】:
      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】:

      可能会迟到,但在您的 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;
      }
      

      【讨论】:

      • 这仍然会在演示发生时导致一些闪烁。
      【解决方案9】:

      这就是我使用故事板的方式,它适用于多种模式。 此示例有 3 个。底部、中间和顶部。

      请确保在界面生成器中正确设置每个 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;
      }
      

      【讨论】:

      • 谢谢,不过这样短闪还是会出现
      • 我在这段代码中根本看不到 rootviewcontroller,即使加载了 3 个控制器也是如此。尽管我使用的控制器并没有太多的功能。也许您一次尝试加载太多?会不会是您看到的启动屏幕?
      • 无论我尝试在根视图控制器中加载什么,有时都会出现。 sometimes 不适合我:(
      • 您可能需要查看自定义过渡。首先加载顶视图控制器,并使用自定义过渡/转场创建类似于模态解雇的动画。
      猜你喜欢
      • 1970-01-01
      • 2016-02-03
      • 1970-01-01
      • 2013-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多