让 UIKit 应用更灵活
Make your UIKit app more flexible
2025年6月9日
一句话判断
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,适合暂停游戏渲染或图像处理等昂贵操作。