【问题标题】:Best way to switch between UISplitViewController and other view controllers?在 UISplitViewController 和其他视图控制器之间切换的最佳方式?
【发布时间】:2010-11-18 09:03:08
【问题描述】:

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

  1. UISplitViewController 应该是应用程序中的顶级视图控制器,即它的视图应该添加为UIWindow 的子视图

  2. 如果使用,UISplitViewController 应该在应用程序的整个生命周期内都存在 - 即不要从 UIWindow 中删除其视图并放置另一个视图,反之亦然

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

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

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

2) 我是否会因为我的方法而面临应用商店拒绝的风险?根据 Apple 的人机界面指南,这种模式策略可能是“滥用”模式对话框。但他们给了我什么其他选择?无论如何,他们会知道我正在这样做吗?

【问题讨论】:

  • 如何将菜单视图作为全屏模式对话框推送到 UISplitViewController?我有同样的问题,我在情节提要中定义了从拆分视图到菜单视图的模态segue,然后在我的splitviewcontroller代码中使用viewDidApear中的performSegueWithIdentifier:但是这样用户总是在菜单模态之前看到拆分视图的一瞥?这个问题可以解决吗?我应该在哪里调用 performeguewithidentifier 来防止这个问题?

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


【解决方案1】:

我真的不相信在 UISplitViewController (例如登录表单) 之前显示一些 UIViewController 的概念竟然如此复杂,直到我不得不创建那种视图层次结构。

我的示例基于 iOS 8 和 XCode 6.0 (Swift),所以我不确定这个问题之前是否以相同的方式存在,或者是由于 iOS 8 引入了一些新错误,但来自我发现的所有类似问题,我都没有看到这个问题的完整“不是很hacky”的解决方案。

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


第一次尝试(模式 segue 到 UISplitViewController):

  1. 创建新的 UIViewController 子类(例如 LoginViewController)
  2. 在情节提要中添加新的视图控制器,将其设置为初始视图控制器(而不是 UISplitViewController)并将其连接到 LoginViewController
  3. 将 UIButton 添加到 LoginViewController 并从该按钮创建模态 segue 到 UISplitViewController
  4. 将 UISplitViewController 的样板设置代码从 AppDelegate 的 didFinishLaunchingWithOptions 移动到 LoginViewController 的 prepareForSegue

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

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


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

  1. 创建新的 UIViewController 子类(例如 LoginViewController)
  2. 在情节提要中添加新的视图控制器,并将其连接到 LoginViewController(但这次将 UISplitViewController 保留为初始视图控制器)
  3. 创建从 UISplitViewController 到 LoginViewController 的模态序列
  4. 将 UIButton 添加到 LoginViewController 并从该按钮创建 unwind segue

最后,将此代码添加到 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 以模态方式连接之前首先显示(我宁愿只显示登录表单,这样用户在登录前看不到 UISplitViewController)
  3. Unwind segue 不会被调用(这完全是另一个错误,我现在不打算讨论这个故事)

解决方案(更新 rootViewController)

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

  1. 为 LoginViewController 和 UISplitViewController 定义 Storyboard ID, 并向 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 中正常工作。

【讨论】:

  • 我已经完成了这项工作,感谢代码,但如果已经登录,它仍然显示登录(带有注销按钮)。我如何让它在你第一次打开应用程序时跳过它你已经登录了吗?
  • @naturalc 我正在使用 NSUserDefaults 为我存储一个布尔值。就这个解决方案而言,我希望我可以选择给予多个支持。这救了我的命。非常感谢!!
  • 这很好用,但是每次切换它们都会填满内存......对这个问题有什么帮助吗?我需要更频繁地更改 rootViewController 而不是仅在登录时...
  • 这很棒,对我有用。但是,我最初并没有使用登录屏幕 - 只是“主”屏幕,带有一个将我带到 SplitVC 的按钮和另一个将我带到另一个 VC 的按钮。我的问题是,当我从“家”VC 转到 SplitVC 时,我怎样才能回到“家”VC?谢谢。
  • @Tony 抱歉,我不明白您有什么问题,您的问题可能需要更多上下文。
【解决方案2】:

触摸!遇到同样的问题并使用模态以同样的方式解决它。在我的情况下,它是一个登录视图,然后主菜单也将显示在拆分视图之前。我使用了与您想出的相同的策略。我(以及与我交谈过的其他几位知识渊博的 iOS 人员)找不到更好的出路。对我来说很好。无论如何,用户永远不会注意到模态。如此呈现它们。是的,我还可以告诉你,在 App Store 上有不少应用程序在做同样的幕后花招。 :) 另一方面,如果你想出一个更好的出路,一定要告诉我:)

【讨论】:

  • 谢谢伯恩!我们还有一个登录屏幕,但为了简洁起见,我把它省略了。我仍然很惊讶 Apple 将所有这些限制都放在 UISplitViewController 上(除此之外),然后完全没有告诉你如何绕过这些限制,例如'使用模态'。我认为 Apple 文档需要更多(任何?)高级 UI 设计理念/模式。
  • 我想你们回答了我的问题:这是不可能的。见stackoverflow.com/questions/4482526/…
【解决方案3】:

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

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

这种方法对我来说效果很好。只要您不必担心多重显示或状态恢复,这个链接代码应该足以满足您的需求:您不必让逻辑向后看或重写现有代码,仍然可以利用在您的应用程序中更深层次的 UISplitView - 没有(AFAIK)违反 Apple 准则。

【讨论】:

    【解决方案4】:

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

    UISplitviewcontroller not as a rootview controller

    【讨论】:

    • 你可以编写自己的拆分视图控制器。这就是我在需要时做的事情。
    【解决方案5】:

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

    在 iPhone 上,这种行为是免费的——因为任何时候都只有一个视图控制器可见。但在 iPad 上,我们必须想出别的办法。我们最终使用了一个根容器视图控制器,它将拆分视图控制器作为子视图控制器添加到它。此根视图控制器嵌入在导航控制器中。当拆分视图控制器中的详细视图控制器想要将新控制器推送到整个拆分视图控制器上时,根视图控制器会推送此新视图控制器及其导航控制器。

    【讨论】:

    • 这不违反UISplitViewController “需要成为根视图控制器” 并且不嵌入任何容器的规则吗?
    • 取自developer.apple.com/documentation/uikit/uisplitviewcontroller:“...拆分视图控制器通常是应用程序窗口的根视图控制器,但它可能嵌入到另一个视图控制器中。”
    【解决方案6】:

    我想通过-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
    

    【讨论】:

      【解决方案7】:

      我做了一个 UISplitView 作为初始视图,然后它以模态方式进入全屏 UIView 并返回到 UISplitView。如果您需要返回到 SplitView,您必须使用自定义 segue。

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

      UIViewController to UISplitViewController

      【讨论】:

        【解决方案8】:

        除了@tadija 的回答,我也有类似的情况:

        我的应用仅适用于手机,我正在添加平板电脑用户界面。我决定在同一个应用程序中使用 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;
            }
        }
        

        【讨论】:

          【解决方案9】:

          另一个选项:在细节视图控制器中,我显示一个模式视图控制器:

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

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

          【讨论】:

          • 您实际上将它放在文件的哪个位置?在配置视图中?在视图中加载了吗?别的地方? - 我把它放在 viewdidload 中,它说 detailviewcontroller 没有名为 updateDisplay 的成员
          • 这段代码属于初始控制器(登录后)。缺少的函数是在显示控制器中填充细节的函数。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-05-09
          • 2010-10-29
          • 1970-01-01
          • 1970-01-01
          • 2019-04-10
          相关资源
          最近更新 更多