在UISplitViewController和其他视图控制器之间切换的最佳方式?

问题描述:

我正在创作一个iPad应用程序。该应用程序中的一个屏幕非常适合使用UISplitViewController。但是,应用程序的顶层是一个主菜单,我不想使用UISplitViewController。这就提出了一个问题,因为苹果的状态:如果使用在UISplitViewController和其他视图控制器之间切换的最佳方式?

  1. UISplitViewController应在应用程序中的顶级视图控制器,即它认为应增加为UIWindow

  2. 子视图,UISplitViewController应在那里为应用程序的生命周期 - 即不要从一个UIWindow其观点,并把另一到位,反之亦然

看了四周,试验,它似乎唯一可行的选择以满足苹果的要求,我们自己的是使用模态对话框。因此,我们的应用程序在根级别有一个UISplitViewController(即它的视图作为UIWindow的子视图添加),并显示我们的主菜单,我们将它作为全屏模式对话框推送到UISplitViewController。然后通过关闭主菜单视图控制器模式对话框,我们可以实际显示我们的分割视图。

这一战略似乎很好地工作。但它引出了一个问题:

1)是否有这种构造的没有更好的办法,没有情态动词,也符合所有要求提及?由于被推送为模态对话框,主UI显得有些奇怪。 (情态动词应该是对重点用户的任务。)

2)我是在应用程序商店拒绝的风险,因为我的方法呢?按照苹果的人机界面准则,这种模式策略可能是“滥用”模式对话。但他们给了我什么其他选择?无论如何,他们知道我在做这个吗?

+0

如何将菜单视图作为全屏模式对话框推送到UISplitViewController上?我有同样的问题,我从故事板中的分割视图菜单视图中定义模式segue,然后在我的splitviewcontroller代码中使用viewDidApear中的performSegueWithIdentifier:但这种方式用户总是在菜单模式之前​​看到分割视图的一瞥?这个问题能解决吗?我应该在哪里调用performseguewithidentifier来防止这个问题? – 2012-02-20 16:19:30

谢谢!同样的问题,并用同样的方式解决它使用模态。在我的情况下,它是一个登录视图,然后是主菜单以及在分割视图之前显示。我使用了和你想象的一样的策略。我(以及其他几位知识渊博的iOS人士)找不到更好的解决方案。对我来说工作得很好。无论如何,用户永远不会注意到模态。目前他们如此。是的,我也可以告诉你,有相当多的应用程序在应用程序商店的引擎下做同样的事情。 :)在另一方面,不要让我知道,如果你图个更好的出路不知何故好歹某个时候:)

+0

谢谢Bourne!我们还有其他的登录屏幕,但为了简洁起见,我将其留下了。我仍然很惊讶苹果把所有这些限制放在了UISplitViewController(其中的东西),然后完全没有告诉你如何避开这些限制,例如'使用模态'。我认为苹果文档需要更多(任何?)高级UI设计理念/模式。 – occulus 2010-11-18 09:51:43

+0

我想你们回答了我的问题:这是不可能的。见http://stackoverflow.com/questions/4482526/stick-uisplitviewcontroller-in-its-own-xib – Krumelur 2010-12-19 16:31:59

对于运行到同样的问题,未来的iOS开发:这里的另一个答案和解释。你必须让它成为根视图控制器。如果不是,则覆盖一个模式。

UISplitviewcontroller not as a rootview controller

+0

那么你可以编写你自己的splitview控制器。每当我需要时,我都会这样做。 – Bourne 2010-12-20 07:03:26

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

看看我的回答on this similar question可以提供帮助。

这种方法的工作对我非常好。只要您不必担心多个显示或状态恢复,此链接的代码应该足以满足您的需要:您不必让逻辑看起来倒退或重写现有代码,并且仍然可以充分利用在您的应用程序中更深层次的UISplitView - 没有(AFAIK)违反Apple准则。

我严重不相信有一些的UIViewController这个概念展现UISplitViewController (例如登录表单)原来是这么复杂,直到我不得不创建一个样的观点hiearchy之前。我的例子基于iOS 8和XCode 6.0(Swift),所以我不确定这个问题是否以同样的方式存在,或者是由于iOS 8引入了一些新的错误,但是从所有我发现的类似问题,我没有看到这个问题的完整'不是很hacky'的解决方案。

我会指导您完成一些我在结束解决方案之前尝试过的一些事情(在本文结尾处)。每个示例均基于在未启用CoreData的情况下从Master-Detail模板创建新项目。


第一次尝试(模态SEGUE到UISplitViewController):

  1. 创建新UIViewController子类(LoginViewController例如)
  2. 在故事板中添加新的视图控制器,其设置为初始视图控制器(代替UISplitViewController的),并把它连接到LoginViewController
  3. 添加的UIButton到LoginViewController,并从该按钮创建模态顺着接下去UISplitViewController
  4. 为UISplitViewController
  5. 移动样板设置代码从AppDelegate中的didFinishLaunchingWithOptions到LoginViewController的prepareForSegue

这几乎工作。我差不多说了,因为在应用程序启动后使用LoginViewController并点击按钮并继续使用UISplitViewController,出现了一个奇怪的错误:显示和隐藏主视图控制器在方向更改不再动画。

后,这个问题并没有真正解决一段时间挣扎,我认为它以某种方式与奇怪的规则是UISplitViewController必须是RootViewController的连接(在这种情况下它是不是,LoginViewController是),所以我给了从这个不完美的解决方案。


第二个尝试(从UISplitViewController模式赛格瑞):

  1. 创造新的UIViewController子类(LoginViewController例如)
  2. 在故事板添加新的视图控制器,并将其连接到LoginViewController(但此时让UISplitViewController成为初始视图控制器)
  3. 从UISplitViewController创建模态segue到LoginViewController
  4. add UIB utton到LoginViewController,并从该按钮创建开卷赛格瑞

最后,添加以下代码到AppDelegate中的didFinishLaunchingWithOptions样板代码后设立UISplitViewController:

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有模式地segued之前(我宁愿只存在登录表单,因此用户不会看到UISplitViewController登录之前)
  3. 开卷赛格瑞没有得到所谓的(这完全是其他错误,我不会进入那个故事现在)

解决方案(更新RootViewController的)

我发现的唯一途径,其正常工作是如果你改变窗口RootViewController的对飞:

  1. 定义故事板ID为LoginViewController和UISplitViewController, 并添加某种的loggedIn属性到AppDelegate中。
  2. 基于此属性,实例化适当的视图控制器,然后将其设置为rootViewController。
  3. didFinishLaunchingWithOptions中没有动画的情况下执行动画,但在从UI调用时动画。

下面是从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中正常工作。

+0

我已经得到了这个工作,感谢代码,但是如果已经登录,它仍然显示登录(带有注销按钮)。当我第一次打开应用程序并且您已经登录时,我该如何跳过? – 2014-12-30 22:14:22

+0

@naturalc我正在使用NSUserDefaults为我存储一个布尔值。就这个解决方案而言,我希望我可以选择提供多个upvote。这拯救了我的生命。非常感谢!! – 2015-01-08 17:20:13

+0

这很好,但它填满了这些内存之间的每一次切换......对于这个问题的任何帮助?我需要更改rootViewController更经常比只当登录... – Heckscheibe 2015-08-13 08:14:28

我想提出我的方法来呈现一个UISplitViewController,你可能想通过-presentViewController:animated:completion:(我们都知道这将不起作用)。 我创建了一个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 

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

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

UIViewController to UISplitViewController

添加到@tadija的答案我在一个类似的情况:

我的应用是仅用于手机,和我添加平板电脑UI。我决定在同一个应用程序中使用Swift,并最终将所有应用程序迁移到使用相同的故事板(当我觉得IPad版本稳定时,使用XCode6的新类对于手机来说应该是微不足道的)。

在我的场景中没有定义任何赛段,它仍然有效。

我的应用程序委托中的代码位于ObjectiveC中,并略有不同 - 但使用了相同的想法。 请注意,与以前的示例不同,我使用场景中的默认视图控制器。我觉得这也适用于IOS7/IPhone,其中运行时将生成一个普通的UINavigationController而不是UISplitViewController。我甚至可能会添加新的代码来推动IPhone上的登录视图控制器,而不是改变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; 
    } 
} 

另一种选择:在详细视图控制器I显示模态视图控制器:

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中,主视图控制器将首先出现,因此需要在主视图控制器上使用非常类似的代码。

+0

你在文件中的哪个位置放置了这个?在配置视图中?在视图中加载?别的地方? - 我把它放在viewdidload中,它说detailviewcontroller没有名为updateDisplay的成员 – 2014-12-30 21:44:19

+0

此代码属于初始控制器(登录后)。缺少的功能是填充显示控制器中的细节。 – elcuco 2015-01-04 07:49:40

刚刚遇到这个问题的项目,并认为我会分享我的解决方案。在我们的情况下(对于iPad),我们想从UISplitViewController开始,并且两个视图控制器都可见(使用preferredDisplayMode = .allVisible)。在详细(右)层次结构中的某个点(我们也有一个用于这一边的导航控制器),我们想要在整个分割视图控制器上推送一个新的视图控制器(不使用模态转换)。

在iPhone上,这种行为是免费的 - 因为任何时候只有一个视图控制器可见。但在iPad上我们不得不找出其他的东西。我们结束了一个根容器视图控制器,它将分割视图控制器作为子视图控制器添加到它。此根视图控制器嵌入在导航控制器中。当分割视图控制器中的详细视图控制器想要在整个分割视图控制器上推送新控制器时,根视图控制器将其新的视图控制器与其导航控制器一起推送。