Highlight
iOS 16 为 UIKit 增加了桌面级导航栏、
UICalendarView、UIPasteControl、自定义 sheet detent、self-resizing cells 和UIHostingConfiguration,让 iPad、iPhone 与 Mac Catalyst app 可以用系统 API 完成更多生产力界面。
核心内容
UIKit 在 iOS 16 的主题很明确:把很多以前需要自定义控件、手写布局或平台分支的工作,收进系统 API。
文档类 app 是第一类受益者。导航栏增加 browser 和 editor 两种样式,支持可自定义的中央工具项、自动溢出菜单、标题菜单,以及 Mac Catalyst 中与 NSToolbar 的自动集成(01:22)。
文本编辑也更接近桌面体验。内建 UITextView 和 WKWebView 只需要打开一个 flag,就可以启用 find and replace;旧的 UIMenuController 被 UIEditMenuInteraction 取代,用同一套机制覆盖触摸菜单和指针上下文菜单(03:29)。
控件层面,Apple 把 inline calendar 从 UIDatePicker 中拆成独立的 UICalendarView。它支持单选、多选、禁用单个日期和日期装饰。UIPageControl 也支持自定义当前页与非当前页的指示器图片,并可以配置方向(06:13)。
还有两类变化会影响日常代码。UISheetPresentationController 支持自定义 detent,cell 在内容变化后可以自动重新计算尺寸,UIKit cell 还可以直接用 UIHostingConfiguration 写 SwiftUI 内容(12:03、19:05、21:25)。
详细内容
用 UICalendarView 做可多选的日历
UICalendarView 使用日期组件 DateComponents 表示日期,避免把日历日期误当成时间点 Date。这是一个重要差别。日历里的“6 月 10 日”需要结合具体 calendar 才有意义。因此,session 特别提醒:如果你的业务需要 Gregorian calendar,要显式设置(07:00)。
// Configuring a calendar view with multi-date selection
let calendarView = UICalendarView()
calendarView.delegate = self
calendarView.calendar = Calendar(identifier: .gregorian)
view.addSubview(calendarView)
let multiDateSelection = UICalendarSelectionMultiDate(delegate: self)
multiDateSelection.selectedDates = myDatabase.selectedDates()
calendarView.selectionBehavior = multiDateSelection
func multiDateSelection(
_ selection: UICalendarSelectionMultiDate,
canSelectDate dateComponents: DateComponents
) -> Bool {
return myDatabase.hasAvailabilities(for: dateComponents)
}
关键点:
UICalendarView()创建独立日历控件,不再把 inline calendar 绑在UIDatePicker上。calendarView.delegate = self让当前对象负责提供日期装饰等回调。calendarView.calendar = Calendar(identifier: .gregorian)明确使用 Gregorian calendar,避免依赖用户当前 calendar 类型。UICalendarSelectionMultiDate(delegate: self)创建多日期选择行为。selectedDates从数据模型恢复已选日期,让日历初始状态与业务数据一致。calendarView.selectionBehavior = multiDateSelection把选择规则挂到日历控件上。canSelectDate返回false时,该日期会显示为灰色,用户不能选择。
给日历日期加装饰
日历不只负责选择日期。它还可以把某一天的状态显示出来,比如忙碌、出行、聚会。做法是实现 calendarView(_:decorationFor:),根据数据模型返回不同的 UICalendarView.Decoration(09:07)。
// Configuring Decorations
func calendarView(
_ calendarView: UICalendarView,
decorationFor dateComponents: DateComponents
) -> UICalendarView.Decoration? {
switch myDatabase.eventType(on: dateComponents) {
case .none:
return nil
case .busy:
return .default()
case .travel:
return .image(airplaneImage, color: .systemOrange)
case .party:
return .customView {
MyPartyEmojiLabel()
}
}
}
关键点:
decorationFor dateComponents使用日期组件查询业务状态。return nil表示这一天没有装饰。.default()显示系统默认的灰色圆点。.image(airplaneImage, color: .systemOrange)用图片和颜色表达出行状态。.customView { MyPartyEmojiLabel() }为特殊状态提供自定义视图。- transcript 明确说明,自定义装饰视图不能交互,并且会被裁剪到可用空间内。
自定义 UIPageControl 的方向和指示器
UIPageControl 在 iOS 16 支持两个以前常见的自定义需求:竖向页面指示器,以及当前页和非当前页使用不同图片(09:53)。
// Vertical page control with custom indicators
pageControl.direction = .topToBottom
pageControl.preferredIndicatorImage = UIImage(systemNamed: "square")
pageControl.preferredCurrentIndicatorImage = UIImage(systemNamed: "square.fill")
关键点:
direction = .topToBottom把 page control 改成从上到下排列。preferredIndicatorImage设置普通页面的指示器图片。preferredCurrentIndicatorImage设置当前页面的指示器图片。- 示例使用 SF Symbols 的
square和square.fill,不需要自己画点或维护选中状态。
用自定义 detent 控制 sheet 高度
iOS 15 引入 sheet detent 后,开发者可以在系统的中等和大尺寸之间切换。iOS 16 增加 .custom detent,可以直接返回固定高度,或基于最大高度计算比例(12:03)。
// Create a custom detent
sheet.detents = [
.large(),
.custom { _ in
200.0
}
]
关键点:
sheet.detents定义 sheet 可以停靠的高度集合。.large()保留系统的大尺寸状态。.custom { _ in 200.0 }增加一个 200 point 高的自定义状态。- session 提醒,闭包返回值不需要计算 bottom safe area inset,系统会处理 floating 和 edge-attached sheet 的差异。
如果高度需要跟容器空间相关,可以读取 context.maximumDetentValue(12:38)。
// Create a custom detent
sheet.detents = [
.large(),
.custom { context in
0.3 * context.maximumDetentValue
}
]
关键点:
context提供当前环境下的最大 detent 高度。0.3 * context.maximumDetentValue表示自定义 detent 使用最大高度的 30%。- 这种写法比固定 point 更适合横竖屏切换和不同设备尺寸。
当 detent 需要被其他 API 引用时,先定义 identifier,再把它传给 .custom(12:42)。
// Define a custom identifier
extension UISheetPresentationController.Detent.Identifier {
static let small = UISheetPresentationController.Detent.Identifier("small")
}
// Assign identifier to custom detent
sheet.detents = [
.large(),
.custom (identifier: .small) { context in
0.3 * context.maximumDetentValue
}
]
// Disable dimming above the custom detent
sheet.largestUndimmedDetentIdentifier = .small
关键点:
- 扩展
UISheetPresentationController.Detent.Identifier,给自定义 detent 一个稳定名称。 .custom(identifier: .small)创建可被引用的自定义 detent。largestUndimmedDetentIdentifier = .small让 sheet 停在 small 及以下状态时不遮罩后面的内容。
让 cell 随内容变化自动调整高度
以前,动态高度 cell 的麻烦在更新之后。内容变了,cell 不一定立即重新计算高度。iOS 16 中,UICollectionView 和 UITableView 的 cell 支持 self-resizing:可见 cell 内部内容变化时,容器可以自动触发尺寸更新(19:05)。
这部分 session 没有给出完整代码片段,但明确给出了 API 入口:
collectionView.selfSizingInvalidation = .enabled
cell.contentView.invalidateIntrinsicContentSize()
关键点:
selfSizingInvalidation是UICollectionView和UITableView上的新属性,用来控制 cell 内容变化后的尺寸失效行为。- 使用
UIListContentConfiguration配置 cell 时,configuration 变化会自动触发 invalidation。 - 其他场景可以在 cell 或
contentView上调用invalidateIntrinsicContentSize()。 - 如果使用 Auto Layout,可以选择
enabledIncludingConstraints,让 content view 内部约束变化也触发尺寸更新。 - 需要无动画更新时,把 invalidation 调用包在
performWithoutAnimation中。
在 UIKit cell 里直接写 SwiftUI
UIKit 和 SwiftUI 混用,以前常见做法是塞一个 hosting controller 或 hosting view。iOS 16 增加 UIHostingConfiguration,让 collection view 和 table view cell 可以直接用 SwiftUI 描述内容(21:43)。
cell.contentConfiguration = UIHostingConfiguration {
VStack {
Image(systemName: "wand.and.stars")
.font(.title)
Text("Like magic!")
.font(.title2).bold()
}
.foregroundStyle(Color.purple)
}
关键点:
cell.contentConfiguration = UIHostingConfiguration { ... }把 SwiftUI 内容作为 cell 的 content configuration。VStack负责垂直排列图标和文字。Image(systemName: "wand.and.stars")使用 SF Symbol。.font(.title)设置图标字号。Text("Like magic!")显示文字。.font(.title2).bold()设置文字样式。.foregroundStyle(Color.purple)给整个 SwiftUI 内容设置紫色前景样式。- transcript 强调,这种方式不需要额外 view 或 view controller。
核心启发
-
做什么:给文档编辑 app 加一个桌面级标题菜单。 为什么值得做:iOS 16 的 navigation title menu 可以自动显示 duplicate、move、rename、export、print 等标准项。 怎么开始:使用 browser 或 editor navigation style,并实现对应 delegate 方法;需要额外命令时再添加自定义菜单项。
-
做什么:做一个可选择多个可预约日期的排期界面。 为什么值得做:
UICalendarView支持多选、禁用单个日期和日期装饰。 怎么开始:创建UICalendarSelectionMultiDate,用canSelectDate过滤无库存日期,再用decorationFor标记忙碌、出行或特殊活动。 -
做什么:把底部 sheet 做成 30% 高度的轻量控制面板。 为什么值得做:自定义 detent 可以按最大高度计算,不再局限于系统的 medium 和 large。 怎么开始:用
.custom { context in 0.3 * context.maximumDetentValue }创建 detent,并在需要时给它定义 identifier。 -
做什么:让评论、富文本或设置项 cell 在内容变化后自动变高。 为什么值得做:iOS 16 的 self-resizing cells 会合并尺寸失效请求,并在合适时机更新 table view 或 collection view。 怎么开始:检查
selfSizingInvalidation,使用UIListContentConfiguration时优先依赖自动 invalidation;自定义内容变化时调用invalidateIntrinsicContentSize()。 -
做什么:在已有 UIKit 列表中逐步引入 SwiftUI cell。 为什么值得做:
UIHostingConfiguration可以直接作为 cell 的 content configuration,不需要额外 hosting controller。 怎么开始:从一个视觉独立的 cell 开始,把cell.contentConfiguration替换为UIHostingConfiguration { ... }。
关联 Session
- Build a desktop-class iPad app — 继续把本 session 提到的 navigation bar、toolbar 和文档工作流落到完整 iPad app。
- Adopt desktop-class editing interactions — 深入讲解
UIEditMenuInteraction、find interaction 和桌面级编辑体验。 - Bring multiple windows to your SwiftUI app — 补充 SwiftUI app 在 iPadOS 上的多窗口能力。
- Adopt Variable Color in SF Symbols — 进一步了解 SF Symbols 在 iOS 16 中的变量颜色与渲染方式更新。
评论
GitHub Issues · utterances