iPad 适配指南 – 基础篇

iPad 适配指南 – 基础篇「iPad 适配指南」 这个系列会介绍在 iPad 上的一些特殊能力,如何更好地适配 iPad,以及适配 iPad 时的一些注意点。 本文作为基础篇,主要介绍 iPad 的转屏分屏、模态,和

「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 推出的 SlideOverSplit View画中画功能。从 iOS 12 开始,应用分屏的概念和操作比较接近于现在的 iPadOS。

在 iPadOS 中,分屏下的应用主要有 8 种状态:横屏 1/3 屏横屏 1/2 屏横屏 2/3 屏横屏全屏竖屏 1/3 屏竖屏 2/3 屏竖屏全屏,以及悬浮窗

iPad 适配指南 - 基础篇

分屏可以通过多种操作唤起,最常见的是长按 Dock 中的图标,然后拖动到屏幕的一侧。

尺寸变化

UITraitCollection 是什么

View / VC 如何兼容大小的变化

viewWillTransition willTransition

无论是旋转屏幕,还是分屏,我们都可以收敛到「尺寸变化」这个概念上一起处理。

在此之前,需要先介绍 UITraitCollection 的概念。

UITraitCollection 是什么

traitCollectionUIViewUIViewControllerUIWindowUIWindowSceneUIScreen等的属性。

  • 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,有两种策略:
  1. viewWillLayoutSubviews中进行布局
  2. 可以在以下两个方法中进行布局的调整:
// 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 出现和大小变化时都会调用viewWillLayoutSubviewswillTransition
    • VC 出现时,如果不主动改变 view 大小,不会调用viewWillTransition,仅当 view 的大小变化时才会调用
  • 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 将界面宽度分成了 CompactRegular 两种类型。

 @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

iPad 适配指南 - 基础篇

详见官方文档:Size Classes – HIG

布局控件篇

模态控件

介绍 modalPresentationStyle 各种样式的效果 以及着重介绍一下 popover 的概念

iPad 适配指南 - 基础篇iPad 适配指南 - 基础篇

在 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 适配指南 - 基础篇

在 iPad 上 formSheet 和 pageSheet 的区别是:

  • pageSheet 的浮窗大小是系统根据系统字体大小确定的,不能修改大小
  • formSheet 和接下来要提到的 popover 的大小,都可以通过 vc 的 preferredContentSize 来指定实际大小。
iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇
pageSheet适合信息密度较高、阅读写作 formSheet默认大小,适合信息密度较低或自定义大小的场景

iOS 13 对 formSheet 的窄屏样式从 fullScreen 变成了现在的层叠卡片样式

对于 formSheetpageSheet,在 iPad 上有手势下滑返回的自带功能。

如果希望介入手势下滑事件,可在 UIAdaptivePresentationControllerDelegate 中进行处理。

Popover 气泡

iPad 适配指南 - 基础篇

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 的取值区别:

横屏全屏 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇
竖屏全屏 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇
窄屏&iPhone iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇
类型 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):

iPad 适配指南 - 基础篇

我们可以移除 Sheets 下的阴影遮罩,让我们可以在展示 Sheet 的时候与下层 View 交互;

或者在 Compact 屏幕下展示非全屏 Sheet

iPad 适配指南 - 基础篇iPad 适配指南 - 基础篇

所有的新特性都可以通过新 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,以在宽屏情况下并列显示多个视图

iPad 适配指南 - 基础篇

上图是 iOS 14 中 UISplitViewController 更新的新接口,允许三栏同时展示。我们可以在系统自带的 邮件app 看到实际的效果。

iOS 14 更新了新的初始化接口:init(style:)。通过这个接口我们可以在初始化时设置两栏或者三栏的布局:

iPad 适配指南 - 基础篇

DisplayMode

规定术语:

Master / Primary:两栏时,展示在左侧的单栏

Detail / Secondary:两栏时,展示在右侧的详细页面

UISplitViewController 有多种显示模式,我们称之为 DisplayMode。这里简要介绍一下:

iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇 iPad 适配指南 - 基础篇
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

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注