WWDC22 UIKit 新变化
最近几年的 WWDC 每年都能看到很多 SwiftUI 的新能力,但不能忽略的是 UIKit 框架的更新。
今年的 What’s New in UIKit Session 主要包括以下几个方面
- 生产力提升
- 控制增强
- API 改进
- UIKit 与 SwiftUI 混编
我按照业务理解和适配进度的优先级,对 session 内容进行了重新排序和整理,对 @ferhanakkan 的仓库进行了一些改动,以下为我的总结。
https://github.com/iamStephenFang/WhatsNewInUIKit
API 改进
在 iOS 16 中部分 API 废弃,需要开发者进行适配,同时有一些新能力可以实现。
UIDevice
为了防止用户留下指纹,
UIDevice.name
现在会报告模型名称而非用户自定义的设备名称。 使用自定义名称需要获得授权。1
2
3// iOS 16 之前 (e.g. "My iPhone")
// iOS 16 (e.g. "iPhone 13")
UIDevice().name不再支持
UIDevice.orientation
, 应使用UIViewController
相关API, 如preferredInterfaceOrientation
来获取应用界面的预期呈现方向。1
2
3
4
5// UIDeviceOrientation(rawValue: 0) -> .unknown
UIDevice().orientation
// UIInterfaceOrientation(rawValue: 1) -> .portrait (iPhone)
UIViewController().preferredInterfaceOrientationForPresentation
UIScreen
iOS 16 为 配备了 M1 芯片的 iPad 以及 Mac 带来了 Stage Manager (台前调度)功能,作为应用开发者无需对代码进行改动即可适用该功能。
如果仍在使用旧版本的 UIScreen API,有必要迁移到新的 UITraitCollection
和 UIScene
API
UIScreen.main
已废弃,需要使用(UIApplication.shared.connectedScenes.first as? UIWindowScene)?.screen
- UIScreen 生命周期通知废弃,包括
didConnectNotification
、didDisconnectNotification
,需要使用UIScene
相关方法
自适应大小 cell
UICollectionView
和 UITableView
的 cell 支持了自适应调整大小能力。控制自适应调整大小的是 selfSizingInvalidation
属性,默认开启。
1 | class UICollectionView { |
- 若使用了
UIListContentConfiguration
配置cell,每当 cell 的配置发生更改时会自动执行 invalidation。 - 若不使用
UIListContentConfiguration
配置cell,可以调用 cell 的invalidateIntrinsicContentSize
方法手动执行 invalidation。 - 若使用 Auto Layout 布局cell,可以通过设置
selfSizingInvalidation
属性为enabledInclingConstraints
来使其接收 Auto Layout 变更。即当 cell 检测到 contentView 内部的任何自动布局变化时,将自动调用invalidateIntrinsicContentSize
方法。
默认情况下 cell 自适应调整大小会伴随着动画,可以在 invalidateIntrinsicContentSize
方法外包一层 performWithoutAnimation
从而取消调整大小时的动画。
1 | @objc private func didTapCollapseButton() { |
UICollectionView
和 UITableView
将 cell 自适应大小的 invalidation 行为智能合并处理,并在最佳时间执行更新。
UISheetPresentationController
在 iOS 15 上苹果推出了表单展示控件,可以通过简单的代码 present 出 .medium()
和 .large()
两种大小的底部表单。
1 | // on iOS 15 |
用户可以通过拖拽的方式实现高度的切换,但是当时底栏不具备自定义高度的能力。
在 iOS 16 上苹果开放了 UISheetPresentationController.Detent.Identifier
提供了自定义的能力。
可以简单返回一个常量值或最大高度的百分比来控制表单高度。
1 | open class Detent : NSObject { |
更标准的做法是通过自定义 identifier 定制表单展示控件的高度。
1 | // Define a custom identifier |
在使用的过程总需要注意表单展示控件高度不包含 SafeAreaInsets
,计算布局时需要注意。
UI 控件
UIPasteControl
在 iOS 16 前在 app 内执行复制粘贴操作会在顶部显示banner,在 iOS 16上 banner 被 alert 所取代。仍然由系统自动提示,根据用户的选项允许剪贴板内容访问。
开发者可以用新加入的 UIPasteControl
作为粘贴控件替换这个 alert,可以在这里找到相关文档。
1 | fileprivate lazy var pasteControl: UIPasteControl = { |
这个适配虽然达成了必须有用户交互才能读取剪贴板的目的,但也等来了一些开发者的吐槽。
https://twitter.com/cyanapps/status/1535187013611438081
UIPageControl
UIPageControl
在 iOS 16 上得到了增强,主要包括以下两点
可以针对不同的选中状态展示不同的图像
1
2
3
4
5/// The preferred image for the current page indicator. Symbol images are recommended. Default is nil.
/// If this value is nil, then UIPageControl will use \c preferredPageIndicatorImage (or its per-page variant) as
/// the indicator image.
@available(iOS 16.0, *)
open var preferredCurrentPageIndicatorImage: UIImage?可以设置布局方向为水平或垂直
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/// The layout direction of the page indicators. The default value is \c UIPageControlDirectionNatural.
@available(iOS 16.0, *)
open var direction: UIPageControl.Direction
@available(iOS 16.0, *)
public enum Direction : Int, @unchecked Sendable {
/// Page indicators are laid out in the natural direction of the system locale.
/// By default, this is equivalent to @c UIPageControlDirectionLeftToRight on LTR locales, and
/// @c UIPageControlDirectionRightToLeft on RTL locales.
case natural = 0
/// Page indicators are laid out from left to right.
case leftToRight = 1
/// Page indicators are laid out from right to left.
case rightToLeft = 2
/// Page indicators are laid out from top to bottom.
case topToBottom = 3
/// Page indicators are laid out from bottom to top.
case bottomToTop = 4
}
GitHub上的 @ferhanakkan 提供了以下 Demo。
1 | private lazy var pageControl: UIPageControl = { |
UICalendarView
之前一直希望 iOS 能够集成一个类似 FSCalendar 的日历组件,在 iOS 16上终于算是等到了。UICalendarView
是 UIDatePicker
的内联日历样式,它和 UIDatePicker
最主要的一个区别在于 UICalendarView
将日期表示为 NSDateComponents
而不是 NSDate
。
UICalendarView
现在作为一个独立的全功能组件形式提供,它具备了以下几种特性。
- 支持日期单选或多选
- 支持在范围内框选日期和禁用个别日期
- 支持对标注日期进行装饰
- 明确由哪个
NSCalendar
表示
接下来尝试构建一个多选日期日历。
- 创建一个
UICalendarView
对象,为其设置delegate
并指定其calendar
属性。如果需要采用农历,更改NSCalendar
的初始化即可(Calendar(identifier: .chinese)
) - 配置多日期选择需要创建一个
UICalendarSelectionMultiDate
对象,并为其selectedDates
属性进行赋值,再将此对象作为selectionBehavior
属性传给我们创建的UICalendarView
对象 - 通过
UICalendarViewDelegate
中的multidateselect: canSelectDate:
方法可以控制哪些日期可以被选择,如防止日历中选中单个日期。无法选择的日期会被置灰。
1 | // Configuring a calendar view with multi-date selection |
UICalendarViewDelegate
提供了 calendarView: decorationForDateComponents:
方法对日历中的日期进行装饰。
- 如果不需要装饰直接返回 nil 即可
- 可以通过简单的图像构造装饰日期
- 可以通过自定义视图装饰日期
- 自定义装饰视图不允许交互
1 | // Configuring Decorations |
导航栏提升
UINavigationItemStyle
新引入了 Browser
、Editor
两种导航样式,目前支持的UINavigationItemStyle
有以下几种
Navigator
:遵循传统的 push / pop 模型,如设置appBrowser
:使用历史记录或文件夹结构导航,如 Safari、系统文件appEditor
:导航栏中间为文件相关操作,标题则在最左边
1 | // Adopt the editor navigation style for the navigation item. |
Center Items
center items 是展示在导航栏中间的的控件组,能够提供对应用程序最重要功能的快速访问。
用户可以移动、移除或添加来定制导航栏的 center items。默认不展示的 center items 出现在自定义弹出窗口中,可以通过点击更多按钮中的 Customize Toolbar 菜单访问。
为了实现这一能力,应用程序需要为导航项的customizationIdentifier
属性提供一个字符串,UIKit 能够根据这个 identifier 自动保存和恢复用户自定义设定。
1 | /// Setting a non-nil customizationIdentifier enables customization and UIKit will automatically save & restore customizations based on this identifier. The identifier must be unique within the scope of the application. |
配置 center Items 需要为 navigationItem.centerItemGroups
属性赋值
- 若需创建用户无法移动或移除的 item,需要调用
UIBarButtonItem
的实例方法createFixedGroup()
- 若需创建可调整的
BarButtonItemGroup
- 拟定
customizationIdentifier
作为唯一标识 UIBarButtonItems
一次只能在一个UIBarButtonItemGroup
中- 将一个 bar button item添加到一个 group 中会将其从之前的任何 group 中移除
isInDefaultCustomization
属性设置默认是否出现在导航栏中BarButtonItemGroup
通常包含多个可以提供定制能力的UIAction
- 拟定
1 |
|
center items 针对 Mac Catalyst 的 NSToolbar 和 iPad 并排模式能够实现自动适配。
文件菜单
当用户点击导航项目的标题时会出现文件菜单,从上到下可以将其拆分成三部分
- Document header:包含文件名、文件类型、文件大小、分享菜单等
- Suggested title menu:与当前文档相关的建议操作
- Custom title menu:自定义操作
Document header
Document header 显示当前文档相关的信息,包括标题、文件类型和大小,还提供了分享或拖放文档的能力。
1 | @available(iOS 16.0, *) |
如果需要Document header相关能力,使用UIDocumentProperties
对象给navigationItem.documentProperties
属性赋值即可。
1 | let documentProperties = UIDocumentProperties(url: document.fileURL) |
Title Menu
配置 title menu 需要给 navigationItem.titleMenuProvider
返回菜单 closure。系统返回给closure了一组 suggested actions,包括移动和复制;应用定义了动作包括文档导出为 HTML 和 PDF。
1 | /// When non-nil, UIKit will generate suggestedActions and call this block to generate a menu that is displayed from the title. |
UINavigationItem
提供了对文件重命名的支持。
1 | public protocol UINavigationItemRenameDelegate : AnyObject { |
启用该能力需要遵循UINavigationItemRenameDelegate
,并使用 navigationItem.renameDelegate
绑定到 self。
1 | navigationItem.renameDelegate = self |
体验提升
如果你对这部分内容感兴趣可以移步此Session
Find and Replace
UITextView
、WKWebView
、PDFView
都支持了系统层级的查找与替换功能。
如果你在开发中的视图是上述 View 的子类,如editorTextView
是UITextView
的子类,启用查找和替换只需要一行代码。
1 | editorTextView.isFindInteractionEnabled = true |
UIEditMenu
Edit menu 交互菜单能够针对当前展示的内容提供诸如剪切、粘贴和粘贴等编辑动作。系统会针对当前用户的交互方式提供符合交互的菜单展示形式。
- 对于触摸交互,动作以 editing menu 的形式展示
- 对于指针的交互,动作以 context menu的形式展示
标准的 UIKit 类,如 UITextView
或者 UITextField
,已经预先支持了Edit Menu交互,你可以便捷地框选一个地址然后获得类似地图导航的交互菜单选项。
在通用视图中添加 Edit menu 交互菜单
- 创建一个 Edit menu 交互对象,并在默认 initializer 传入一个可选的 delegate。
- 在视图上调用
addInteraction(_:)
来添加交互对象 - 创建一个 gesture recognizer 来触发交互,并将其添加到视图中
以下苹果提供的例子中创建了一个由长按触发的 Edit menu 交互菜单。
1 | override func viewDidLoad() { |
- Edit menu 交互菜单包括标准编辑操作,包括剪贴、复制、删除等等,可参考
UIResponderStandardEditActions
。 - 可以使用
UIEditMenuInteractionDelegate
提供的方法向Edit menu 交互菜单添加额外项目。 - 对于文本视图可以使用
UITextViewDelegate
、UITextFieldDelegate
或UITextInput
中的方法为特定文本范围指定菜单显示的项目。
Sidebar
在 iOS 16 中,侧边栏会在 Slide-over(侧拉)模式下自动显示。UIKit 会管理一组私有视图。
SFSymbols
SFSymbols 在 iOS 16 上支持四种 renderingMode
,分别是
- monochrome
- multicolor
- hierarchical
- palette
在 iOS 15 及之前的版本中,默认使用 monochrome 渲染 symbol。
在 iOS 16 如果没有指定renderingMode,默认使用 hierarchical 渲染 symbol,可以通过 UIImage.SymbolConfiguration.preferringMonochrome()
指定渲染方式。
同时增加了对可变 symbol 的支持,即支持根据从 0 到 1 的值变化映射 symbol 的变化,当然,这需要 symbol 自身支持该能力。
假设 App 使用了speaker.3.wave.fill
符号表示当前音量级别,在值为 0 时扬声器波纹消失表示最低音量水平,当该值增加到 1 时扬声器的波形逐渐填充完整,表示音量水平提高。
使用方法也非常简单直白,通过标准的 SF Symbols API 为 UIImage 设置 variableValue
参数即可,甚至将该属性与 renderingMode
组合使用以进一步设计符号的样式。
以下为 GitHub上 @ferhanakkan 的 Demo。
1 | @objc private func sliderDidValueChange(_ sender: UISlider) { |
学习如何创建自定义变量符号可以观看 Adopt variable color in SF Symbols 和 What’s new in SF Symbols 4 这两个Session。
Swift Concurrency and Sendable
UIKit 现在可以与 Swift Concurrency 同时使用,包括 immutable 类型,如以下的类型遵循了 Sendable
UIImage
UIColor
UIFont
UITraitCollection
对象可以在 MainActor 和自定义 actor 之间发送而不会收到编译器警告。
苹果提供的例子中,有一个叫做 Processor 的自定义 actor,以及一个被绑定到 MainActor 的叫做 ImageViewer 的 ViewController。在 sendImageForProcessing
方法中 ImageViewer 将图像发送给 Processor进行处理,目前是安全的。
因为 UIImage 是 immutable 类型, Processor 必须创建新的拷贝来执行操作。任何引用原始图像的代码都不会显示这些修改,共享状态也不会发生不安全的变化。
对比一下因为 mutable 而没有遵循 Sendable 的 UIBezierPath,以前只能在文档中表示,现在可以由编译器进行检查。
要了解更多关于 Sendable 和 Swift Concurrency 移步视频 Eliminate data races using Swift Concurrency 和 Visualize and optimize Swift Concurrency。
UIKit and SwiftUI
过去如果想要实现将 SwiftUI 视图嵌入 UIView 视图,一般需要这么做。
1 | let controller = UIHostingController(rootView: SwiftUIView()) |
在 iOS 16上推出了 UIHostingConfiguration
,你可以在 UICollectionView
和 UITableView
中以 contentConfiguration
的方式将 SwiftUI 构建的 View 嵌入 cell。
1 | cell.contentConfiguration = UIHostingConfiguration { |
结束语
最近几年的 WWDC 中看到苹果对于 UIKit 的理解,倡导使用 configuration 的方式构建组件、菜单,一方面参照人机交互指南构建组件能够确保交互不被滥用,另一方面降低了开发者的接入成本。
作为开发者,一起尝试兼容 app 到 iOS 16,适配新的API,探索新的业务可能性吧。