Make your UIKit app more flexible
SwiftUI & UI Frameworks 进阶 0m

让 UIKit 应用更灵活

Make your UIKit app more flexible

2025年6月9日

在 Apple 官方观看视频

一句话判断

UIScene 生命周期即将变成强制要求——现在不迁移以后编译都过不了。但 Apple 给的迁移路径其实很清晰:Scene 管生命周期和状态恢复,SplitView/TabBar 管布局适应,Safe Area + Layout Guide 管自适应 UI。问题在于老项目的迁移成本。

这场 Session 讲了什么

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

基础层是 Scene。Scene 是 App UI 的独立实例,有各自的生命周期、状态保存/恢复、窗口几何信息。关键信息:在 iOS 26 之后的下一个 major 版本,UIScene 生命周期将变成使用最新 SDK 构建时的强制要求。不需要支持多 Scene,但必须用 Scene 生命周期替代旧的 AppDelegate 生命周期。Session 用一个任务计时器 App 展示了完整实现:Scene Delegate 管理窗口创建、状态恢复(通过 userActivity)、AirPlay 外部显示的 Scene 配置。

中间层是容器视图控制器。UISplitViewController 新增了交互式列拖拽调整大小(支持指针适配)、自定义列最小/最大/首选宽度、新增 splitViewControllerLayoutEnvironment trait 区分展开/折叠状态、Inspector 列的一级支持(展开时在 secondary 列旁边,折叠时变成 Sheet)。UITabBarController 在不同平台上的适配:iPhone 底部、iPad 顶部(可切换为侧边栏)、Mac 工具栏或侧边栏、visionOS 装饰带。

顶层是自适应 API。Safe Area 的正确使用(背景可以延伸到 Safe Area 外,内容不能)、Layout Margins Guide 用于统一间距、iPadOS 26 新增的窗口控制按钮(关闭/最小化/排列)的适配——用带 corner adaptation 的 layout margins guide 把内容避开窗口控制。prefersInterfaceOrientationLocked 可以锁定方向,isInteractivelyResizing 可以在交互式调整大小期间暂停昂贵的渲染。

值得深挖的点

UIScene 生命周期的强制迁移时间线

Session 明确说了”next major release following iOS 26”——也就是 iOS 27(或类似命名),UIScene 生命周期将成为使用最新 SDK 构建时的硬性要求。这不是”建议迁移”而是”必须迁移”。更关键的是 UIRequiresFullscreen 已经被废弃,会在未来版本被忽略。

对于还在用 AppDelegate 生命周期的老项目,迁移路径是:先在 Info.plist 里配置 UISceneSession,然后实现 UISceneDelegate,把 AppDelegate 里的窗口创建逻辑搬过去。Session 给出的 Tech Note “Migrating to the UIKit scene-based life cycle” 是官方参考。

UISplitViewController Inspector 列

Inspector 列是一个被低估的特性。它本质上是一个”详情的详情”列——在 secondary 列旁边提供额外信息面板。Preview App 用 Inspector 显示照片的元数据。展开时紧跟在 secondary 列后面,折叠时自动变成 Sheet,不需要你写任何适配代码。

实现上只需要做两件事:给 splitViewController 设置 inspector 列的 viewController,然后调 showInspector 来显示。默认隐藏,按需展示。

窗口控制按钮的 Layout 适配

iPadOS 26 引入了类似 macOS 的窗口控制(关闭、最小化、排列按钮)。这些按钮出现在 Scene 内容区域里,和你的 UI 共用空间。系统组件(UINavigationBar)会自动避开,但你自己的 UI 需要主动适配。

关键 API 是带 corner adaptation 的 layout margins guide——它自动把 Safe Area inset 从窗口控制按钮所在的边缘方向扩展,让内容和按钮保持距离。对于顶部栏状内容,用 horizontal corner adaptation。

代码片段

Scene 状态保存与恢复

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }
        let window = UIWindow(windowScene: windowScene)

        // 从 stateRestorationActivity 恢复 UI
        if let activity = session.stateRestorationActivity {
            restoreInteractionState(with: activity)
        }

        window.rootViewController = MainViewController()
        window.makeKeyAndVisible()
        self.window = window
    }

    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        let activity = NSUserActivity(activityType: "com.app.timer")
        activity.userInfo = ["selectedTimer": currentTimerID]
        return activity
    }

    func scene(_ scene: UIScene,
               restoreInteractionStateWith activity: NSUserActivity) {
        if let timerID = activity.userInfo?["selectedTimer"] as? String {
            // 恢复选中的计时器
            restoreSelection(timerID: timerID)
        }
    }
}

坑:stateRestorationActivity 在 Scene 进入后台前由系统调用,不是你自己决定时机。不要在这里做异步操作——它需要同步返回。

SplitView Inspector 列

// 设置 inspector
splitViewController.setViewController(metadataVC, for: .secondary)

// 显示 inspector
splitViewController.show(.secondary)

// 检查展开/折叠状态
func updateDisclosureIndicators() {
    let isCollapsed = traitCollection.splitViewControllerLayoutEnvironment == .collapsed
    // 折叠时显示 disclosure indicator
    cell.accessoryType = isCollapsed ? .disclosureIndicator : .none
}

坑:Inspector 列在 splitViewController 首次出现时是隐藏的。你需要主动调 show 来展示——不像 primary/secondary 列那样自动显示。

窗口控制按钮适配

// 顶部内容避开窗口控制
func setupTopBar() {
    let guide = view.layoutMarginsGuideWithCornerAdaptation(.horizontal)

    NSLayoutConstraint.activate([
        topBar.leadingAnchor.constraint(equalTo: guide.leadingAnchor),
        topBar.trailingAnchor.constraint(equalTo: guide.trailingAnchor),
        topBar.topAnchor.constraint(equalTo: view.topAnchor)
    ])
}

坑:窗口控制的样式可以用 preferredWindowingControlStyle 在 Scene Delegate 里指定——.automatic、.visible、.hidden。选 .hidden 前要想清楚用户怎么关闭/最小化窗口。

最佳实践

Scene 迁移的优先级:先做最小改动——只把窗口创建和生命周期事件从 AppDelegate 搬到 SceneDelegate,不改任何 UI 代码。确认一切正常后,再利用 Scene 的能力做多窗口、状态恢复等高级功能。不要一次性大改。

SplitView 的列宽设置要谨慎。如果你设置了过大的最小宽度,可能导致窄屏幕下某些列无法显示——降低了应用的灵活性。默认值通常就是最优的,只在有明确需求时才调整。

布局方面,所有重要内容都必须在 Safe Area 内。背景可以延伸出去——但交互元素和文字不行。Layout Margins Guide 是统一间距的标准做法——不要在每个 View 里手写 margin 值。

还有什么值得关注

  • iOS 26 SDK 构建的 App 不再被系统缩放或 letterbox 适配新屏幕尺寸——这意味着你需要自己确保 UI 在所有尺寸上都能正确显示。
  • SwiftUI 和 UIKit 的 Scene 类型现在可以在同一个 App 里混合使用——check “What’s new in UIKit”。
  • UITabBarController 的 UITabGroup 可以提供 managingNavigationController,当侧边栏不可用时自动把 tab group 内的 view controller 推入导航栈。
  • isInteractivelyResizing 在交互式拖拽期间返回 true,适合暂停游戏渲染或图像处理等昂贵操作。
SwiftUI