WWDC Quick Look 💓 By SwiftGGTeam
What’s new in UIKit

What’s new in UIKit

观看原视频

Highlight

iOS 26 的 UIKit 把 Swift Observation 集成进了 layoutSubviewsupdateProperties 等更新方法,引用 @Observable 属性即自动建立依赖、自动失效视图,无需再手动调 setNeedsLayout


核心内容

UIKit 老开发者都熟悉这套流程:模型变了,调 setNeedsLayout,再在 layoutSubviews 里读模型刷新视图;写动画时还得在 closure 里手动 layoutIfNeeded(),否则约束变化不会跟着动起来。这套依赖关系全靠人维护,写漏一处就出现少更新或多更新,UICollectionView cell 配置里尤其容易踩坑。

iOS 26 的 UIKit 把 Swift Observation 直接焊在了核心更新流程里。layoutSubviewsupdateProperties、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 现在会在 layoutSubviewsupdateProperties 等方法里自动追踪你读到的 @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 中读 showStatusstatusText,UIKit 自动记录依赖。
  • 后续这两个属性任意一个被改写,UIKit 会自动 invalidate 该视图并重跑 viewWillLayoutSubviews
  • 不需要 KVO、不需要 Combine、不需要手写 setNeedsLayout
  • iOS 18 上可通过在 Info.plistUIObservationTrackingEnabled 反向部署。

updateProperties:内容更新和布局解耦

新方法 updatePropertieslayoutSubviews 之前执行,专门用来配置内容、样式、行为(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 内修改 constantisActive,依赖视图自动平滑动画到新位置。

主菜单系统配置

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 的 ImmersiveSpace18: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.animationDurationmessage.endFrame 直接拿到键盘动画时长和终止 frame,不需要 userInfo[UIResponder.keyboardFrameEndUserInfoKey] 那套字典查找+强转。
  • 配合 .flushUpdates 让约束修改自动跟着键盘动画。

核心启发

1. 重构 cell 配置到 configurationUpdateHandler + @Observable

为什么值得做:现有 cell 配置代码常常分散在 cellForItemAtprepareForReuse、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 直接给出 animationDurationendFrame 等属性,编译期检查。

怎么开始:把 addObserver(forName:object:queue:using:) 替换为 addObserver(of:for:),回调签名改为 (message) in,直接通过 message.xxx 读取属性;动画 closure 加 .flushUpdates,约束改动自动跟随键盘动画。


关联 Session

评论

GitHub Issues · utterances