在UISplitViewController和其他视图控制器之间切换的最佳方法是什么?

时间:2010-11-18 09:03:09

标签: iphone ipad user-interface uisplitviewcontroller appstore-approval

我正在创作一款iPad应用。应用程序中的一个屏幕非常适合使用UISplitViewController。但是,应用程序的顶级是一个主菜单,我不想使用UISplitViewController。这提出了一个问题,因为Apple说:

  1. UISplitViewController应该是应用中的顶级视图控制器,即应将其视图添加为UIWindow

  2. 的子视图
  3. 如果使用,UISplitViewController应该在应用程序的生命周期中存在 - 即不要从UIWindow中移除其视图并将其他视图放在适当位置,反之亦然

  4. 阅读并进行了实验,似乎只有满足Apple要求的可行选择,而我们自己的选择是使用模态对话框。所以我们的应用程序在根级别有一个UISplitViewController(即它的视图被添加为UIWindow的子视图),为了显示我们的主菜单,我们将它作为全屏模式对话框推送到UISplitViewController上。然后通过关闭主菜单视图控制器模式对话框,我们实际上可以显示我们的拆分视图。

    这种策略似乎运作良好。但它引出了一些问题:

    1)有没有更好的方法来构建这个,没有模态,也满足提到的所有要求?由于被推送为模态对话框,主UI出现似乎有点奇怪。 (模态应该用于重点用户任务。)

    2)由于我的做法,我是否有遭受应用商店拒绝的风险?根据Apple的人机界面指南,这种模态策略可能是“误用”模态对话框。但是他们给了我什么其他选择呢?无论如何,他们会知道我这样做了吗?

9 个答案:

答案 0 :(得分:19)

我真的不相信在UISplitViewController (例如登录表单)之前显示一些UIViewController的概念变得如此复杂,直到我不得不创建那种查看hiearchy。

我的示例基于iOS 8和XCode 6.0(Swift),因此我不确定此问题是否以同样的方式存在,或者是由于iOS 8引入的一些新错误,但是我发现所有类似的问题,我没有看到完全“不是非常hacky”解决这个问题的方法。

在我最终找到解决方案之前,我将引导您完成我尝试过的一些事情(在本文末尾)。每个示例都基于在未启用CoreData的情况下从Master-Detail模板创建新项目。


首先尝试(模态segue到UISplitViewController):

  1. 创建新的UIViewController子类(例如LoginViewController)
  2. 在storyboard中添加新的视图控制器,将其设置为初始视图控制器(而不是UISplitViewController)并将其连接到LoginViewController
  3. 将UIButton添加到LoginViewController并从该按钮创建模态segue到UISplitViewController
  4. 将UISplitViewController的样板设置代码从AppDelegate的didFinishLaunchingWithOptions移动到LoginViewController的prepareForSegue
  5. 这几乎奏效了。我差点说,因为在使用LoginViewController启动应用程序并点击按钮并转到UISplitViewController后,会出现一个奇怪的错误:显示并隐藏主视图控制器的方向更改不再是动画。

    经过一段时间努力解决这个问题并且没有真正的解决方案,我认为它与某种方式连接的是奇怪的规则,UISplitViewController必须是rootViewController(在这种情况下它不是,LoginViewController是)所以我放弃了这个不太完美的解决方案。


    第二次尝试(来自UISplitViewController的模态segue):

    1. 创建新的UIViewController子类(例如LoginViewController)
    2. 在storyboard中添加新的视图控制器,并将其连接到LoginViewController(但这次将UISplitViewController保留为初始视图控制器)
    3. 从UISplitViewController创建模态segue到LoginViewController
    4. 将UIButton添加到LoginViewController并从该按钮创建展开segue
    5. 最后,在设置UISplitViewController的样板代码后,将此代码添加到AppDelegate的didFinishLaunchingWithOptions

      window?.makeKeyAndVisible()
      splitViewController.performSegueWithIdentifier("segueToLogin", sender: self)
      return true
      

      或尝试使用此代码:

      window?.makeKeyAndVisible()
      let loginViewController = splitViewController.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
      splitViewController.presentViewController(loginViewController, animated: false, completion: nil)
      return true
      

      这两个例子都产生了同样的坏事:

      1. 控制台输出:Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
      2. UISplitViewController必须首先显示在LoginViewController被模态化之前(我宁愿只提供登录表单,所以用户在登录前没有看到UISplitViewController)
      3. Unwind segue没有被调用(这完全是其他错误,我现在不会进入那个故事)

      4. 解决方案(更新rootViewController)

        我发现哪种方法正常工作的唯一方法是你动态更改窗口的rootViewController:

        1. 为LoginViewController和UISplitViewController定义Storyboard ID, 并向AppDelegate添加某种loggedIn属性。
        2. 基于此属性,实例化相应的视图控制器,然后将其设置为rootViewController。
        3. didFinishLaunchingWithOptions中没有动画但在从UI调用时动画。
        4. 以下是来自AppDelegate的示例代码:

          var loggedIn = false
          
          func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
              setupRootViewController(false)
              return true
          }
          
          func setupRootViewController(animated: Bool) {
              if let window = self.window {
                  var newRootViewController: UIViewController? = nil
                  var transition: UIViewAnimationOptions
          
                  // create and setup appropriate rootViewController
                  if !loggedIn {
                      let loginViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
                      newRootViewController = loginViewController
                      transition = .TransitionFlipFromLeft
          
                  } else {
                      let splitViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
                      let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
                      navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
                      splitViewController.delegate = self
          
                      let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
                      let controller = masterNavigationController.topViewController as MasterViewController
          
                      newRootViewController = splitViewController
                      transition = .TransitionFlipFromRight
                  }
          
                  // update app's rootViewController
                  if let rootVC = newRootViewController {
                      if animated {
                          UIView.transitionWithView(window, duration: 0.5, options: transition, animations: { () -> Void in
                              window.rootViewController = rootVC
                              }, completion: nil)
                      } else {
                          window.rootViewController = rootVC
                      }
                  }
              }
          }
          

          这是来自LoginViewController的示例代码:

          @IBAction func login(sender: UIButton) {
              let delegate = UIApplication.sharedApplication().delegate as AppDelegate
              delegate.loggedIn = true
              delegate.setupRootViewController(true)
          }
          

          我还想知道是否有更好/更清洁的方法可以在iOS 8中正常使用。

答案 1 :(得分:6)

TOUCHE!进入相同的问题并使用模态以相同的方式解决它。在我的例子中,它是一个登录视图,然后主菜单也显示在splitview之前。我使用了你想到的相同策略。我(以及我采访过的其他几位知识渊博的iOS人员)找不到更好的出路。对我来说很好。用户无论如何都不会注意到模态。这样介绍他们。是的,我也可以告诉你,有很多应用程序在App商店的引擎技巧下做同样的事情。 :)另一方面,请告诉我,如果你想办法在某种程度上找到更好的方法:)

答案 2 :(得分:3)

谁说你只能有一个窗口? :)

查看我的回答on this similar question是否有帮助。

这种方法对我来说非常有效。只要您不必担心多个显示或状态恢复,此链接代码应足以满足您的需求:您不必使逻辑向后看或重写现有代码,并且仍然可以利用UISplitView在您的应用程序中更深层次 - 没有(AFAIK)打破Apple指南。

答案 3 :(得分:1)

对于未来的iOS开发人员遇到同样的问题:这是另一个答案和解释。你必须使它成为根视图控制器。如果不是,请覆盖模态。

UISplitviewcontroller not as a rootview controller

答案 4 :(得分:1)

刚刚在项目上遇到这个问题并且认为我要分享我的解决方案。在我们的案例中(对于iPad),我们希望以UISplitViewController开头,两个视图控制器都可见(使用preferredDisplayMode = .allVisible)。在细节(右)层次结构中的某个时刻(我们也有这个侧面的导航控制器)我们想在整个拆分视图控制器上推送一个新的视图控制器(不使用模态转换)。

在iPhone上,此行为是免费的 - 因为任何时候只能看到一个视图控制器。但是在iPad上,我们不得不想出其他的东西。我们最终选择了一个根容器视图控制器,它将拆分视图控制器作为子视图控制器添加到它。此根视图控制器嵌入在导航控制器中。当拆分视图控制器中的详细视图控制器想要在整个拆分视图控制器上推送新控制器时,根视图控制器会使用其导航控制器推送此新视图控制器。

答案 5 :(得分:0)

我想通过-presentViewController:animated:completion:(我们都知道虽然不会工作)来贡献我的方法来呈现UISplitViewController。 我创建了一个UISplitViewController子类,它响应:

-presentAsRootViewController
-returnToPreviousViewController

该类与其他成功的方法一样,将UISplitViewController设置为窗口的rootViewController,但使用与-presentViewController:animated:completion:

获得的动画类似的动画(默认情况下)

PresentableSplitViewController.h

#import <UIKit/UIKit.h>    
@interface PresentableSplitViewController : UISplitViewController    
- (void) presentAsRootViewController;
@end

PresentableSplitViewController.m

#import "PresentableSplitViewController.h"

@interface PresentableSplitViewController ()
@property (nonatomic, strong) UIViewController *previousViewController;
@end

@implementation PresentableSplitViewController

- (void) presentAsRootViewController {

    UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
    _previousViewController=window.rootViewController;

    UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
    window.rootViewController = self;

    [window insertSubview:windowSnapShot atIndex:0];

    CGRect dstFrame=self.view.frame;

    CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
    offset.width*=self.view.frame.size.width;
    offset.height*=self.view.frame.size.height;
    self.view.frame=CGRectOffset(self.view.frame, offset.width, offset.height);

    [UIView animateWithDuration:0.5
                          delay:0.0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         self.view.frame=dstFrame;
                     } completion:^(BOOL finished) {
                         [windowSnapShot removeFromSuperview];
                     }];
}

- (void) returnToPreviousViewController {
    if(_previousViewController) {

        UIWindow *window=[[[UIApplication sharedApplication] delegate] window];

        UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
        window.rootViewController = _previousViewController;

        [window addSubview:windowSnapShot];

        CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
        offset.width*=windowSnapShot.frame.size.width;
        offset.height*=windowSnapShot.frame.size.height;

        CGRect dstFrame=CGRectOffset(windowSnapShot.frame, offset.width, offset.height);

        [UIView animateWithDuration:0.5
                              delay:0.0
             usingSpringWithDamping:1.0
              initialSpringVelocity:0.0
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             windowSnapShot.frame=dstFrame;
                         } completion:^(BOOL finished) {
                             [windowSnapShot removeFromSuperview];
                             _previousViewController=nil;
                         }];
    }
}

@end

答案 6 :(得分:0)

我做了一个UISplitView作为初始视图,而不是模态化为全屏UIView并返回到UISplitView。如果您需要返回SplitView,则必须使用自定义segue。

阅读此链接(从日语翻译)

UIViewController to UISplitViewController

答案 7 :(得分:0)

添加到@tadija的答案我处于类似情况:

我的应用只适用于手机,我正在添加平板电脑用户界面。我决定在同一个应用程序中使用Swift进行操作 - 并最终将所有应用程序迁移到使用相同的故事板(当我觉得IPad版本稳定时,将它用于手机应该对来自XCode6的新类来说是微不足道的。)

我的场景中尚未定义任何segue,但仍然有效。

我的app委托中的代码是在ObjectiveC中,并且略有不同 - 但使用相同的想法。 请注意,我使用场景中的默认视图控制器,与之前的示例不同。我觉得这也适用于IOS7 / IPhone,其中运行时将生成常规UINavigationController而不是UISplitViewController。我甚至可以添加新的代码来推送IPhones上的登录视图控制器,而不是更改rootVC。

- (void) setupRootViewController:(BOOL) animated {
    UIViewController *newController = nil;
    UIStoryboard *board = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewAnimationOptions transition = UIViewAnimationOptionTransitionCrossDissolve;

    if (!loggedIn) {
        newController = [board instantiateViewControllerWithIdentifier:@"LoginViewController"];
    } else {
        newController = [board instantiateInitialViewController];
    }

    if (animated) {
        [UIView transitionWithView: self.window duration:0.5 options:transition animations:^{
            self.window.rootViewController = newController;
            NSLog(@"setup root view controller animated");
        } completion:^(BOOL finished) {
            NSLog(@"setup root view controller finished");
        }];
    } else {
        self.window.rootViewController = newController;
    }
}

答案 8 :(得分:0)

另一个选项:在详细信息视图控制器中,我显示模态视图控制器:

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if (!appDelegate.loggedIn) {
    // display the login form
    let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
    let login = storyboard.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
    self.presentViewController(login, animated: false, completion: { () -> Void in
       // user logged in and is valid now
       self.updateDisplay()
    })
} else {
    updateDisplay()
}

请勿在不设置登录标志的情况下关闭登录控制器。请注意,在IPhones中,主视图控制器将首先出现,因此需要在主视图控制器上使用非常相似的代码。