WWDC Quick Look 💓 By SwiftGGTeam
Make your UIKit app more flexible

Make your UIKit app more flexible

观看原视频

Highlight

三个层次构建灵活的 UIKit 应用。

核心内容

很多 UIKit 老项目都还在用 AppDelegate 生命周期。一开始这套设计够用:一个 App 一个窗口,启动、进入后台、退出,三件事处理完就行。但是 iPad 多窗口、Mac Catalyst、AirPlay 外接显示、visionOS 多场景出现之后,AppDelegate 那套全局状态就开始打架——窗口 A 进后台时,窗口 B 还在前台,timer 该不该暂停?外接显示器接上时,要不要弹出主界面?这些问题在 AppDelegate 里都得自己写胶水代码。

iOS 26 之后的下一个 major 版本,UIScene 生命周期将成为用最新 SDK 构建的强制要求(02:19)。Apple 在这个 Session 里把灵活性拆成三层:底层是 Scene,每个 Scene 是 App UI 的独立实例,有自己的视图控制器、状态保存、窗口几何信息;中间层是容器视图控制器,UISplitViewController 和 UITabBarController 负责跨 iPhone/iPad/Mac/visionOS 的导航适配;顶层是自适应 API,Safe Area、Layout Margins Guide、窗口控制按钮的避让,让你的 UI 在任何尺寸下都不被遮挡。

详细内容

Scene 配置由 AppDelegate 决定03:02)。一个 App 可以有多种 Scene 类型,比如普通主界面和 AirPlay 外接显示。AppDelegate 在 configurationForConnectingSceneSession 里根据 sessionRole 返回不同配置:

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     configurationForConnecting sceneSession: UISceneSession,
                     options: UIScene.ConnectionOptions) -> UISceneConfiguration {

        if sceneSession.role == .windowExternalDisplayNonInteractive {
            return UISceneConfiguration(name: "Timer Scene",
                                        sessionRole: sceneSession.role)
        } else {
            return UISceneConfiguration(name: "Main Scene",
                                        sessionRole: sceneSession.role)
        }
    }
}

关键点:

  • sceneSession.role 区分场景用途,.windowExternalDisplayNonInteractive 表示 AirPlay 这类只读外接显示。
  • 返回的 UISceneConfiguration 名字必须和 Info.plist 里声明的 Scene 配置匹配。
  • 同一个 App 可以注册多份配置,按 role 路由。

Scene 的窗口由 SceneDelegate 创建03:30)。在 sceneWillConnectTo 里关联窗口和 Scene,并把模型注入到根视图控制器:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    var timerModel = TimerModel()

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {

        let windowScene = scene as! UIWindowScene
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = TimerViewController(model: timerModel)
        window.makeKeyAndVisible()
        self.window = window
    }
}

关键点:

  • timerModel 是 Scene 级别的状态,每个窗口有自己的 timer,不再是全局单例。
  • 如果 Scene 配置指定了 storyboard,窗口会自动创建,不用手写。
  • UIWindow(windowScene:) 把窗口绑定到当前 Scene,多窗口下不会串。

状态恢复用 NSUserActivity04:09)。Scene 进入后台时,系统会调用 stateRestorationActivity(for:) 拿到 userActivity 并持久化;Scene 重新连接时,通过 restoreInteractionStateWith 把状态还原回来:

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
    let userActivity = NSUserActivity(activityType: "com.example.timer.ui-state")
    userActivity.userInfo = ["selectedTimeFormat": timerModel.selectedTimeFormat]
    return userActivity
}

func scene(_ scene: UIScene, restoreInteractionStateWith userActivity: NSUserActivity) {
    if let selectedTimeFormat = userActivity.userInfo?["selectedTimeFormat"] as? String {
        timerModel.selectedTimeFormat = selectedTimeFormat
    }
}

关键点:

  • activityType 必须和 Info.plist 里声明的一致,否则系统不会持久化。
  • userInfo 只放可序列化的 UI 状态,比如选中项、滚动位置、文本输入。
  • 模型层的数据(任务、文档)应该走自己的存储,userActivity 只管 UI。

SplitView 的列宽和布局环境04:46)。新增的 splitViewControllerLayoutEnvironment trait 告诉你当前 SplitView 是展开还是折叠状态,可以据此切换 cell 的样式:

override func updateConfiguration(using state: UICellConfigurationState) {
    if state.traitCollection.splitViewControllerLayoutEnvironment == .collapsed {
        accessories = [.disclosureIndicator()]
    } else {
        accessories = []
    }
}

关键点:

  • .collapsed 表示 SplitView 列被压成单栏导航栈,需要 disclosure 提示用户还能往下钻。
  • .expanded 时是多列并排显示,cell 选中后内容直接显示在 secondary 列,不需要箭头。
  • 这个 trait 是 trait collection 的一部分,trait 改变时 cell 会自动重新配置。

避让窗口控制按钮13:04)。iPadOS 26 新增了关闭/最小化/排列三个窗口控制按钮。layoutGuide(for: .margins(cornerAdaptation:)) 拿到一个能避开窗口控制的布局指南:

let contentGuide = containerView.layoutGuide(for: .margins(cornerAdaptation: .horizontal))

NSLayoutConstraint.activate([
    contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
    contentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor),
    contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor),
    contentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor)
])

关键点:

  • .horizontal 角落适配适合顶部 bar 类内容,会从 trailing 边避让窗口控制。
  • 系统组件如 UINavigationBar 已经自动适配,只有自定义顶部 UI 才需要手动处理。
  • 不写这个适配,自定义按钮就会被窗口控制盖住。

调整大小期间暂停昂贵渲染14:44)。游戏或重渲染场景下,用户拖拽窗口边缘时不必每帧都重生成资源:

func windowScene(
    _ windowScene: UIWindowScene,
    didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry) {

    let geometry = windowScene.effectiveGeometry
    let sceneSize = geometry.coordinateSpace.bounds.size

    if !geometry.isInteractivelyResizing && sceneSize != previousSceneSize {
        previousSceneSize = sceneSize
        gameAssetManager.updateAssets(sceneSize: sceneSize)
    }
}

关键点:

  • isInteractivelyResizing 在用户拖拽边缘时为 true,松手后为 false。
  • 只在松手后更新一次资源,避免拖拽过程中反复重建纹理、重排布局。
  • 适合游戏、视频播放器、自定义渲染界面。

核心启发

  • 做什么:把 AppDelegate 生命周期迁移到 Scene 生命周期。

    • 为什么值得做:iOS 26 之后的下一个 major 版本会强制要求,提前迁移避免被赶 deadline;多窗口、状态恢复、AirPlay 外接显示这些能力都依赖 Scene。
    • 怎么开始:读 TN3187 技术文档,先做最小改动——把窗口创建和生命周期事件从 AppDelegate 搬到 SceneDelegate,UI 代码一个不动;跑通后再考虑多窗口、stateRestorationActivity 这些进阶能力。
  • 做什么:给 SplitView 加 Inspector 列。

    • 为什么值得做:iPadOS 26 的 Preview、Mail、Files 都用 Inspector 展示选中项的元数据;展开时在 secondary 列旁边,折叠时自动变 Sheet,不用自己写适配。
    • 怎么开始:调 splitViewController.setViewController(_:for: .inspector) 注册 inspector view controller,然后 splitViewController.show(.inspector) 显示;配合 splitViewControllerLayoutEnvironment trait 在 collapsed 时调整 inspector 内容的紧凑程度。
  • 做什么:给重渲染场景加 isInteractivelyResizing 检查。

    • 为什么值得做:iPad 多窗口和 Mac 上用户经常拖拽窗口大小,每次 geometry 变化都重建资源会卡顿;松手后再更新一次就够了。
    • 怎么开始:在 SceneDelegate 实现 windowScene(_:didUpdateEffectiveGeometry:),判断 geometry.isInteractivelyResizing == false 再触发昂贵操作;游戏纹理、视频解码、PDF 渲染都适用。
  • 做什么:用 Layout Margins Guide 替代手写 margin 数字。

    • 为什么值得做:每个平台的标准 margin 不同,硬编码 16/20 在 visionOS 或 Mac Catalyst 下会错位;Layout Margins Guide 自动跟随 Safe Area 和窗口控制调整。
    • 怎么开始:把 view.layoutMarginsGuide 当作约束的锚点,所有内容向它对齐;窗口控制场景额外用 view.layoutGuide(for: .margins(cornerAdaptation: .horizontal))

关联 Session

评论

GitHub Issues · utterances