UIKit 新特性
What's new in UIKit
2025年6月9日
一句话判断
UIKit 终于在 layoutSubviews() 里原生支持了 @Observable,这比 Liquid Glass 的视觉更新重要得多——它意味着你终于可以把项目里那些散落各处的 KVO(键值观察)和 NotificationCenter(通知中心)监听一次性干掉了。
这场 Session 讲了什么
UIKit 在 iOS 19 里最实质的变化是数据驱动范式的迁移。@Observable 对象的属性在 layoutSubviews() 中被访问时,UIKit 会自动追踪依赖关系并在属性变化时触发重新布局。这把 UIKit 从”命令式通知 UI 更新”拽到了”声明式描述依赖”的轨道上,和 SwiftUI 的数据流模型终于对齐了。对存量项目来说,这是一个可以逐步推进的迁移路径,不需要一次性重写。
视觉层面,Liquid Glass 的处理策略是”系统控件自动适配,自定义控件手动接入”。重新编译后导航栏、标签栏、工具栏自动获得新外观;自定义视图通过 UIGlassEffect 和 UIGlassContainerEffect 接入。iPadOS 这边则引入了完整的菜单栏系统,用户从屏幕顶部下滑就能唤出应用菜单,UIMainMenuSystem 负责配置,对键盘重度用户是实打实的效率提升。
值得深挖的点
@Observable 的依赖追踪到底在哪个粒度生效
UIKit 对 @Observable 的集成点是 layoutSubviews(),而不是 viewDidLoad() 或 viewWillAppear()。这个选择本身就说明了 Apple 的设计意图:追踪粒度是”单次布局周期内访问了哪些属性”。假设你的 @Observable 对象有 20 个属性,但这次 layoutSubviews() 调用只读了 name 和 avatarURL,那么只有这两个属性的变化会触发下一次布局。这是属性级别的精准追踪,和 SwiftUI 的 body 计算属性追踪机制是同一套。
但这带来一个容易踩的坑:如果你在 layoutSubviews() 里调用了某个方法,而这个方法内部访问了 @Observable 对象的属性,依赖追踪仍然会生效。这意味着你的依赖图可能比你预期的更大。和旧方案对比,KVO 需要你手动 addObserver 每个属性,NotificationCenter 更是完全没有自动追踪——@Observable 的声明式依赖追踪在准确性和便利性上都碾压了旧方案。代价是你要把 NSObject 子类换成 @Observable 宏标记的类,这对有大量 NSObject 依赖的代码库来说是个不小的工程量。
Liquid Glass 的接入策略和潜在冲突
Apple 对 Liquid Glass 的接入策略很务实:系统控件零代码适配,自定义控件提供 UIGlassEffect API。UIGlassEffect 上的 isInteractive 属性让玻璃效果能响应按压等交互,UIGlassContainerEffect 则负责把多个子视图组织在一起实现跨视图的变形动画。按钮层面提供了 glass() 和 prominentGlass() 两种样式。
但这里有一个容易忽略的问题:如果你的项目对导航栏、标签栏做过自定义背景(比如 UINavigationBarAppearance 的 backgroundImage),重新编译后这些自定义可能会和 Liquid Glass 的自动适配冲突。这不是 API 层面的 breaking change,而是视觉层面的——你的自定义背景会遮盖住系统自动应用的玻璃效果。迁移时需要逐一检查这些自定义 appearance 设置,判断是否还需要保留。
代码片段
// 用 @Observable 驱动 UIKit 自动刷新
// 场景:用户资料页,数据变化时 UI 自动更新
@Observable
class UserViewModel {
var name: String = "未登录"
var avatarURL: URL?
}
class ProfileViewController: UIViewController {
let viewModel = UserViewModel()
override func layoutSubviews() {
super.layoutSubviews()
// 访问的属性会被自动追踪,变化时系统重新调用 layoutSubviews()
nameLabel.text = viewModel.name
avatarImageView.loadImage(from: viewModel.avatarURL)
}
}
// 坑:如果 layoutSubviews() 里调用了其他方法,那些方法里访问的属性也会被追踪,依赖图可能比你预期的大
// 为自定义视图添加 Liquid Glass 效果
// 场景:自定义卡片需要融入系统新设计语言
let glassView = UIView()
let glassEffect = UIGlassEffect()
glassEffect.isInteractive = true // 按压时有缩放反馈
glassView.addEffect(glassEffect)
// 坑:玻璃效果需要背景内容才能体现材质感,底下没东西就只是一块半透明色块
// iPadOS 菜单栏配置
// 场景:为 iPad 应用添加菜单系统和键盘快捷键
let menuSystem = UIMainMenuSystem.main
let fileMenu = UIMenu(title: "文件", children: [
UIKeyCommand(title: "新建", action: #selector(newFile), input: "N", modifierFlags: .command),
UIKeyCommand(title: "打开", action: #selector(openFile), input: "O", modifierFlags: .command),
])
menuSystem.menu = UIMenu(children: [fileMenu])
menuSystem.setNeedsRebuild()
// 坑:菜单项的 action 必须有对应的 responder chain 处理者,否则点了没反应
最佳实践
建议先花半天时间把项目里最核心的 2-3 个数据模型从 NSObject + KVO 迁移到 @Observable。不用全面铺开,先挑一个页面验证流程。layoutSubviews() 的自动追踪意味着不需要手动 addObserver,但需要确保所有 UI 更新逻辑都在 layoutSubviews() 里,而不是散落在各种生命周期回调中。
Liquid Glass 方面,不建议急着做自定义视图的玻璃效果接入——系统控件的自动适配已经覆盖了大部分场景。先重新编译跑一遍,检查哪些地方和自定义 appearance 冲突,把冲突的自定义背景清理掉。自定义视图的 UIGlassEffect 留到真正需要那个视觉效果时再加。
iPadOS 菜单栏的优先级最低,除非 iPad 应用已经有大量键盘快捷键在用。UIMainMenuSystem 的配置不复杂,但菜单结构的设计需要花心思——不是把所有功能塞进去就好。
还有什么值得关注
UIBarButtonItem新增badge属性,一行代码就能给导航栏按钮加未读数角标,如果你之前是自定义角标视图,现在可以换掉了。- HDR 颜色选取器原生支持和
UITraitHDRHeadroomUsageLimittrait 的组合,让 HDR 内容在失去焦点时自动降级功耗,对展示 HDR 照片的应用很有意义。 @Observable在 UIKit 中的原生支持可能加速 Combine 在 UIKit 层的退场,如果你的新项目还在用 Combine 做 UI 绑定,值得重新评估。