使用 UISplitViewController 优化大屏分栏式体验

type
status
date
slug
summary
tags
category
icon
password
在日常的使用中,我们可能会注意到 iOS 系统应用以及一些优秀应用在 iPad 以及一些宽屏设备上拥有着良好的体验效果,和在 iPhone 上的单页面不同,他们展示的效果是分栏式的,左右两边排布着两个页面,在大屏设备上可以很好的提高我们的使用效率以及良好的视觉感受。
最直观的效果,我们可以打开手机的自动屏幕旋转,然后打开设置 app,横屏使用。
notion image
这种效果可以更好的适配宽屏,最大化的利用屏幕空间,也可以让一些 iOS app 在 iPad 上看起来不那么奇怪,并且苹果也发布了M1芯片的 mac,让所有的 app 可以不需要做任何操作直接在 mac 上 Appstore 下载运行,如果我们不对当前的 app 进行一定的适配,在 Mac 上将不能拉伸窗口,只能通过一个固定的尺寸使用,随着 M1的普及以及 iPad 的生产力增强,app 在各种尺寸的屏幕上适配也是大势所趋。一看之下感觉开发起来很难成本很高,从控制一个页面变成两个页面甚至多个页面似乎要做很多界面上的处理,但实际上系统提供的 UISplitViewController 可以很好的满足我们的需求,接下来将介绍如何通过UISplitViewController 在各种屏幕上塑造良好的应用体验。

一、基础知识

基本概念 Compact & Regular

notion image
首先我们了解一些基础的概念,UIKit 在 iOS8上提出了 SizeClass 的方式,来帮助开发者解决屏幕适配的问题。SizeClass 中包含两个类型 Compact 和 Regular,可以理解为 UIInterface 宽度或者高度有一个默认的高度值和宽度值,高于这个值就被认为是 Regular 常规尺寸,低于这个就被认为 Compact 紧凑尺寸。
  • Compact 指的是紧凑型,意味着有限的空间,分别在宽高上对应着 wC 和 hC。
  • Regular 指的是 常规型,意味着无限的空间,分别在宽高上对应着 wR 和 hR。
我们也可以在 xib 上看到这些值的身影,例如字体和颜色,通过针对其进行特殊的设置,可以实现不同类型的界面上显示不同的颜色和字体。
notion image
知道了 Compact 和 Regular 的含义,我们就可以知道什么状态下显示分栏,什么时候显示单页面了。UISplitViewController 会在屏幕宽度为 Compact 的时候显示单页面,在 Regular 的时候显示分栏,这是系统默认控制的。

了解控制器 UISplitViewController

UISplitViewController 继承自 UIViewController,为了方便理解,中文将它称为分栏控制器,如果我们需要使用它,苹果建议将他设置为 rootViewController。在 iOS14 上,苹果对 UISplitViewController 新增了很多的API,之前只支持两列,分别为 primary 主控制器,secondary 二级控制器 ,iOS14 可以支持三列,分别为 primary 主控制器,supplementary 附加控制器 ,secondary 二级控制器,因为大多数的应用还需要支持 iOS14 之前的版本,并且新增的 API 都很简单,下面就以 iOS14之前的 API 为主。
notion image
UISplitViewController 有一个 Bool 类型的属性 isCollapsed,表示是否折叠,这个属性对应着两种状态,一个是 collapsed/折叠,以及 expanded/展开,在 Compact 紧凑型视图上是折叠的,这个时候默认会展示 primary 主控制器 ,此时是没有分栏效果的,就是单页面展示,在 Regular 常规型视图上是展开的,也就是能够分栏展示。isCollapsed 是只读的,我们不能手动设置,是系统根据当前视图是 compact 或者 Regular 来自动控制的。
在分栏控制器展开状态下,我们可以注意到不同的 app 分栏的展示形式不太一样,有的是并排展示,有的是屏幕边缘右滑触发显示,如下图,这是因为分栏控制器有着不同的显示模式,对应的属性为 UISplitViewController.DisplayMode。我们可以通过赋值preferredDisplayMode来设置我们偏好的显示模式,系统会尽量满足我们的偏好,如果不设置的话,默认为 automatic,这种情况下在 iPad 竖直状态 primary 主控制器是隐藏在侧边栏,也就是 oneOverSecondary状态,横屏模式下是默认并排展开,也就是 oneBesideSecondary。下图就是 iOS14支持的一些显示模式,iOS14之前只支持下图中的secondaryOnlyoneBesideSecondaryoneOverSecondary 三种显示样式。注意 secondaryOnly 虽然也是显示一个单页面,但是此时 isCollapsed 值为 false,是展开状态。
notion image
我们可以通过 viewControllers 给 UISplitViewController 设置初始结构。默认的子 viewControllers都是导航控制器,如果不是导航控制器的话,UISplitViewController 会给 vc 嵌套上一个导航控制器。

API 文档

UISplitViewController 的 API 文档不长,我们可以过一遍其中一些重要属性和方法的含义。
UISplitViewControllerDelegate 代理可以定制一些我们自己的实现方案,这些方法都是可选的,如果不实现的化,默认就会以系统的实现方式。

二、实践

明确了基本的概念以及 API,我们开始实现一个分栏项目,首先我们要明确我们的需求是什么,也就是确定什么情况下要分栏,什么时候下折叠,我以微信 的方案作为实践,需求是:
  1. 在 iPhone 上只支持竖屏。
  1. 在 iPad 上横屏模式下支持分栏,竖屏模式下不分栏,展示单页面,就像在手机上使用一样。
  1. 二级页面默认展示占位控制器。
  1. 展开的时候主控制器始终显示 tabbarController,二级控制器显示详情页面。
  1. 折叠起来的时候将二级控制器的页面叠进主控制器中。
  1. 保存每个 tab 下对应的二级控制器的堆栈状态,下次回到 tab 的时候需要恢复状态。
emmm...如果上面的需求还不是很清楚,体验一下 iPad 版微信或者 Taio 就知道了(Taio 支持 iPhone 横屏分栏)。
notion image

初始化 SplitViewController

根据需求,我们先确定页面结构,如下图:
notion image
我这里在 NavigationController 里面嵌套 TabbarController,实际上根据项目的需求反过来也没问题。
这里因为我们需要对 UISplitController 做一些特殊的处理,所以创建一个子类,做一些定制的操作。

折叠展开界面的切换

完成到这里,我们运行应用,在 iPad 上就可以看到分栏的界面了,但是在 iPhone 上运行,会发现默认展示的是 emptyController,这是因为打开应用的时候会调用代理方法 splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool,默认会返回 false 执行系统默认的折叠操作,调用 primaryViewController 的 collapseSecondaryViewController(_:for:) 方法,这个方法会将secondaryNavigationController 里面的子控制器移到 primaryNavigationController 中,然后移除 secondaryNavigationController,我们二级控制器里面是 emptyController,所以折叠状态下默认会被移到 primaryViewController 中。
上面这种情况,需要我们自己在代理方法中控制折叠和展开子控制器的转移,代码如下:

二级控制器的跳转和切换

我们希望分栏控制器的 push 符合在 iPhone 上的用户习惯,点击左边的主控制器跳转页面的时候,右边二级控制器清空堆栈仅保留 emptyController,然后 push 到新页面。如果点击右边的二级控制跳转页面的时候,二级控制器直接 push。系统给 UIViewController 提供了两个方法 open func show(_ vc: UIViewController, sender: Any?) 以及 open func showDetailViewController(_ vc: UIViewController, sender: Any?)
open func show(_ vc: UIViewController, sender: Any?) 使用这个方法,视图控制器不需要知道它是嵌入在导航控制器还是分栏视图控制器内。UISplitViewController和UINavigationController类重写了这个方法,并根据它们的设计来处理呈现。如果是导航控制器,就等同于 push,这个例子中我们使用的分栏控制器,会替换掉主控制器。
open func showDetailViewController(_ vc: UIViewController, sender: Any?) 类似于上面的方法,UISplitViewController 默认会替换 secondaryController,但如果此时分栏控制器是折叠状态,就会调用 show(_ vc: UIViewController, sender: Any?)
对于我们的项目,只需要在四个一级 tab 页面 push 的时候判断当前分栏控制器是否展开,如果展开就调用 showDetailViewController,然后在 SplitViewController 中实现 showDetail 的代理方法,截断系统的替换方案,由我们自己实现。
这样就实现了点击左边的主控制器,显示到二级控制器上,至于二级控制器的内部跳转,直接 push 即可,不需要多余的处理了。

保存每个 Tab 的导航栈状态

使用微信的时候你会发现,在每个 tab 下做的跳转会保存下来,下次切换回来的时候仍然显示的原来的导航栈。
按照我们的界面框架,导航栏嵌套标签栏,需要我们自己保存好堆栈数组,如果是标签栏嵌套导航栏,则会相对简单,实现方案可以参考这篇文章的最后小节。
我这里的处理是每个 tab 对应一个可选的 viewcontrollers 数组,保存导航栈里面的控制器。

iPad 竖屏时折叠分栏控制器

此时功能已经基本完成,还有一个问题是我们想控制什么情况下折叠,什么情况下展开分栏控制器,但是 isCollapsed是只读的,系统根据当前的界面紧凑型还是常规型来决定是折叠还是分栏,那么我们是否可以控制紧凑型和常规型的判断条件?
UIViewController 中有一个方法 - (nullable UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController,重写子控制器的UITraitCollection,可以修改所有子控制器的特性。我们可以通过这个方法来控制视图的类型。
这里就需要创建一个控制器,将它作为 SplitViewController 的 parent,并且将它设置为 rootViewController,然后重写他的这个方法。
以上,就可以实现类似于微信 iPad 上的分栏的效果。

总结

关于 UISplitController 的文档不多,这篇文章是摸索加实践完成的,认真阅读官方文档,理解原理,实现我们想要的效果就不难,不排除有一些错误,有任何问题欢迎讨论交流。
 
相关文档:
 
notion image
  • 📕 小红书/即刻:@轻舟
  • ☕ 如果我的内容有帮助到你,可以请我喝杯咖啡,这将鼓励我为你创造更多有价值的内容。
Buy Me A Coffee
  • Giscus

© 轻舟 2017-2024