WWDC Quick Look 💓 By SwiftGGTeam
Modernize your UIKit app

Modernize your UIKit app

观看原视频

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 会自动追踪常见方法(如 layoutSubviewsdraw(_:))中对 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
}

05:19)(05:35

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 会自动追踪 layoutSubviewsdraw(_:) 等方法中对 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 的闭包参数类型必须匹配被监听对象的实际类型(如 GalleryViewMyViewController),系统靠类型推断来分发回调。闭包在 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.mainwindow?.windowScene?.screen从 window scene 动态获取
UIScreen.main.scaletraitCollection.displayScaleTrait Collection 自动追踪
UIScreen.main.boundsview.bounds.sizewindowScene.effectiveGeometry看 View 或 Scene 的实际空间
UIDevice.current.userInterfaceIdiomtraitCollection.horizontalSizeClass用 Size Class 做布局决策
UIApplication.shared.statusBarOrientationtraitCollection.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

评论

GitHub Issues · utterances