What's new in UIKit
UIKit 进阶 35m

UIKit 新特性

What's new in UIKit

2025年6月9日

在 Apple 官方观看视频

一句话判断

UIKit 终于在 layoutSubviews() 里原生支持了 @Observable,这比 Liquid Glass 的视觉更新重要得多——它意味着你终于可以把项目里那些散落各处的 KVO(键值观察)和 NotificationCenter(通知中心)监听一次性干掉了。

这场 Session 讲了什么

UIKit 在 iOS 19 里最实质的变化是数据驱动范式的迁移。@Observable 对象的属性在 layoutSubviews() 中被访问时,UIKit 会自动追踪依赖关系并在属性变化时触发重新布局。这把 UIKit 从”命令式通知 UI 更新”拽到了”声明式描述依赖”的轨道上,和 SwiftUI 的数据流模型终于对齐了。对存量项目来说,这是一个可以逐步推进的迁移路径,不需要一次性重写。

视觉层面,Liquid Glass 的处理策略是”系统控件自动适配,自定义控件手动接入”。重新编译后导航栏、标签栏、工具栏自动获得新外观;自定义视图通过 UIGlassEffectUIGlassContainerEffect 接入。iPadOS 这边则引入了完整的菜单栏系统,用户从屏幕顶部下滑就能唤出应用菜单,UIMainMenuSystem 负责配置,对键盘重度用户是实打实的效率提升。

值得深挖的点

@Observable 的依赖追踪到底在哪个粒度生效

UIKit 对 @Observable 的集成点是 layoutSubviews(),而不是 viewDidLoad()viewWillAppear()。这个选择本身就说明了 Apple 的设计意图:追踪粒度是”单次布局周期内访问了哪些属性”。假设你的 @Observable 对象有 20 个属性,但这次 layoutSubviews() 调用只读了 nameavatarURL,那么只有这两个属性的变化会触发下一次布局。这是属性级别的精准追踪,和 SwiftUI 的 body 计算属性追踪机制是同一套。

但这带来一个容易踩的坑:如果你在 layoutSubviews() 里调用了某个方法,而这个方法内部访问了 @Observable 对象的属性,依赖追踪仍然会生效。这意味着你的依赖图可能比你预期的更大。和旧方案对比,KVO 需要你手动 addObserver 每个属性,NotificationCenter 更是完全没有自动追踪——@Observable 的声明式依赖追踪在准确性和便利性上都碾压了旧方案。代价是你要把 NSObject 子类换成 @Observable 宏标记的类,这对有大量 NSObject 依赖的代码库来说是个不小的工程量。

Liquid Glass 的接入策略和潜在冲突

Apple 对 Liquid Glass 的接入策略很务实:系统控件零代码适配,自定义控件提供 UIGlassEffect API。UIGlassEffect 上的 isInteractive 属性让玻璃效果能响应按压等交互,UIGlassContainerEffect 则负责把多个子视图组织在一起实现跨视图的变形动画。按钮层面提供了 glass()prominentGlass() 两种样式。

但这里有一个容易忽略的问题:如果你的项目对导航栏、标签栏做过自定义背景(比如 UINavigationBarAppearancebackgroundImage),重新编译后这些自定义可能会和 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 颜色选取器原生支持和 UITraitHDRHeadroomUsageLimit trait 的组合,让 HDR 内容在失去焦点时自动降级功耗,对展示 HDR 照片的应用很有意义。
  • @Observable 在 UIKit 中的原生支持可能加速 Combine 在 UIKit 层的退场,如果你的新项目还在用 Combine 做 UI 绑定,值得重新评估。
UIKit Liquid Glass iPadOS Observable HDR