WWDC Quick Look 💓 By SwiftGGTeam
Enhance your UI animations and transitions

Enhance your UI animations and transitions

观看原视频

Highlight

iOS 18 新增的 Zoom Transition 和 SwiftUI-UIKit 动画打通,让跨框架的连续交互变得更简单。

核心内容

开发一个流畅的转场动画通常需要大量代码。在 UIKit 中要写 CAAnimation,在 SwiftUI 中要写 withAnimation,如果视图层级是混合的,还要手动同步两边的动画状态。

iOS 18 解决了这个问题。首先是 Zoom Transition:点击列表中的 cell,它会放大成全屏详情页。这不是一个简单的视觉特效——转场全程可交互,你可以随时抓取拖拽,甚至转场进行到一半时反向操作。它不只是 Navigation Push 可用,fullScreenCover 和 sheet 也支持。

第二个更底层的改变是 SwiftUI Animation 现在可以直接驱动 UIView 和 NSView。不用再写 UIView.animate,直接传一个 SwiftUI 动画类型即可。这意味着在混合视图层级里,可以用同一套动画 API 统一管理所有视图。

Session 还深入讲解了 Zoom Transition 在 UIKit 中的生命周期回调时序。当你”在 Push 进行中按返回键”这种复杂场景发生时,系统不会取消 Push,而是将它立即完成并转换为 Pop。从视图控制器的角度看,它总是会到达 Appeared 状态,所有回调(viewWillAppear/willAppear/didAppear)都会按预期顺序执行。

详细内容

Zoom Transition 基础用法

SwiftUI 中的 Zoom Transition 需要两个步骤:

NavigationLink {
    BraceletEditor(bracelet)
        .navigationTransitionStyle(
            .zoom(
                sourceID: bracelet.id,
                in: braceletList
            )
        )
} label: {
    BraceletPreview(bracelet)
}
.matchedTransitionSource(
    id: bracelet.id,
    in: braceletList
)

关键点

  • navigationTransitionStyle(.zoom(...)) 声明目标视图使用 zoom 转场(2:10
  • .matchedTransitionSource 标记源视图,两者用相同的 idnamespace 配对
  • 转场时,源视图会 morph 成目标视图,保持同一 UI 元素在屏幕上的连续性

UIKit 中用法类似,在 Push 前设置 preferredTransition

func showEditor(for bracelet: Bracelet) {
    let braceletEditor = BraceletEditor(bracelet)
    braceletEditor.preferredTransition = .zoom { context in
        let editor = context.zoomedViewController
            as! BraceletEditor
        return cell(for: editor.bracelet)
    }
    navigationController?.pushViewController(braceletEditor, animated: true)
}

关键点

  • preferredTransition 设置在目标视图控制器上(3:02
  • closure 会在 zoom in 和 zoom out 时各调用一次
  • 应该返回一个稳定的标识符(如 model 对象)而非直接捕获视图,避免 UICollectionView 复用导致的问题
  • context 取当前数据,支持详情页内左右滑动后返回正确源视图

转场生命周期和兼容性

UIKit 的可中断转场有一个重要设计:中断的 Push 不会被取消,而是立即完成并转为 Pop

这意味着:

  • Push 的 viewWillAppearviewDidAppear 一定会被调用
  • 接着在同一 runloop 循环中,Pop 开始,触发 viewWillDisappearviewDidDisappear
  • 从代码角度看,就像 Push 正常完成后立即开始 Pop

这是为了兼容现有代码。如果取消 Push,视图控制器从未”真正出现过”,很多假设”只在 viewWillAppear 做一次”的代码会出问题。

SwiftUI Animation 驱动 UIKit 视图

iOS 18 最大的打通是 SwiftUI Animation 类型可以直接用于 UIView 动画:

UIView.animate(.spring(duration: 0.5)) {
    bead.center = endOfBracelet
}

关键点

  • 参数不再是传统的 UIView.AnimationOptions,而是 SwiftUI 的 Animation 类型(8:39
  • 支持所有 SwiftUI 动画类型,包括 CustomAnimations
  • 内部实现不创建 CAAnimation,直接操作 layer 的 presentation values
  • presentation layer 仍会正确反映动画状态

在 UIViewRepresentable 中,用新的 context.animate 桥接动画:

struct BeadBoxWrapper: UIViewRepresentable {
    @Binding var isOpen: Bool

    func updateUIView(_ box: BeadBox, context: Context) {
        context.animate {
            box.lid.center.y = isOpen ? -100 : 100
        }
    }
}

struct BraceletEditor: View {
    @State private var isBeadBoxOpen = false
    var body: some View {
        BeadBoxWrapper($isBeadBoxOpen.animated())
            .onTapGesture {
                isBeadBoxOpen.toggle()
            }
    }
}

关键点

  • .animated() modifier 让 binding 变化携带动画信息(9:56
  • context.animate 捕获当前 Transaction 上的动画,应用到 UIView 变更
  • 如果 Transaction 没有动画,代码会立即执行,无需手动判断
  • SwiftUI 和 UIView 的动画完美同步

手势驱动的连续动画

拖拽手势结束后保持速度,传统做法需要计算初始 velocity:

switch gesture.state {
case .changed:
    UIView.animate(.interactiveSpring) {
        bead.center = gesture.translation
    }

case .ended:
    UIView.animate(.spring) {
        bead.center = endOfBracelet
    }
}

关键点

  • 手势进行中用 .interactiveSpring,每次变化创建新动画,彼此 retarget(11:39
  • 手势结束后用普通 .spring,自动继承之前的 velocity
  • 无需手动计算 unit velocity,SwiftUI 动画会自动处理
  • 这是 SwiftUI 动画的优势:连续事件触发的动画会自动合并并保持速度

核心启发

  1. 用 Zoom Transition 替代默认 Push 动画:如果你的 App 有”从列表大图进入详情页”的场景,直接启用 Zoom Transition。这是 iOS 18 系统级设计语言,Photos、Messages 都在用。只需添加 modifier 和 source 标记,视觉连续性显著提升。

  2. 在混合视图层级用 SwiftUI Animation 统一管理动效:不要在 UIKit 部分手写 UIView.animate,在 SwiftUI 部分用 withAnimation。新 API 让你用一套动画类型驱动所有视图。特别是 UIViewRepresentable 里的 UIKit 视图,用 context.animate 桥接,外部动画配置会自动传递进去。

  3. 拖拽交互用 interactiveSpring 简化速度计算:凡是”拖拽后弹回”的场景(列表拖动刷新、卡片拖拽),用 .interactiveSpring 配合手势变化,手势结束时的普通 spring 动画会自动继承速度。无需手动计算 velocity/distance 比值,代码更简洁、手感更自然。

关联 Session

评论

GitHub Issues · utterances