Highlight
iOS 27 要求所有 UIKit App 彻底放弃硬编码设备类型和屏幕方向,改用 Trait Collection 和 Size Classes 响应任意窗口尺寸;iPhone App 在 iPad 和 Mac 上成为完全可自由拉伸的窗口,未迁移到 UIScene 生命周期的 App 将无法启动。
核心内容
从”固定设备”到”任意窗口”
以前写 UIKit App,很多人会这样判断布局:
if UIDevice.current.userInterfaceIdiom == .pad {
// iPad 布局
} else {
// iPhone 布局
}
或者根据屏幕方向决定界面结构。这套逻辑在 iOS 27 下彻底失效了。
iPhone App 现在可以在 Mac 的 iPhone Mirroring 中被用户自由拉伸成任意比例。iPhone-only App 在 iPad 上运行也不再是固定比例的”大手机”,而是和其他 iPad App 一样完全可调整大小。这意味着你的 App 可能在 16:9 的 Mac 窗口里运行,也可能在竖长的 iPhone 屏幕上运行,而 userInterfaceIdiom 始终是 .phone。
Apple 的建议很直接:停止用设备类型做布局决策,改用 Size Classes。
UIScene 生命周期成为强制要求
(02:22)
用最新 SDK 构建的 App,如果没有采用 UIScene 生命周期,将直接无法启动。这是自适应布局的基础设施——没有 Scene,系统无法管理多个窗口尺寸和生命周期状态。
如果你还在用 AppDelegate 那一套,需要尽快迁移。Apple 提供了 WWDC25 的 session “Make your UIKit app more flexible” 和文档 “Transitioning to the UIKit scene-based life cycle” 作为参考。
告别 UIScreen.main
(03:02)
当 App 被镜像到 Mac 或移动到 iPad 外接显示器时,Scene 关联的屏幕会发生变化。UIScreen.main 返回的永远是错误的屏幕信息。
正确做法是从 window scene 动态获取:
let screen = window?.windowScene?.screen
更好的做法是彻底移除对 screen 的依赖,改用 Trait Collection 中的替代 API。
屏幕缩放和可用空间的正确获取方式
(03:49)
以前用 UIScreen.main.scale 获取缩放比例,现在改用 traitCollection.displayScale:
override func layoutSubviews() {
super.layoutSubviews()
let displayScale = traitCollection.displayScale
// 根据 displayScale 调整绘制逻辑
}
UIKit 会自动追踪常见方法(如 layoutSubviews、draw(_:))中对 trait 的读取。当 displayScale 变化时,系统会自动重新调用这些方法,不需要手动监听。
对于自动追踪覆盖不到的地方,使用 registerForTraitChanges:
let displayScaleTrait: [UITrait] = [UITraitDisplayScale.self]
registerForTraitChanges(displayScaleTrait) {
(view: GalleryView, previousTraitCollection: UITraitCollection) in
view.cache.invalidate()
}
(04:36)
获取可用空间也不再用 UIScreen.main.bounds,而是看 Scene 的有效几何信息或 View 自身的 bounds:
// 在 SceneDelegate 中监听几何变化
func windowScene(
_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previousEffectiveGeometry: UIWindowScene.Geometry
) {
let geometry = windowScene.effectiveGeometry
let availableSpace = geometry.coordinateSpace.bounds
}
// 在 ViewController 中直接看 view 的 bounds
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let availableSpace = view.bounds.size
}
Tab Bar 和 Navigation Bar 的新能力
(09:51)
iPhone App 现在可以选择在大窗口下显示侧边栏而非底部 Tab Bar:
tabBarController.sidebar.preferredPlacement = .sidebar
系统会根据水平 Size Class 决定是否展示侧边栏。你可以检查 tabBarController.sidebar.isAvailable 判断当前环境是否支持。
还可以设置一个始终可见的 prominent tab:
tabBarController.prominentTabIdentifier = "cart"
(10:53)
Navigation Bar 新增了滚动最小化控制:
navigationItem.barMinimizationBehavior = .always
navigationItem.barMinimizationSafeAreaAdjustment = .never
(11:30)
CoreMotion 和 CoreLocation 的 Body 协议
(08:12)
UIView 现在遵循 CoreMotion 和 CoreLocation 的 Body 协议。直接把 View 赋值给 motion manager,系统会自动处理坐标空间转换:
override func viewDidLoad() {
super.viewDidLoad()
motionManager.deviceMotionBody = view
locationManager.headingBody = view
}
不再需要根据屏幕方向手动计算坐标旋转。
Apple Intelligence 集成
(12:56)
iOS 27 的 Menu 会自动显示 Ask Siri 按钮。你可以用新的 View Annotations API 给特定 View 标注 AppEntity,让 Siri 获得更精准的上下文。
如果 App 支持拖拽,Siri 可以直接从你的 drag delegate 加载资源。注意:拖拽可能由 Siri 无手势触发,不要在 sessionWillBegin 中展示模态 UI,有状态的 UI 初始化代码放到 sessionDidMove 中。
Xcode 27 的 App Modernization Skill
(14:16)
Xcode 27 内置了一个 App 现代化 skill,可以自动完成大部分迁移工作:
- 把
UIScreen.main调用替换为traitCollection或 scene bounds 检查 - 把设备类型检查替换为 Size Class 检查
- 把 App 生命周期转换为 Scene 生命周期
- 复杂任务会主动提问,大任务会添加注释标记剩余工作
导出 skill 供其他工具使用:
xcrun agent skills export
(15:05)
详细内容
代码示例 1:从 window scene 获取屏幕信息
当 App 被镜像到 Mac 或移动到 iPad 外接显示器时,Scene 关联的屏幕会发生变化。UIScreen.main 返回的永远是错误的屏幕信息。
正确做法是从 window scene 动态获取:
let screen = window?.windowScene?.screen
关键点:UIScreen.main 在 iOS 27 下已废弃,因为它无法处理多屏幕场景。windowScene.screen 始终返回当前 Scene 实际所在的屏幕,在 iPhone Mirroring 和外接显示器切换时自动更新。
代码示例 2:用 registerForTraitChanges 监听 trait 变化
UIKit 会自动追踪 layoutSubviews、draw(_:) 等方法中对 trait 的读取,当 displayScale 变化时自动重绘。但对于自动追踪覆盖不到的地方(如缓存失效、后台数据重算),需要显式注册监听:
let displayScaleTrait: [UITrait] = [UITraitDisplayScale.self]
registerForTraitChanges(displayScaleTrait) {
(view: GalleryView, previousTraitCollection: UITraitCollection) in
view.cache.invalidate()
}
也可以监听多个 trait 的组合变化:
let geometryTraits: [UITrait] = [
UITraitHorizontalSizeClass.self,
UITraitVerticalSizeClass.self
]
registerForTraitChanges(geometryTraits) { (vc: MyViewController, _) in
vc.updateLayoutForCurrentSizeClass()
}
关键点:registerForTraitChanges 的闭包参数类型必须匹配被监听对象的实际类型(如 GalleryView 或 MyViewController),系统靠类型推断来分发回调。闭包在 trait 变化时同步调用,适合处理轻量级的状态更新;若需要异步或复杂逻辑,建议在闭包中触发自己的状态机。
代码示例 3:Tab Bar 侧边栏适配
iPhone App 在大窗口下可以自动切换为侧边栏导航,节省底部空间:
tabBarController.sidebar.preferredPlacement = .sidebar
系统会根据水平 Size Class 决定是否展示侧边栏。你可以检查 tabBarController.sidebar.isAvailable 判断当前环境是否支持。
还可以设置一个始终可见的 prominent tab,确保核心功能在侧边栏折叠时仍可访问:
tabBarController.prominentTabIdentifier = "cart"
关键点:preferredPlacement = .sidebar 告诉系统在条件合适时”在条件允许时优先用侧边栏”。当窗口缩小到 compact 水平 Size Class 时,系统会自动切回底部 Tab Bar,无需手动处理。prominentTabIdentifier 对应的 tab 在侧边栏模式下会突出显示,在折叠模式下会保留为浮动按钮。
自适应布局的关键 API 迁移
| 旧写法 | 新写法 | 说明 |
|---|---|---|
UIScreen.main | window?.windowScene?.screen | 从 window scene 动态获取 |
UIScreen.main.scale | traitCollection.displayScale | Trait Collection 自动追踪 |
UIScreen.main.bounds | view.bounds.size 或 windowScene.effectiveGeometry | 看 View 或 Scene 的实际空间 |
UIDevice.current.userInterfaceIdiom | traitCollection.horizontalSizeClass | 用 Size Class 做布局决策 |
UIApplication.shared.statusBarOrientation | traitCollection.horizontalSizeClass / verticalSizeClass | 方向检查不再有意义 |
游戏 App 的特殊处理
游戏通常需要固定方向,iOS 27 对 UIRequiresFullscreen 的行为做了调整:不再完全禁止拉伸,而是启用离散拉伸(discrete resizing)。每次改变窗口大小时,系统会切换到一个匹配新尺寸的屏幕配置,游戏始终以完整质量渲染。
测试建议
Xcode 27 的 Device Hub 和 Xcode Previews 支持”进入拉伸模式”,可以直接拖拽设备边缘自由调整大小,不需要在多个模拟器之间切换。完成初步验证后,务必在真实设备上测试 iPhone Mirroring 和 iPad 外接显示器场景。
核心启发
-
把老项目的
UIScreen.main全局替换掉做什么:搜索项目中所有
UIScreen.main引用,替换为traitCollection.displayScale或 window scene 引用。为什么值得做:iOS 27 下
UIScreen.main返回错误数据,会导致图片模糊、布局错位。怎么开始:用 Xcode 的 Find Navigator 搜索
UIScreen.main,逐处替换。或者直接用 Xcode 27 的 App modernization skill 自动处理。 -
给 Tab Bar 加上侧边栏适配
做什么:在
viewDidLoad中设置tabBarController?.sidebar.preferredPlacement = .sidebar。为什么值得做:iPhone App 在 Mac 上拉伸成大窗口后,底部 Tab Bar 会浪费大量横向空间,侧边栏能展示更多导航层级。
怎么开始:一行代码开启,再用
sidebar.isAvailable判断当前是否展示侧边栏,调整相关 UI。 -
用 prominent tab 保护核心导航
做什么:给最重要的 Tab(如购物车、首页)设置
prominentTabIdentifier。为什么值得做:窗口缩小时侧边栏可能折叠,prominent tab 保证核心功能始终可访问。
怎么开始:在创建 UITabBarController 后设置
tabBarController.prominentTabIdentifier = "your-tab-id"。 -
让 Navigation Bar 在滚动时自动收起
做什么:设置
navigationItem.barMinimizationBehavior = .always。为什么值得做:内容区域增大,阅读体验更好。系统默认只在特定条件下收起,强制开启可以统一行为。
怎么开始:在 ViewController 的初始化代码中设置该属性。如果自行处理 safe area,同时设置
barMinimizationSafeAreaAdjustment = .never。 -
导出 Xcode skill 到自己的工作流
做什么:运行
xcrun agent skills export,把 Xcode 的 modernization skill 导出为 markdown 文件。为什么值得做:可以在 Cursor、Claude Code 等其他工具中复用 Apple 官方整理的现代化规则,保持团队迁移策略一致。
怎么开始:在 Terminal 中运行命令,导出的文件可以直接导入其他 AI 编码工具。
关联 Session
- Make your UIKit app more flexible — 更深入的 UIKit 自适应布局指南,Scene 生命周期迁移的详细步骤
- Elevate your tab and sidebar experience in iPadOS — Tab Bar 与侧边栏的深度集成,管理 Tab Group 的最佳实践
- Get the most out of Device Hub — Xcode 27 新 Device Hub 的完整功能介绍,包含自由拉伸测试
- Explore advanced App Intents features for Siri and Apple Intelligence — View Annotations API 和 Siri 集成的详细用法
- SwiftUI — 如果你同时维护 SwiftUI 代码,了解 SwiftUI 在自适应布局方面的最新进展
评论
GitHub Issues · utterances