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

29

我正在开发一个iPad应用程序。应用程序中的一个屏幕非常适合使用UISplitViewController。然而,应用程序的顶级是一个主菜单,我不想使用UISplitViewController。这带来了一个问题,因为苹果公司规定:

  1. UISplitViewController 应该是应用程序中最高级别的视图控制器,即将其视图添加为 UIWindow 的子视图。

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

经过阅读和实验,似乎满足苹果公司的要求和我们自己的要求的唯一可行选项是使用模态对话框。 因此,我们的应用程序在根级别上具有一个UISplitViewController(即将其视图添加为 UIWindow 的子视图),并且要显示我们的主菜单,我们将其作为全屏模式对话框推入到 UISplitViewController 上。然后通过关闭主菜单视图控制器模态对话框,我们可以实际显示分割视图。

这种策略似乎很好用。但这引出了以下问题:

1)有没有更好的结构方式,而无需使用模态对话框,同时满足所有要求? 将主UI作为模态对话框推入似乎有点奇怪。(模态对话框应该是针对关注用户任务的。)

2)我的方法是否有因此被App Store拒绝的风险? 这种模态策略可能会“错误地”使用模态对话框,符合苹果的人机界面指南。 但是他们给我什么其他选择? 无论如何,他们会知道我在这样做吗?


如何将菜单视图作为全屏模态对话框推送到UISplitViewController上?我也遇到了同样的问题,我在故事板中从分割视图到菜单视图定义了一个模态转场,然后在我的splitviewcontroller代码中,在viewDidApear中使用performSegueWithIdentifier,但是这种方式会让用户在菜单模态之前看到分割视图的一瞥。这个问题能解决吗?我应该在哪里调用performseguewithidentifier来防止这个问题? - Abbas Mousavi
9个回答

19

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

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

在本文末尾提供解决方案之前,我将指导您通过我尝试过的一些事情。每个示例都基于创建未启用CoreData的主细节模板的新项目。


第一次尝试(modal segue到UISplitViewController):

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

这几乎起作用了。我说几乎,因为当使用LoginViewController启动应用程序并点击按钮并segue到UISplitViewController时,会出现奇怪的错误:在方向更改时显示和隐藏主视图控制器不再有动画效果

在与这个问题斗争一段时间并没有找到真正的解决方法后,我认为它与那个奇怪的规则相关,即UISplitViewController必须是rootViewController(但在这种情况下它不是,LoginViewController是),因此我放弃了这个不太完美的解决方案。


第二次尝试(从UISplitViewController进行modal segue):

  1. 创建新的UIViewController子类(例如LoginViewController)
  2. 在故事板中添加新的视图控制器,并将其连接到LoginViewController(但这次保留UISplitViewController作为初始视图控制器)
  3. 从UISplitViewController创建模态segue到LoginViewController
  4. 在LoginViewController中添加UIButton并创建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. 退回跳转未被调用(这是完全不同的错误,我现在不想深入讨论)

解决方案(更新 rootViewController)

我找到的唯一有效的方法是在运行时更改窗口的 rootViewController:

  1. 为 LoginViewController 和 UISplitViewController 定义故事板 ID,并向 AppDelegate 添加某种 loggedIn 属性。
  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为我存储布尔值。就这个解决方案而言,我希望我能够有多于一个点赞的选择。这拯救了我的生命。非常感谢你!! - Louie Bertoncin
这个工作得很好,但是每次在这些之间切换时都会填满内存...有什么帮助解决这个问题的方法吗?我需要更频繁地更改rootViewController,而不仅仅是在登录时。 - Heckscheibe
这很棒,对我有用。然而,我最初没有使用登录屏幕 - 只是一个带有一个按钮的“主页”屏幕,该按钮将带我到SplitVC和另一个按钮将带我到另一个VC。我的问题是,当我从“主页” VC 到 SplitVC 时,如何返回“主页” VC?谢谢。 - Tony
@Tony 抱歉,我不明白你遇到了什么问题,你的问题可能需要更多的上下文信息。 - tadija
@tadija - 是的,对不起。我按照上面的步骤操作后,现在可以用新的“主页”(HomeVC)替换我的根视图控制器了。这个页面有两个按钮。一个按钮指向(原始的)splitVC。所以当我在SplitVC屏幕中时,我需要能够返回到HomeVC,以允许用户选择第二个按钮,该按钮指向另一个VC。 - Tony
显示剩余2条评论

6

太棒了!我也遇到了同样的问题,并使用模态框以相同的方式解决了它。在我的情况下,登录视图和主菜单都需要在分割视图之前显示。我采用了与您思考的相同策略。我和其他一些了解iOS的专业人士都无法找到更好的方法。对我来说,效果很好。用户从未注意到模态框。表现得像这样。是的,我也可以告诉您,在App Store上有相当多的应用程序在幕后进行同样的技巧。 :) 另外,如果您想出更好的解决方案,请务必让我知道。


谢谢 Bourne!我们还有一个登录界面在其他界面之上,但为了简洁起见,我没有提到它。我仍然很惊讶苹果对 UISplitViewController(等等)施加了所有这些限制,然后完全没有告诉你如何绕过这些限制,例如“使用模态”。我认为苹果文档需要更多(或任何)高级 UI 设计思路/模式。 - occulus
我认为你们回答了我的问题:这是不可能的。请参见 https://dev59.com/fFLTa4cB1Zd3GeqPdcZb。 - Krumelur

3

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

看看我的答案在这个类似的问题上是否有帮助。

这种方法对我来说非常有效。只要您不必担心多个显示器或状态恢复,此链接代码就足以完成您所需的操作:您不必使逻辑向后看或重写现有代码,并且仍然可以在应用程序中更深层次地利用UISplitView - 无需(据我所知)违反Apple指南。


1

好的,您可以编写自己的分屏视图控制器。每当我需要时,这就是我所做的。 - Bourne

1
我在一个项目中遇到了这个问题,想分享一下我的解决方案。在我们的情况下(用于iPad),我们希望从一个UISplitViewController开始,同时显示两个视图控制器(使用preferredDisplayMode = .allVisible)。在详细(右侧)层次结构的某个点上(我们也为此侧使用了导航控制器),我们想要推送一个新的视图控制器覆盖整个分割视图控制器(而不是使用模态转换)。
在iPhone上,这种行为是免费的 - 因为任何时候只有一个视图控制器可见。但在iPad上,我们不得不想出其他办法。我们最终选择了一个根容器视图控制器,将分割视图控制器作为子视图控制器添加到其中。这个根视图控制器嵌入在一个导航控制器中。当分割视图控制器中的详细视图控制器想要推送一个新的控制器覆盖整个分割视图控制器时,根视图控制器就会使用它的导航控制器推送这个新的视图控制器。

这难道不违反了 UISplitViewController 必须是根视图控制器而不能嵌入任何容器的规则吗? - Sebastian Dwornik
2
“...分割视图控制器通常是您的应用程序窗口的根视图控制器,但它也可以嵌入到另一个视图控制器中。” - Evan R

0

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

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

UIViewController to UISplitViewController


0

补充@tadija的答案,我也遇到了类似的情况:

我的应用程序只适用于手机,现在我正在添加平板电脑UI。我决定在同一个应用程序中使用Swift来完成这个任务,并最终将所有应用程序迁移到使用相同的storyboard(当我感觉iPad版本稳定时,使用新的XCode6类应该很容易适用于手机)。

我的场景中尚未定义任何segue,但它仍然可以工作。

我的应用程序委托中的代码是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;
    }
}

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

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

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Jason G
这段代码应该放在初始控制器中(登录后)。缺失的函数是填充显示控制器中详细信息的函数。 - elcuco

0

我想分享一下我的方法来展示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

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