Highlight
iOS 26 的 UIKit 把 Swift Observation 集成进了
layoutSubviews、updateProperties等更新方法,引用@Observable属性即自动建立依赖、自动失效视图,无需再手动调setNeedsLayout。
核心内容
UIKit 老开发者都熟悉这套流程:模型变了,调 setNeedsLayout,再在 layoutSubviews 里读模型刷新视图;写动画时还得在 closure 里手动 layoutIfNeeded(),否则约束变化不会跟着动起来。这套依赖关系全靠人维护,写漏一处就出现少更新或多更新,UICollectionView cell 配置里尤其容易踩坑。
iOS 26 的 UIKit 把 Swift Observation 直接焊在了核心更新流程里。layoutSubviews、updateProperties、cell 的 configurationUpdateHandler 等方法只要读到 @Observable 对象的属性,UIKit 会自动建立依赖、在属性变化时精准失效对应视图。配套新增的 updateProperties 方法把”内容/样式更新”从”布局计算”中拆出来——前者跑在 layoutSubviews 之前、互不依赖,避免无关事件触发整个布局。动画也补上了 flushUpdates 选项,UIKit 会在动画开始前自动 flush 所有待处理更新,不用再写 layoutIfNeeded()。这套组合拳让 UIKit 写起来更接近 SwiftUI 的声明式心智,但保留了原有的视图树和性能模型。
除此之外,iPadOS 26 把 macOS 菜单栏带到了 iPad,从顶部下滑即可呼出;UISplitViewController 原生支持 inspector 列;SwiftUI scene 可以通过 UIHostingSceneDelegate 嵌入到 UIKit app;UIColor 支持 HDR;通知系统升级为强类型 NotificationCenter.Message;SF Symbols 7 引入 draw 动画。还有一条硬性变化:iOS 26 之后版本用最新 SDK 构建的 app,必须迁移到 UIScene 生命周期才能启动。
详细内容
自动观察追踪:@Observable 即用即追踪
UIKit 现在会在 layoutSubviews、updateProperties 等方法里自动追踪你读到的 @Observable 属性(10:54)。
// Using an Observable object and automatic observation tracking
@Observable class UnreadMessagesModel {
var showStatus: Bool
var statusText: String
}
class MessageListViewController: UIViewController {
var unreadMessagesModel: UnreadMessagesModel
var statusLabel: UILabel
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
statusLabel.alpha = unreadMessagesModel.showStatus ? 1.0 : 0.0
statusLabel.text = unreadMessagesModel.statusText
}
}
关键点:
UnreadMessagesModel标记@Observable,两个属性自动具备观察能力。viewWillLayoutSubviews中读showStatus和statusText,UIKit 自动记录依赖。- 后续这两个属性任意一个被改写,UIKit 会自动 invalidate 该视图并重跑
viewWillLayoutSubviews。 - 不需要 KVO、不需要 Combine、不需要手写
setNeedsLayout。 - iOS 18 上可通过在
Info.plist加UIObservationTrackingEnabled反向部署。
updateProperties:内容更新和布局解耦
新方法 updateProperties 在 layoutSubviews 之前执行,专门用来配置内容、样式、行为(13:27)。
// Using automatic observation tracking and updateProperties()
@Observable class BadgeModel {
var badgeCount: Int?
}
class MyViewController: UIViewController {
var model: BadgeModel
let folderButton: UIBarButtonItem
override func updateProperties() {
super.updateProperties()
if let badgeCount = model.badgeCount {
folderButton.badge = .count(badgeCount)
} else {
folderButton.badge = nil
}
}
}
关键点:
updateProperties在 trait 更新之后、layoutSubviews之前执行。- 这里只配置 badge 内容,
badgeCount变化时只会重跑这段逻辑,不会触发整个布局 pass。 - 可以读 trait collection(已经更新过),也可以在内部 invalidate 布局,UIKit 紧接着会执行布局。
- 手动触发用
setNeedsUpdateProperties。 - 它和
layoutSubviews互补,不替代——内容/样式放这里,几何计算放layoutSubviews。
flushUpdates:动画里自动 flush 更新
iOS 26 给 UIView 动画加了 .flushUpdates 选项,自动在动画开始前后应用待处理的失效(16:57)。
// Using the flushUpdates animation option to automatically animate updates
// Automatically animate changes with Observable objects
UIView.animate(options: .flushUpdates) {
model.badgeColor = .red
}
关键点:
- closure 里只改
@Observable属性,不调用layoutIfNeeded()。 - UIKit 在动画开始前 flush 一次更新,结束时再 flush 一次。
- 依赖该属性的视图会被自动捕获到动画里。
- 同样适用于 Auto Layout 约束变更:在 closure 内修改
constant或isActive,依赖视图自动平滑动画到新位置。
主菜单系统配置
iPadOS 26 引入菜单栏,UIMenuBuilder 之外新增 UIMainMenuSystem.Configuration,用于声明性地裁剪默认命令(4:56)。
// Main menu system configuration
var config = UIMainMenuSystem.Configuration()
// Declare support for default commands, like printing
config.printingPreference = .included
// Opt out of default commands, like inspector
config.inspectorPreference = .removed
// Configure the Find commands to be a single "Search" element
config.findingConfiguration.style = .search
关键点:
printingPreference = .included让 app 接入系统打印命令套件。inspectorPreference = .removed关闭新加入的 inspector 切换命令。findingConfiguration.style = .search把 Find 命令族折叠成单个 Search——对图片/音乐类 app 比 Find Next/Previous 更合适。- 应在
application(_:didFinishLaunchingWithOptions:)里调用一次,因为设置 configuration 会触发一次菜单重建。 - 提示:iOS 26 起,storyboard 中的菜单不再加载,必须用代码构建。
Focus-based deferred menu element
History 这类菜单内容依赖当前焦点(比如哪个浏览 profile),不能写死在主菜单 build 里(8:06)。
// Focus-based deferred menu elements
class BrowserViewController: UIViewController {
// ...
override func provider(
for deferredElement: UIDeferredMenuElement
) -> UIDeferredMenuElement.Provider? {
if deferredElement.identifier == .browserHistory {
return UIDeferredMenuElement.Provider { completion in
let browserHistoryMenuElements = profile.browserHistoryElements()
completion(browserHistoryMenuElements)
}
}
return nil
}
}
关键点:
- 在 App Delegate 用
UIDeferredMenuElement.usingFocus(identifier:shouldCacheItems:)创建占位元素并插入主菜单。 - View controller 重写
provider(for:),按 identifier 返回UIDeferredMenuElement.Provider,UIKit 沿响应链向上找到第一个能提供内容的 responder。 - 优势:菜单填充按需进行,不需要因 profile 切换就重建整张主菜单。
把 SwiftUI scene 嵌进 UIKit app
UIHostingSceneDelegate 让 UIKit app 可以承载 SwiftUI 的 WindowGroup 和 visionOS 的 ImmersiveSpace(18:07)。
// Setting up a UIHostingSceneDelegate
import UIKit
import SwiftUI
class ZenGardenSceneDelegate: UIResponder, UIHostingSceneDelegate {
static var rootScene: some Scene {
WindowGroup(id: "zengarden") {
ZenGardenView()
}
#if os(visionOS)
ImmersiveSpace(id: "zengardenspace") {
ZenGardenSpace()
}
.immersionStyle(selection: .constant(.full),
in: .mixed, .progressive, .full)
#endif
}
}
关键点:
rootScene是 SwiftUI 的Scene,可包含多个 WindowGroup / ImmersiveSpace。- 在
application(_:configurationForConnecting:options:)中把delegateClass设为这个类即可挂上。 - 用
UISceneSessionActivationRequest(hostingDelegateClass:id:)程序化请求特定 scene,例如打开 visionOS 沉浸空间。 - 适合 UIKit app 渐进式接入 SwiftUI,也适合需要 visionOS 沉浸空间的场景。
强类型 NotificationCenter.Message
NotificationCenter.default.addObserver(of:for:) 现在返回强类型 message,告别 userInfo 字典(20:54)。
// Adopting Swift notifications
override func viewDidLoad() {
super.viewDidLoad()
let keyboardObserver = NotificationCenter.default.addObserver(
of: UIScreen.self
for: .keyboardWillShow
) { message in
UIView.animate(
withDuration: message.animationDuration, delay: 0, options: .flushUpdates
) {
// Use message.endFrame to animate the layout of views with the keyboard
let keyboardOverlap = view.bounds.maxY - message.endFrame.minY
bottomConstraint.constant = keyboardOverlap
}
}
}
关键点:
for: .keyboardWillShow是强类型NotificationCenter.Message名。message.animationDuration、message.endFrame直接拿到键盘动画时长和终止 frame,不需要userInfo[UIResponder.keyboardFrameEndUserInfoKey]那套字典查找+强转。- 配合
.flushUpdates让约束修改自动跟着键盘动画。
核心启发
1. 重构 cell 配置到 configurationUpdateHandler + @Observable
为什么值得做:现有 cell 配置代码常常分散在 cellForItemAt、prepareForReuse、KVO 回调里,状态同步极易出错。configurationUpdateHandler 已经支持自动观察追踪,可一处定义、自动更新(11:48)。
怎么开始:把 cell 的数据模型改成 @Observable class,在 cellForItemAt 里 dequeue 后赋值 configurationUpdateHandler,handler 内部读模型属性配置 UIListContentConfiguration。模型属性变更会自动触发 cell 重配,不用手动调 setNeedsConfigurationUpdate。
2. 把”内容/样式”逻辑从 layoutSubviews 搬到 updateProperties
为什么值得做:layoutSubviews 在每次 resize、滚动、subview 变化时都会跑一遍,把 badge、文本、颜色等内容更新混在里面会做大量无效计算。新的 updateProperties 只在内容相关失效时触发,性能更好且职责清晰。
怎么开始:审视现有 viewWillLayoutSubviews/layoutSubviews 实现,把所有”读模型→设属性”的代码挪到 override func updateProperties()。布局计算(frame、约束 constant)保留在原方法。需要手动触发时调 setNeedsUpdateProperties()。
3. 现在就启动 UIScene 迁移工作
为什么值得做:iOS 26 之后版本用最新 SDK 构建的 app,未迁移到 UIScene 生命周期将无法启动;很多 UIApplicationDelegate 老回调和 UIWindow 初始化方法已 deprecated(21:19)。
怎么开始:阅读 Apple 技术说明 Migrating to the UIKit scene-based life cycle,从 Info.plist 添加 UIApplicationSceneManifest 开始;即便是单窗口 app 也要做迁移;把 application(_:didFinishLaunchingWithOptions:) 中的 UI 准备工作搬到 UISceneDelegate.scene(_:willConnectTo:options:);UIWindow 改用 init(windowScene:)。
4. 给键盘/通知响应代码升级到强类型 NotificationCenter.Message
为什么值得做:传统 keyboardWillShowNotification + userInfo 字典查找的代码可读性差,key 名易写错且需要强转。强类型 message 直接给出 animationDuration、endFrame 等属性,编译期检查。
怎么开始:把 addObserver(forName:object:queue:using:) 替换为 addObserver(of:for:),回调签名改为 (message) in,直接通过 message.xxx 读取属性;动画 closure 加 .flushUpdates,约束改动自动跟随键盘动画。
关联 Session
- Build a UIKit app with the new design — 把现有 UIKit app 升级到 Liquid Glass 新设计的实操指南
- Make your UIKit app more flexible — 容器 view controller、layout margin、迁移 UIRequiresFullScreen
- Elevate the design of your iPad app — iPadOS 菜单栏与新设计的设计层指南
- What’s new in SF Symbols 7 — 详解 draw 动画、variable draw、gradient 渲染模式
- Take your iPad apps to the next level — 主菜单与多任务方面的全景介绍
评论
GitHub Issues · utterances