【问题标题】:Xcode Master-Detail Template adding UITabBarController as the Master of the UISplitViewControllerXcode Master-Detail Template 添加 UITabBarController 作为 UISplitViewController 的 Master
【发布时间】:2016-04-13 18:53:15
【问题描述】:

我正在使用 Xcode 7(虽然我认为这也适用于 6),主从模板稍作修改。修改是将主UINavigationController替换为UITabBarController。

UITabBarController 包含几个 UINavigationController,它们又与 Detail UINavigationController 有 segues。

使用 iPhone 6+ 模拟器,它在横向模式下运行良好(非折叠)。当 UISplitViewController 折叠时,应用程序将停止按预期运行。发生了两件事:

  • 在 AppDelegate 的 didFinishLaunchingWithOptions 中,splitViewController.viewControllers 数组包含 UITabBarController(主)和 UINavigationController(详细)。在didFinishLaunchingWithOptionssplitViewController:collapseSecondaryViewController:ontoPrimaryViewController 之间的某个地方,详细信息UINavigationControllersplitViewController.viewControllers 中删除。如果 master 和 detail 都是 UINavigationController 对象,则不会发生这种情况。有趣的是,在 Apple 的 UISplitViewController 文档中,他们谈到了 viewControllers 属性:“展开拆分视图界面时,此属性包含两个视图控制器;当它折叠时,此属性仅包含一个视图控制器。”文档中的断言似乎仅在我修改故事板以将 UITabBarController 作为主控后才成立,在默认的主从模板中,断言不成立(即无论是否拆分视图折叠或展开)。
  • 如果我在折叠模式下运行时添加项目,则详细视图会以模态方式呈现,而不是被推送到选定选项卡的导航控制器上,因此无法返回主视图。旋转到横向会导致 EXC_BAD_ACCESS 崩溃 - 出于某种原因,我也收到错误消息:"<Error>: CGImageCreate: invalid image size: 0 x 0."

在这个阶段对“vanilla”模板应用程序的唯一更改是将 Master UINavigationController 替换为包含 UINavigationControllers 的 UITabBarController,情节提要如下:

我还在MasterViewController.m 中注释掉了以下行,以防止更早的崩溃:

self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];

我的问题是:

  1. 为什么当我将 UITabBarController 添加为主而不是保留 UINavigationController 时,viewControllers 属性的内容会发生变化?我问是因为我认为这与确定为什么细节 VC 以模态方式呈现而不是被推送有关。我假设,由于在折叠模式下,SplitViewController 的行为类似于 UINavigationController,当 UISplitViewController 和详细视图之间存在非 UINavigationController 时,这样做会有些困难。
  2. 从问题 1 开始,为什么细节控制器会以模态方式呈现?

我正在努力了解正在发生的事情,就像我试图找到一种正确的方法来让它工作(如果有一个(或多个)的话)。

进一步讨论及相关问题

我的问题与这个问题非常相似: UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone

结合使用“Dreaming In Binary”和“HpTerm”的答案,我取得了一些成功。

如果我首先接受 Dreaming In Binary 的答案(逐字使用该答案中的代码),那么应用程序的行为几乎与以前相同;我特别注意以下几点:

  • 从纵向视图中选择时,详细视图仍以模态方式呈现;由于我们添加了此方法(取自上述答案),因此基本上采用了稍微不同的详细视图呈现路径:

    -(BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender
    {
      UITabBarController *masterVC = splitViewController.viewControllers[0];
    
      if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
        [masterVC.selectedViewController showViewController:detailVC sender:sender];
      else
        [splitViewController setViewControllers:@[masterVC, detailVC]];
    
      return YES;
    }
    

    showViewController 仍然会触发 detailViewController 的模态展示。纵向右上方没有导航按钮,但是如果将设备旋转到横向,则整个横向将由详细视图获取,但我们确实看到使用 splitViewController.displayModeButton 项设置了 navigationItem 的 leftBarButtonItem。问题是该按钮除了显示“展开”按钮(用于隐藏主视图)之外绝对没有任何作用;如果您按下按钮,视图不会发生任何变化(无论如何,主视图已经隐藏)但按钮会更改为“返回”样式,这通常允许您再次显示主视图。在这种情况下,“返回”按钮在您按下时不起作用。本质上,您可以在“展开”和“返回”之间切换按钮,但按下它时没有其他效果。如果您旋转回横向,那么您将再次获得模态显示的详细视图,无法返回主视图。

    • 我仍然看到错误 <Error>: CGImageCreate: invalid image size: 0 x 0.,但它并未伴随 EXC_BAD_ACCESS 崩溃。

接下来是 HpTerm 在 Swift 中的解决方案,在我的世界里是这样的:

- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailViewController sender:(id)sender
{
    UITabBarController * masterViewController = splitViewController.viewControllers[0];

    if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
    {
        UIViewController * detailViewSubController = ((UINavigationController *)detailViewController).viewControllers[0];
        [((UINavigationController *)masterViewController.selectedViewController) pushViewController:(UIViewController *)detailViewSubController animated:NO];
      return YES;
    }
    else
    {
      return NO;
    }
}

所以在上面,我们没有尝试使用细节的UINavigationControllershowViewController,而是从细节UINavigationController 中提取DetailViewController,并将DetailViewController 推送到主UINavigationController。没有将细节UINavigationController 推送到主控UINavigationController 的原因是抛出了异常(适用于将任何导航控制器推送到另一个控制器)。

这实际上产生了一个几乎完美的结果,但并不完全。首先,好的部分:

  • 在纵向中,一切正常,我们将细节推到 掌握选择,当我们返回时,我们回到 正确掌握。
  • 在旋转到横向时,拆分视图正确显示主视图和细节,我们可以使用栏按钮项正确显示/隐藏主视图。

现在不好了:

  • 在旋转回纵向时(在主端选择了一个项目),我们会转到 master 视图,而实际上我们应该转到所选的详细项目。

所以这里的问题是,是否可以纠正“坏”点,以便如果在纵向选择项目时,旋转回横向将我们带到所选的详细项目?

抱歉,拖了这么久,如果你做到了这一步,我向你致敬!我也对缩小问题的建议持开放态度,但希望提供足够的细节来准确突出正在发生的事情。

【问题讨论】:

    标签: ios xcode cocoa-touch


    【解决方案1】:

    我仍然愿意接受其他答案,因为有几种方法可以解决这个问题,但一种有效的方法是修改 Master-Detail App Template 的 splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: 实现。

    先解释一下;在上述问题的最后一部分中,splitViewController:showDetailViewController:sender: 的实现解决了 UI 端的一个(重要)问题。它允许将详细视图推送到主 UINavigationController 的堆栈上,同时保持主 UITabBarController 可见并允许导航回主视图。它通过将DetailViewController 从拆分视图控制器的详细导航控制器中取出并将DetailViewController 推入堆栈来在折叠模式下执行此操作。这与默认的 Master-Detail 模板不同,因为在该版本中,详细导航控制器被推送到主导航控制器上并且仍然可以工作(我只是无法找到一种方法让它在添加后像这样工作UITabBarController)。完成此操作后,当我们旋转到横向(展开)视图时,一切仍然正常(细节导航控制器返回),但是当我们旋转到纵向(折叠)视图时,即使在选择了一个详细项目。

    我通过触发从 master 到详细视图的 segue 向 splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: 添加附加条件,强制推送详细视图(如果有要显示的数据):

    if ([secondaryViewController isKindOfClass:[DetailViewController class]] && [(DetailViewController *)secondaryViewController detailItem] != nil && [primaryViewController isKindOfClass:[UITabBarController class]])
    {
        UINavigationController * currentMasterNavigationController = ((UITabBarController *)primaryViewController).selectedViewController;
        MasterViewController * masterViewController = (MasterViewController *)[currentMasterNavigationController visibleViewController];
        [masterViewController performSegueWithIdentifier:@"showDetail" sender:masterViewController];
        return YES;
    }
    

    修改后的完整功能是:

    - (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 if ([secondaryViewController isKindOfClass:[DetailViewController class]] && [(DetailViewController *)secondaryViewController detailItem] != nil && [primaryViewController isKindOfClass:[UITabBarController class]])
        {
            UINavigationController * currentMasterNavigationController = ((UITabBarController *)primaryViewController).selectedViewController;
            MasterViewController * masterViewController = (MasterViewController *)[currentMasterNavigationController visibleViewController];
            [masterViewController performSegueWithIdentifier:@"showDetail" sender:masterViewController];
            return YES;
        }
        return NO;
    }
    

    这对我来说似乎仍然太过分了,但它确实有效。我希望看到一个比这更干净的答案,它更像默认模板,同时仍然允许UITabBarController

    【讨论】:

      【解决方案2】:

      我相信您对工作实施有所了解。我花了几个星期才完成。

      Apple 的 UISplitViewController 确实需要一些额外的调整才能使大小类更改正常工作。我已经把自适应 UISplitViewController 和 UITabBarController 作为主视图控制器开源在 GitHub indievox-inc/TabBarSplitViewController 上,应该值得一试。

      您可能想检查the original implementation,它更容易移植到Objective-C代码。

      以下主要功能:

       // MARK: to Compact Width size class (collapse)
          public func primaryViewControllerForCollapsingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
              if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
                     primaryNavViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                      let secondaryViewController = splitViewController.viewControllers[1]
                      if !(secondaryViewController.dynamicType === DetailViewControllerType.Empty) {
                          dispatch_async(dispatch_get_main_queue()) {     // otherwise we may get console error "<Error>: CGImageCreate: invalid image size: 0 x 0."
                              primaryNavViewController.showViewController(secondaryViewController, sender: secondaryViewController)
                          }
                          return primaryTabViewController
                      }
              }
      
              return nil
          }
      
      // MARK: to Regular Width size class (separate/expand)
          public func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController) -> UIViewController? {
              if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
                     primaryNavViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                      let primaryTopViewController = primaryNavViewController.topViewController
                      if let primaryTopViewController = primaryTopViewController {
                          if ((primaryTopViewController.dynamicType === DetailViewControllerType.General)
                           || (primaryTopViewController.dynamicType === DetailViewControllerType.Empty)) {
                              primaryNavViewController.popViewControllerAnimated(false)
                              return primaryTopViewController
                          }
                      }
              }
      
              return DetailViewControllerType.Empty.init()
          }
      
      // MARK: override showDetailViewController
          public func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
              let isCompactWidth = (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.Compact)
              if isCompactWidth {
                  if let primaryTabViewController = splitViewController.viewControllers[0] as? UITabBarController,
                            primaryViewController = primaryTabViewController.selectedViewController as? UINavigationController {
                              primaryViewController.showViewController(vc, sender: sender)
                              return true
                  }
              }
      
              return false
          }
      

      【讨论】:

      • 最好把相关代码放在这里,而不是放在外部网站上(和Github一样好)。
      • 当然,我已经添加了关键函数来使整个 UISplitViewController 工作。只需要阅读文档并进行测试即可完全了解 UISplitViewControllerDelegate 的工作原理。
      猜你喜欢
      • 1970-01-01
      • 2011-12-23
      • 1970-01-01
      • 1970-01-01
      • 2012-07-09
      • 2021-11-11
      • 1970-01-01
      • 2016-03-25
      • 1970-01-01
      相关资源
      最近更新 更多