What's new in UIKit
SwiftUI & UI Frameworks 进阶 20m

UIKit 的新变化

What's new in UIKit

2024年6月10日

在 Apple 官方观看视频

一句话判断

iOS 18 的 UIKit 最值得关注的是用 SwiftUI Animation 驱动 UIKit 动画——两个框架的动画系统终于打通了。

这场 Session 讲了什么

iOS 18 的 UIKit 在三个方向做了更新。第一是 UI 层面的改进:document-based app 有了全新的启动体验设计能力,Tab Bar 在 iPad 上变得紧凑且可折叠为 sidebar,新的 zoom transition 让导航转场更具连续性和交互性。第二是 UIKit 和 SwiftUI 的互操作性提升:现在可以用 SwiftUI 的 Animation 类型来驱动 UIKit view 的动画,包括自定义 CustomAnimation;手势系统也做了统一,UIKit 和 SwiftUI 手势之间可以设置依赖关系,还能通过新的 UIGestureRecognizerRepresentable 把 UIKit 手势直接加到 SwiftUI 层级中。第三是大量基础 API 的改进,包括 automatic trait tracking、collection/table view 的 list environment trait 等。

值得深挖的点

SwiftUI Animation 驱动 UIKit 动画

这是 UIKit 今年最重要的变化之一。以前你要做弹簧动画,需要用 UISpringTimingParametersUIViewPropertyAnimator,参数调试痛苦。现在直接用 SwiftUI 的 Animation.spring() 就能驱动 UIView 的动画。更关键的是,在做手势驱动的交互动画时,你可以在手势变化阶段和手势结束阶段分别触发动画,SwiftUI 的 spring 动画会自动保持手势拖拽阶段到释放阶段的连续速度——这解决了交互式动画中长期存在的速度不连续问题。

这种打通意味着你不需要在 UIKit 中手写复杂的速度衔接逻辑。SwiftUI 的动画系统已经帮你处理好了 interactive 到 non-interactive 的速度过渡。对于有复杂手势交互的 UIKit app,这个改动能显著减少动画相关代码量。

Automatic Trait Tracking

UIKit 的 trait 系统一直需要手动注册:在 traitCollectionDidChange 中检查特定 trait 的变化,然后手动调用 setNeedsLayoutsetNeedsDisplay。iOS 18 引入了 automatic trait tracking——在 layoutSubviewsdrawRect 等方法中访问的 trait 会被自动记录,当这些 trait 发生变化时,UIKit 会自动触发对应的 invalidation。

这个设计的精妙之处在于它是零成本的:只有你实际访问的 trait 才会创建依赖,不需要的 trait 不会产生任何开销。而且它永远处于激活状态,不需要 opt-in。你只需要删掉之前手动注册 trait 变化的代码,一切自动生效。

代码片段

用 SwiftUI Animation 驱动 UIKit 动画

场景:给 UIView 做一个弹簧动画,且在手势交互时保持速度连续。

import UIKit
import SwiftUI

// 手势变化时
UIView.animate(using: Animation.spring(duration: 0.4, bounce: 0.2)) {
    view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}

// 手势结束时
UIView.animate(using: Animation.spring(duration: 0.4, bounce: 0.2)) {
    view.transform = .identity
}

坑:确保同时引入了 SwiftUI framework,UIView.animate(using:) 的参数类型是 SwiftUI 的 Animation

UIKit 和 SwiftUI 手势的依赖协调

场景:UIKit 的单击手势和 SwiftUI 的双击手势需要共存——单击只在双击失败时才触发。

// SwiftUI 侧:给手势设置名字
let doubleTap = SpatialTapGesture(count: 2)
    .name("SwiftUIDoubleTap")

// UIKit 侧:在 delegate 中设置失败依赖
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                       shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    if otherGestureRecognizer.name == "SwiftUIDoubleTap" {
        return true
    }
    return false
}

坑:手势名称匹配是字符串比较,确保命名一致。只有 iOS 18 才支持跨框架的手势依赖。

Automatic Trait Tracking

场景:之前手动注册 trait 变化,现在可以直接删除。

// iOS 17: 需要手动注册
override func layoutSubviews() {
    super.layoutSubviews()
    if traitCollection.horizontalSizeClass == .compact {
        // 紧凑布局
    } else {
        // 常规布局
    }
}

// 需要额外注册
// registerForTraitChanges([UITraitHorizontalSizeClass.self]) { ... }

// iOS 18: 只需 layoutSubviews,删掉 registerForTraitChanges 即可
// 访问 horizontalSizeClass 时自动创建依赖

坑:这个功能只在支持的 update 方法中生效(layoutSubviews、drawRect 等),不在所有方法中都可用,具体列表参考文档。

最佳实践

已有项目:逐步迁移动画代码——新写的交互式动画用 UIView.animate(using:) + SwiftUI spring,旧的 UIViewPropertyAnimator 代码不需要立刻改。优先在新功能中采用 automatic trait tracking,旧的 registerForTraitChanges 可以在下次碰到相关代码时顺手删除。

新项目:Tab Bar 直接用新的 UITabUITabGroup API 来描述结构,不要再用旧的 UITabBarItem 数组方式。新的 API 能自动适配 tab bar 和 sidebar 两种形态,在 Mac Catalyst 和 visionOS 上也能获得原生体验。

还有什么值得关注

  • 新的 zoom transition 支持导航和 present 两种场景,而且是 continuously interactive 的——你可以在转场的任何阶段抓住拖拽。
  • Collection view 的 list section 和 table view 现在都设置了 list environment trait,UIListContentConfiguration 会自动根据 list 环境调整样式,你不再需要在配置 cell 时知道 list 的具体样式。
  • Document-based app 的启动体验可以完全自定义设计,包括模板文档创建引导。
WWDC 2024