Modernize your UIKit app
SwiftUI & UI Frameworks 进阶 16m

升级改造你的 UIKit App

Modernize your UIKit app

2026年6月10日

在 Apple 官方观看视频

一句话判断

iPhone App 在 Mac (iPhone Mirroring) 和 iPad 上变成了“完全可自由拉伸的窗口”,这直接宣判了 UIScreen.main 和硬编码设备类型的死刑,逼迫所有 UIKit 老项目必须彻底拥抱 Size Classes (尺寸类别)。

这场 Session 讲了什么

以前 iPhone app 在 iPad 或 Mac 上跑,基本就是个固定比例的“大手机”或者带黑边的窗口。现在 iOS 27 让 iPhone app 彻底变成了像 Mac app 一样可以随意拖拽边缘改变大小的窗口。

为了配合这个变化,Apple 直接下狠手:不用 UIScene (场景) 生命周期直接不让启动,UIScreen.main (主屏幕) 拿到的数据全是错的,查设备类型和屏幕方向也失去了意义。这对还在用老一套“面向特定设备尺寸写死布局”的 UIKit 团队是暴击,你必须全面转向 Size Classes、Trait Collections (特征集合) 和 Window Scene 提供的动态几何信息。好消息是 Xcode 27 引入了 AI Agent 来帮你自动做这些苦力活。

值得深挖的点

UIScreen.main 的彻底死亡与 Trait Collection 的全面接管

多屏幕和自由拉伸窗口让“主屏幕”这个概念变得荒谬。你的 app 可能在 Mac 的外接显示器上,也可能在 iPad 的 Stage Manager (台前调度) 里被缩到四分之一大小。这时候去问 UIScreen.main.scale 或者 bounds,拿到的完全是错误信息。

Apple 给出的解法是用 traitCollection.displayScale 拿缩放比,用 windowScene.effectiveGeometry 拿可用空间。这里有个非常棒的设计是 Automatic trait tracking (自动特征追踪)。只要你在 layoutSubviews 这类系统回调里读取了 trait 属性,系统就会自动追踪它。一旦 trait 改变,系统会自动重新调用 layoutSubviews,你连 registerForTraitChanges 都不用写。这大大降低了响应式布局的样板代码量。对于那些不在标准生命周期里的自定义绘制,才需要手动注册监听。

UIKit 的“iPad 化”与自适应容器

既然 iPhone app 可以拉伸成 iPad 尺寸,UI 控件也得跟上,不能拉伸后底部还是挤着一个小小的 TabBar。tabBarController.sidebar.preferredPlacement = .sidebar 这个 API 很聪明,它不是强制你写两套 UI,而是让系统根据当前窗口空间“自动”决定是显示底部 Tab 还是侧边栏。

配合 prominentTabIdentifier,你可以钉住一个核心 Tab,保证在窗口被压缩、侧边栏折叠时它依然可见。Navigation bar (导航栏) 新增的 barMinimizationBehavior 也是同理,允许导航栏在滚动时滑走,把控制权交给系统,让内容在拉伸时有更多呼吸空间。这其实是 UIKit 在疯狂吸收 SwiftUI 的声明式和自适应思想,让老框架在新时代的窗口管理下不至于太难看。

代码片段

1. 获取正确的屏幕缩放 (替代 UIScreen.main.scale)

场景:在自定义 View 的布局或绘制中获取屏幕缩放比例。

override func layoutSubviews() {
    super.layoutSubviews()
    
    // 只要在这里读取 displayScale,系统就会自动追踪它的变化
    // 当窗口拖到不同分辨率的屏幕时,会自动重新触发 layoutSubviews
    let scale = traitCollection.displayScale
    
    // 根据 scale 调整线宽或图片资源
    layer.borderWidth = 1.0 / scale
}

坑:千万别在 init 或者非 UI 刷新的地方读取 trait 然后缓存起来,trait 是动态变化的,缓存会导致拉伸后 UI 模糊或错位。

2. 监听特征变化 (处理非标准 UI 刷新)

场景:在自定义的渲染引擎或缓存逻辑中,需要监听屏幕缩放或尺寸类别的变化。

// 手动注册 trait 变化监听
let traitsToWatch: [UITrait] = [UITraitDisplayScale.self, UITraitHorizontalSizeClass.self]

registerForTraitChanges(traitsToWatch) { (view: MyCustomView, previousTraits: UITraitCollection) in
    // 当缩放比例或水平尺寸类别改变时,清除旧缓存并重新生成
    view.imageCache.invalidate()
    view.rebuildRenderPipeline()
}

坑:闭包里传入的 previousTraits 是用来对比的,别把它当成当前状态去用。当前状态永远从 view.traitCollection 里拿。

3. 开启 TabBar 侧边栏自适应

场景:让 iPhone App 在大窗口下自动显示侧边栏,小窗口下恢复底部 Tab。

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 告诉系统我们支持侧边栏布局,系统会根据窗口宽度自动切换
    tabBarController?.sidebar.preferredPlacement = .sidebar
    
    // 钉住“首页”Tab,确保在窗口极度压缩、侧边栏折叠时它依然显示
    tabBarController?.prominentTabIdentifier = "home_tab"
}

坑:设置 preferredPlacement 后,记得用 tabBarController?.sidebar.isAvailable 检查一下当前环境是否真的支持,别盲目依赖。

最佳实践

已有项目的迁移策略:第一步也是最重要的一步,赶紧确认你的 App 已经迁移到了 UIScene 生命周期,iOS 27 SDK 下没迁移的 App 直接无法启动。第二步,全局搜索 UIScreen.mainUIDevice.current.userInterfaceIdiominterfaceOrientation,把这些硬编码全部替换为 Trait Collection 和 Size Classes。强烈建议直接跑一下 Xcode 27 自带的 App modernization skill (应用现代化技能),让 AI Agent 帮你完成这些机械的替换工作。

新项目的采用建议:彻底忘掉“这是 iPhone 还是 iPad”的概念。在画 UI 的时候,脑子里只应该有“现在是 Compact (紧凑) 还是 Regular (常规)”的尺寸类别。使用 UIRequiresFullscreen 时要谨慎,除非你是强依赖固定比例的游戏,否则尽量让 App 支持自由拉伸。

实战中容易踩的坑:在处理 CoreMotion 或 CoreLocation 数据时,以前需要根据屏幕方向手动计算坐标系旋转。现在直接把 UIView 赋值给 motionManager.deviceMotionBody,系统会自动帮你把物理坐标转换到当前 View 的坐标系,别再自己写三角函数算角度了。

还有什么值得关注

  • Menu 图片可见性控制:在 iPadOS 和 macOS 的菜单栏中,Menu element (菜单元素) 的图片默认可能不显示,需要用 preferredImageVisibility 强制覆盖。
  • Siri 与 Drag & Drop 整合:如果你的 App 支持拖拽,Siri 可以直接读取你 Drag handler (拖拽处理器) 里的资源,但切记别在 sessionWillBegin 里弹 Modal UI,因为现在的拖拽可以由 Siri 无手势触发。
  • Navigation Bar 滚动隐藏:导航栏现在支持在滚动时自动滑走 (barMinimizationBehavior),以前用 .soft 覆盖默认样式的 App 需要重新评估设计,因为 .automatic 的默认视觉效果已经大改了。
UIKit Size Classes Trait Collections UIScene Adaptivity