iPhone竖屏下UISplitViewController显示详细视图控制器而不是主视图控制器

187
我正在使用 Xcode 6 中的通用故事板,目标是 iOS 7 及以上版本。我已经实现了一个 UISplitViewController,在运行 iOS 8 的 iPhone 上原生支持,并且 Xcode 会自动将其后移至 iOS 7。它的表现非常好,除了当你在运行 iOS 8 的 iPhone 上以纵向方式启动应用程序时,分割视图的详细视图控制器被显示,而我预期首先看到主视图控制器。我认为这是 iOS 8 的一个 bug,因为当你在 iOS 7 上运行应用程序时,它会正确显示主视图控制器。但现在 iOS 8 已经发布了,这个问题仍然存在。我该如何设置才能在分割视图控制器要折叠(只显示一个视图控制器)时,显示主视图控制器而不是详细视图?我是在界面构建器中创建了这个分割视图控制器。分割视图控制器是选项卡栏控制器中的第一个视图控制器。主视图控制器和详细视图控制器都是嵌入在表视图控制器中的导航控制器。
14个回答

246

哦,这让我头疼了几天,一直想不出怎么做。最糟糕的是,使用主细节模板创建一个新的Xcode iOS项目完全没有问题。幸运的是,最后正是这个小事实帮我找到了解决方案。

有一些帖子建议在UISplitViewControllerDelegate上实现新的primaryViewControllerForCollapsingSplitViewController:方法来解决此问题。我试过了,但无济于事。苹果在主细节模板中采用的方法似乎有效,它实现了一个新的(说出这个方法名需要深吸一口气)splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:代理方法(同样在UISplitViewControllerDelegate上)。根据文档,该方法:

要求委托人调整主视图控制器,并将次级视图控制器合并到折叠界面中。

请务必阅读该方法的讨论部分以获取更详细的信息。

苹果处理此问题的方式是:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
  ontoPrimaryViewController:(UIViewController *)primaryViewController {

    if ([secondaryViewController isKindOfClass:[UINavigationController class]]
        && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]]
        && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) {

        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;

    } else {

        return NO;

    }
}

这个实现基本上执行以下操作:

  1. 如果secondaryViewController是我们期望的(一个UINavigationController),并且它正在显示我们期望的内容(一个DetailViewController-您的视图控制器),但没有模型(detailItem),则返回“Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
  2. 否则,返回"NO,让分割视图控制器尝试将次要视图控制器的内容合并到折叠的界面中"

在 iPhone 竖屏 模式下的结果如下(无论是在竖屏开始还是旋转到竖屏——或更准确的紧凑大小类别):

  1. 如果您的视图正确
    • 并且具有模型,则显示详细视图控制器
    • 但没有模型,则显示主视图控制器
  2. 如果您的视图不正确
    • 显示主视图控制器

清晰易懂。


8
太棒了!我只需将UISplitViewController子类化,然后从该方法中始终返回YES,然后只需更改故事板中的拆分视图类即可,因为我希望在iPhone上竖屏时始终显示主视图。 :) - Jordan H
2
我希望我的主视图控制器在“iPhone”处于“纵向”模式时被隐藏,因为我有一个默认的详细视图控制器设置。我应该如何做到这一点?我的主和详细都是VC类型。具体来说,我的详细是MMDrawerController。请帮忙解决。 - Harshit Gupta
4
我尝试了Joey的建议,通过子类化UISplitViewController,但发现并没有起作用:splitViewController:collapseSecondaryViewController:ontoPrimaryViewController:从未被调用。相反,我复制了苹果的模板,并将其放在了AppDelagate中。这就需要对在application didFinishLaunchingWithOptions:下创建UISplitViewController进行一些更改(我也复制了苹果的模板)。 - Nick
8
@Joey的评论在viewDidLoad中设置self.delegate = self; 并在.h文件中添加<UISplitViewControllerDelegate>。谢谢! - fellowworldcitizen
2
这对我来说似乎是正确的答案,因为我遇到了完全相同的问题。然而,由于某种原因,我的 splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: 从未被调用过。看起来委托被正确地设置在我的应用程序委托的 applicationDidFinishLaunchingWithOptions: 方法中。有其他人遇到过这个问题并且没有解决吗? - Tim Dean
显示剩余15条评论

62

下面是Swift中的被接受的答案。只需创建此子类并将其分配给您在故事板中的splitViewController。

//GlobalSplitViewController.swift

import UIKit

class GlobalSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

  override func viewDidLoad() {
    super.viewDidLoad()

    self.delegate = self
  }

  func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool{
    return true
  }

}

3
太好了,这很有帮助。但是出现了一个新问题。将我带回主视图的后退按钮消失了(从未显示过)。我该如何找回它?编辑:没关系,我自己解决了 :-)。对于其他用户:在detailView中添加以下内容:self.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem(),self.navigationItem.leftItemsSupplementBackButton = true - Tom Tallak Solbu
3
现在在Swift中有一个函数 splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool。它用于折叠次要视图控制器并将其合并到主要视图控制器上。 - Dan Rosenstark
3
似乎只有当屏幕尺寸较小时才会调用该委托方法。它在iPhone上被调用,但在iPad竖屏模式下没有被调用,这意味着它并未解决问题,因为iPad竖屏模式也处于折叠模式。在iOS 12.1上进行了测试。 - Daniel

24

马克S正确答案的Swift版本

由苹果公司的Master-Detail模板提供。

func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController:UIViewController, ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
    if topAsDetailController.detailItem == nil {
        // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return true
    }
    return false
}

澄清

(Mark S说的有点混乱)

这个代理方法被称为splitViewController: collapseSecondaryViewController: ontoPrimaryViewController:,因为它就是这么做的。当宽度变得更加紧凑时(例如从横屏切换到竖屏),它需要将分割视图控制器合并成其中一个。

该函数返回一个布尔值来决定是否折叠详细信息并显示主要内容。

所以在我们的情况下,我们将根据是否选择了详细信息来决定。我们如何知道我们是否已经选择了详细信息?如果我们遵循苹果的主细模板,详细视图控制器应该具有一个可选变量来保存详细信息,因此,如果它为空(.None),则尚未选择任何内容,我们应该显示主视图,让用户选择。

就是这样。


只是想澄清一下我为什么要回滚@sschale的编辑。 那段代码是从“苹果公司的主细节模板”中引用的,它并不打算很好或简洁,只是事实而已。 :) - NiñoScript

9

根据文档,您需要使用委托来告诉UISplitViewController在“折叠界面”(也就是您的情况下的“纵向模式”)中不要将详细视图合并。在Swift 4中,用于实现该功能的委托方法已更名为:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    return true
}

9
   #import <UIKit/UIKit.h>

    @interface SplitProductView : UISplitViewController<UISplitViewControllerDelegate>




    @end

.m:

#import "SplitProductView.h"
#import "PriceDetailTableView.h"

@interface SplitProductView ()

@end

@implementation SplitProductView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.delegate = self;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
  ontoPrimaryViewController:(UIViewController *)primaryViewController {

    if ([secondaryViewController isKindOfClass:[UINavigationController class]]
        && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[PriceDetailTableView class]]

        //&& ([(PriceDetailTableView *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)

        ) {

        // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return YES;

    } else {

        return NO;

    }
}
@end

9

我的应用程序是用Swift 2.x编写的,可以正常运行。在将其转换为Swift 3.0后(使用XCode转换器),它开始在纵向模式下首先显示详细信息而不是主信息。问题在于函数splitViewController的名称没有更改以匹配UISplitViewControllerDelegate的新名称。

手动更改该函数的名称后,我的应用程序现在可以正常工作:

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
    guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
    guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
    if topAsDetailController.game == nil {
        // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
        return true
    }
    return false
}

我和你遇到了同样的问题,但我不理解你的解决方案。我在你发布的代码中没有看到任何变化。你能具体说明一下吗?谢谢。 - bibscy
许多方法都稍微改了名字。 - Dave
Tony的答案是Swift 3语法,适用于@NiñoScript的答案(该答案适用于早期版本的Swift)。 - Hellojeffy
2
对于Swift 3,请不要忘记在viewDidLoad方法中添加self.delegate = self - Fer

7
如果您在详细视图控制器中没有默认值可显示,只需在故事板中删除SplitViewController和详细的UIViewController之间的默认segue即可。这将使其始终首先进入Master View Controller。
其副作用是,除非在主视图控制器中触发“显示详细信息Segue”,否则您将在SplitViewController中看到一个完整大小的视图。

好的技巧。我的应用程序只能在竖屏模式下使用,我可以做到这一点。 - Peacemoon
这是正确的,除非在横向方向上,您将看到视图的右侧为空,可能填充灰色。 - vedrano

4
在我看来,你应该更通用地解决这个问题。你可以继承UISplitViewController并在嵌入的视图控制器中实现协议。
class MasterShowingSplitViewController: UISplitViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension MasterShowingSplitViewController: UISplitViewControllerDelegate {
    func splitViewController(splitViewController: UISplitViewController,
                             collapseSecondaryViewController secondaryViewController: UIViewController,
                             ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
        guard let masterNavigationController = primaryViewController as? UINavigationController,
                  master = masterNavigationController.topViewController as? SplitViewControllerCollapseProtocol else {
            return true
        }
        return master.shouldShowMasterOnCollapse()

    }
}

protocol SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool
}

UITableViewController的示例实现:

extension SettingsTableViewController: SplitViewControllerCollapseProtocol {
    func shouldShowMasterOnCollapse() -> Bool {
        return tableView.indexPathForSelectedRow == nil
    }
}

希望能帮到你。 这样你就可以重用这个类,并且只需要实现协议即可。

委托方法从未被调用! - K_Mohit
它在iPad和iPhone 6/7/8 Plus上不被称为。这是你的问题吗?请查看:https://dev59.com/uYnda4cB1Zd3GeqPF_GI - user1199624

4

对于那些找不到cs193p星期五课程的人:

在Swift 3.1.1中,创建UISplitViewController的子类并实现其中一个代理方法对我来说非常有效:

class MainSplitViewController: UISplitViewController, UISplitViewControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
}

func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
    return true
} }

My storyboard


正如@olito所指出的那样,在Swift 4中,这个语法已经改变为:public func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool - Robuske

2

当您需要从主视图开始时,只需从SplitView控制器中删除DetailViewController即可。

UISplitViewController *splitViewController = (UISplitViewController *)[self.storyboard instantiateViewControllerWithIdentifier:@"SETTINGS"];
splitViewController.delegate = self;
[self.navigationController presentViewController:splitViewController animated:YES completion:nil];
if (IPHONE) {
    NSMutableArray * cntlrs = [splitViewController.viewControllers mutableCopy];
    [cntlrs removeLastObject];
    splitViewController.viewControllers = cntlrs;
}

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