「iPad 适配指南」 这个系列会介绍在 iPad 上的一些特殊能力,如何更好地适配 iPad,以及适配 iPad 时的一些注意点。
本文作为基础篇,主要介绍 iPad 的转屏分屏、模态,和 SplitVC 能力。
如何判断 iPad 设备
如何判断设备, iPad 的各种形态
if UIDevice.current.userInterfaceIdiom == .pad {}
在 M1 Mac 上运行的 iOS 应用取到的
userInterfaceIdiom
属性为.pad
在 Mac Catalyst 上运行的应用取到的
userInterfaceIdiom
属性为.mac
分屏适配篇
iPad 和 iPhone 最大的不同是,我们往往在 iPhone 上会限定 App 的方向恒定为 Portrait,但在 iPad 上,我们不仅要处理旋转屏,还要处理各种分屏的情况。
分屏
iOS 上的分屏最早可以追溯到随 iOS 10 推出的 SlideOver
、Split View
和画中画
功能。从 iOS 12 开始,应用分屏的概念和操作比较接近于现在的 iPadOS。
在 iPadOS 中,分屏下的应用主要有 8 种状态:横屏 1/3 屏
、横屏 1/2 屏
、横屏 2/3 屏
、横屏全屏
、竖屏 1/3 屏
、竖屏 2/3 屏
、竖屏全屏
,以及悬浮窗
。
分屏可以通过多种操作唤起,最常见的是长按 Dock 中的图标,然后拖动到屏幕的一侧。
尺寸变化
UITraitCollection 是什么
View / VC 如何兼容大小的变化
viewWillTransition willTransition
无论是旋转屏幕,还是分屏,我们都可以收敛到「尺寸变化」这个概念上一起处理。
在此之前,需要先介绍 UITraitCollection
的概念。
UITraitCollection 是什么
traitCollection
是UIView
,UIViewController
,UIWindow
,UIWindowScene
和UIScreen
等的属性。
Transition
是指 vc 将会变化,变化的新属性集合会在traitCollection
这个属性集合中。traitCollection
属性集合常用的属性有:纵横宽度的 sizeClass,是否是 darkMode 等属性
除了UIWindowScene
是直接实现的属性,其他列举到的都是通过 UITraitEnvironment
协议来实现的:
public protocol UITraitEnvironment : NSObjectProtocol {
@available(iOS 8.0, *)
var traitCollection: UITraitCollection { get }
/** To be overridden as needed to provide custom behavior when the environment's traits change. */
@available(iOS 8.0, *)
func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection?)
}
traitCollectionDidChange
一般会用在响应 iOS 界面环境的变化,对窗口大小变化的兼容会在接下来的一节中讲到。
View / VC 兼容大小的变化
约束布局不必考虑尺寸的变化
- 对于 View,可以在
layoutSubviews
中进行 frame 布局或响应尺寸的变化。当窗口大小发生变化的时候,VC 会调用 View 的该方法。 - 对于 VC,有两种策略:
- 在
viewWillLayoutSubviews
中进行布局 - 可以在以下两个方法中进行布局的调整:
// UIViewController 实现了这个协议
public protocol UIContentContainer : NSObjectProtocol {
@available(iOS 8.0, *)
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
@available(iOS 8.0, *)
func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)
}
-
调用时机的区别在于:
- VC 出现和大小变化时都会调用
viewWillLayoutSubviews
和willTransition
- VC 出现时,如果不主动改变 view 大小,不会调用
viewWillTransition
,仅当 view 的大小变化时才会调用
- VC 出现和大小变化时都会调用
-
2 中的两个函数的区别在于,窗口大小变化时:
-
willTransition
会先被调用。可以通过重写该方法获得即将变成的新 traitCollection- 注意:此时取 view/vc/window 的 traitCollection 仍为旧值
-
viewWillTransition
后被调用。可以通过重写该方法获得即将变成的新 size- 注意:此时取 view/vc/window 的 traitCollection 和 bounds.size 仍为旧值
- 最佳实践:如果需要在 viewWillTransition 中获取即将变成的新 traitCollection,可以考虑在 vc 持有一个
lastTraitCollection
,并且在willTransition
时更新其值。
-
这三种方法不仅仅会在上述情形中被调用。
App 在 iPad 退出后台或锁屏时,因为要生成横屏和竖屏的截图以便在 App Switcher 中显示,都会被多次调用。
详见后文「锁屏/退到后台时在 iPad 上的特殊情况」
UIScreen 的使用
大家可能早已习惯直接使用 UIScreen.main.bounds
。这在过去的一台设备只有唯一屏幕、一个屏幕只有唯一应用
情况下是没有问题的。但事情正在发生改变:在 iPadOS 上,一个屏幕已经能显示多个应用了,在 Apple Silicon Mac 上,一个设备也能有多个显示内容不一样的屏幕,应用并不一定会在 UIScreen.main
上显示。
我们应该遵循的原则是:在每个 UIView 中,获取自身的 bounds 属性,或者利用元素间的相对关系 Auto Layout 进行布局。应该尽量避免获取设备本身的宽高来进行布局。
SizeClass 介绍
介绍 sizeClass 概念,以及各种 iOS 窗口尺寸对应的 CR 值
概念
日常我们所说的Size Class,是UITraitCollection
中的两个属性:
@available(iOS 8.0, *)
open class UITraitCollection : NSObject, NSCopying, NSSecureCoding {
/// 水平 size class,最常用
open var horizontalSizeClass: UIUserInterfaceSizeClass { get }
/// 竖直 size class,用的少
open var verticalSizeClass: UIUserInterfaceSizeClass { get }
}
Size Class
将界面宽度分成了 Compact
和 Regular
两种类型。
@available(iOS 8.0, *)
public enum UIUserInterfaceSizeClass : Int {
/// 未指定
case unspecified = 0
/// 紧致
case compact = 1
/// 正常(宽松)
case regular = 2
}
对于每个 View / VC / Window / WindowScene / Screen,都有 size class 的概念。
Size Class
对我们最重要的意义是:
响应式布局最重要的即是
断点
。所谓断点,就是一个分界线,在这个分界线的两边,我们会采取不同的布局策略。而Size Class
给我们提供了关于断点
的指导。
系统水平方向 Size Class 规则
- 目前在 iPhone 竖屏时,
horizontalSizeClass
都是Compact
,其他情况比较复杂,参考官方文档,不展开赘述; - 在 iPad 上,
全屏
和横屏2/3分屏
都是Regular
; 横屏1/2分屏
时,只有 12.9 寸的 iPad 是Regular
;- 除此之外的其他情况都是
Compact
。
详见官方文档:Size Classes – HIG
布局控件篇
模态控件
介绍 modalPresentationStyle 各种样式的效果 以及着重介绍一下 popover 的概念
在 iPad 上,我们经常看到这样的页面。看起来两者差异很大,似乎需要做很多的适配,但其实代码很简单,我们只需要两行代码,就能同时完成在 iPhone 上和 iPad 上的适配:
vc.modalPresentationStyle = .formSheet
self.present(vc, animated: true)
这里涉及到了 modalPresentationStyle 的概念。
我们知道,一个 VC 可以被 push,也可以被 present。
两者在用法上的区别是,present 的页面会阻挡用户的其他操作,使其专注在当前页面上。
Sheet
在 iPad 上有两种种最常见的样式:.formSheet
和.pageSheet
,这三种都是 present 前可以设置给 VC 的样式。
在 iPhone 上,两种 Sheet 的样式没有什么分别:
在 iPad 上 formSheet 和 pageSheet 的区别是:
pageSheet
的浮窗大小是系统根据系统字体大小确定的,不能修改大小formSheet
和接下来要提到的popover
的大小,都可以通过 vc 的preferredContentSize
来指定实际大小。
pageSheet适合信息密度较高、阅读写作 | formSheet默认大小,适合信息密度较低或自定义大小的场景 |
iOS 13 对 formSheet 的窄屏样式从 fullScreen 变成了现在的层叠卡片样式
对于 formSheet
和 pageSheet
,在 iPad 上有手势下滑返回的自带功能。
如果希望介入手势下滑事件,可在 UIAdaptivePresentationControllerDelegate 中进行处理。
Popover 气泡
Popover 是 iPad 上非常常见的一种交互元素。
前面我们介绍到的 modalPresentationStyle
,还有一种取值即为 .popover
但与前面几种我们提到的 Style 不同的是,除了简单的指定 modalPresentationStyle
之外,我们还需要设置几个属性:
// 指定样式
pushvc.modalPresentationStyle = .popover
// 指定 Popover 指向的矩形
pushvc.popoverPresentationController?.sourceRect = btn.frame
// 指定 Popover 指向的 View,必须指定,否则会崩溃
pushvc.popoverPresentationController?.sourceView = self.view
// 指定 Popover 允许的箭头朝向
pushvc.popoverPresentationController?.permittedArrowDirections = .up
self.present(pushvc, animated: true)
modalPresentationStyle
我们以 iPad Pro 11-inch, iOS 14, SplitVC detailVC(yellow) present(purple 40% 透明度)VC 的 case 为例,简单介绍一下所有 modalPresentationStyle 的取值区别:
横屏全屏 | ||||||
---|---|---|---|---|---|---|
竖屏全屏 | ||||||
窄屏&iPhone | ||||||
类型 | fullScreen | pageSheet | formSheet | currentContext | overFullScreen | overCurrentContext |
大小特点 | 覆盖全屏 | 更大尺寸的模态 | 可自定义大小的模态,默认大小如图 | 只覆盖当前区域 | 覆盖全屏 | 只覆盖当前区域 |
当然,系统也提供了 custom 样式,以提供自定义动画和样式的能力。
over*
与*
的区别是:
over*
不会将覆盖的视图从视图层级撤下
iOS 15 | Customize and resize sheets in UIKit
Video: Customize and resize sheets in UIKit – WWDC 2021 – Videos – Apple Developer
在 iOS 15 中,Sheets 又有了一些新能力:
我们可以更精细化地控制 Sheets 的垂直高度了,比如创建一个半屏 Sheet,或者让 Sheet 可以在半屏高度停靠(Dedents):
我们可以移除 Sheets 下的阴影遮罩,让我们可以在展示 Sheet 的时候与下层 View 交互;
或者在 Compact 屏幕下展示非全屏 Sheet
所有的新特性都可以通过新 API:UISheetPresentationController 来进行行为的控制。
当 VC 的 modalPresentationStyle
为 formSheet / pageSheet (by default) 时,我们可以这样取得 UISheetPresentationController
// Get a sheet
if let sheet = viewController.sheetPresentationController {
// Customize the sheet
}
present(viewController, animated: true)
路由跳转
UISplitViewController
介绍 UISplitViewController 是什么
master detail 概念
showMaster / showDetail 的概念
各种 displayMode 代表什么
为了更好地利用 iPad 更大屏幕的尺寸,系统提供了 UISplitViewController
,以在宽屏情况下并列显示多个视图
上图是 iOS 14 中 UISplitViewController
更新的新接口,允许三栏同时展示。我们可以在系统自带的 邮件app 看到实际的效果。
iOS 14 更新了新的初始化接口:init(style:)
。通过这个接口我们可以在初始化时设置两栏或者三栏的布局:
DisplayMode
规定术语:
Master / Primary:两栏时,展示在左侧的单栏
Detail / Secondary:两栏时,展示在右侧的详细页面
UISplitViewController
有多种显示模式,我们称之为 DisplayMode
。这里简要介绍一下:
automatic | secondaryOnlyprimaryHidden | oneBesideSecondaryallVisable | oneOverSecondaryprimaryOverlay | twoBesideSecondary iOS 14 available | twoOverSecondary iOS 14 available | twoDisplaceSecondary iOS 14 available |
自动模式,根据屏幕大小自动切换 | 只展示 detail 页 | Master 和 detail 并列展示 | Master 盖住了 detail | 两栏与 detail 并列 | 两栏盖住了 detail | 两栏将 detail 向右挤开,参考 邮件.app |
简单概括:Bseide 意为并列显示,over 意为上层会覆盖下层的一个部分,Displace 意为上层会挤开下层。
常用接口
路由行为
如果是使用 init(style:)
初始化的 iOS 14 列风格 的 SplitVC,一切会变得省心很多:
-
用
setViewController(_:for:)
来设置 VC 应该展示在哪一列 -
用
viewController(for:)
来获取指定列的 VC -
SplitVC 会自动把所有的 childVC 用 navigationController 包住。
- 如果设置的时候没有提供 navigationController,SplitVC 会自动创建一个。
- 通过 SplitVC 的 children 属性可以找到 navigationController。
-
用
show(_:)
或者hide(_:)
来展示或隐藏指定列
如果是传统风格的 SplitVC(只支持 master & detail 的显示,不支持更多栏):
- 如果需要,应该手动为 master 和 detail 手动设置 navigationController 以实现路由跳转。
- 直接设置
viewControllers
属性,默认第一个为 master,第二个为 detail,会忽略更多(如果有) - 使用
show(_:sender:)
来在 master 中找到 navigationController 进行 push vc - 使用
showDetailViewController(_:sender:)
来 在 detail 中找到 navigationController 进行 push vc
尺寸变化
在 iPad 上,用户可能进行的分屏操作会突然改变程序的视图大小。当视图较窄时,SplitVC 的分栏布局可能不再适合,我们可能需要将所有栏中的 viewControllers 进行合并。当视图变宽时,我们又需要将 viewControllers 分配到不同的列当中。在这里我们称之为 Collapse & Expand。
我们可以在 SplitVC 的 delegate 中控制上述行为:
public protocol UISplitViewControllerDelegate {
// Return the view controller which is to become the primary view controller after `splitViewController` is collapsed due to a transition to
// the horizontally-compact size class. If you return `nil`, then the argument will perform its default behavior (i.e. to use its current primary view
// controller).
@available(iOS 8.0, *)
optional func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController?
// Return the view controller which is to become the primary view controller after the `splitViewController` is expanded due to a transition
// to the horizontally-regular size class. If you return `nil`, then the argument will perform its default behavior (i.e. to use its current
// primary view controller.)
@available(iOS 8.0, *)
optional func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController?
// This method is called when a split view controller is collapsing its children for a transition to a compact-width size class. Override this
// method to perform custom adjustments to the view controller hierarchy of the target controller. When you return from this method, you're
// expected to have modified the `primaryViewController` so as to be suitable for display in a compact-width split view controller, potentially
// using `secondaryViewController` to do so. Return YES to prevent UIKit from applying its default behavior; return NO to request that UIKit
// perform its default collapsing behavior.
@available(iOS 8.0, *)
optional func splitViewController( _ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool
// This method is called when a split view controller is separating its child into two children for a transition from a compact-width size
// class to a regular-width size class. Override this method to perform custom separation behavior. The controller returned from this method
// will be set as the secondary view controller of the split view controller. When you return from this method, `primaryViewController` should
// have been configured for display in a regular-width split view controller. If you return `nil`, then `UISplitViewController` will perform
// its default behavior.
@available(iOS 8.0, *)
optional func splitViewController( _ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController?
}
锁屏/退到后台时在 iPad 上的特殊情况
在 iOS 上,因为需要在 App Switcher 中显示各应用在横屏、竖屏、分屏情况下的界面预览,所以系统会提前在应用锁屏或退到后台时,对应用进行模拟界面变化并截图。
系统函数名为beginSnapshotSession。
在 iPad 上的整个模拟界面变化的过程中,一般会模拟横屏、竖屏、分屏等几种大小。处于最上层的 VC 可能会收到多次 willTransition / viewWillTransition / viewWillLayout 的调用。
在存在 SplitVC 的情况中,甚至因为模拟分屏,导致 mergeMasterAndDetail 时隐藏了 VC,调用到 VC 的 viewDidDisappear,也是有可能的。
今天的文章iPad 适配指南 – 基础篇分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21222.html