29

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

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

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

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

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

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

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

4

9 回答 9

19

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

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

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


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

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

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

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


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

  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 添加某种类型的 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 中正常工作。

于 2014-09-22T17:36:49.637 回答
6

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

于 2010-11-18T09:17:51.350 回答
3

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

看看我对这个类似问题的回答是否有帮助。

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

于 2013-11-14T16:49:49.960 回答
1

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

UISplitviewcontroller 不作为 rootview 控制器

于 2010-12-19T16:49:33.970 回答
1

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

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

于 2018-03-08T13:50:50.750 回答
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
于 2014-10-30T18:29:23.527 回答
0

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

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

UIViewController 到 UISplitViewController

于 2014-11-12T17:32:04.253 回答
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 中,主视图控制器将首先出现,因此主视图控制器上需要非常相似的代码。

于 2014-12-22T14:19:31.493 回答
0

除了@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;
    }
}
于 2014-12-22T13:44:33.537 回答