大多数浏览器和
Developer App 均支持流媒体播放。
-
在 SwiftUI 中轻松完成高级动画
了解如何利用 SwiftUI 的最新更新将动画提升到新的水平。加入我们,我们将逐步完成动画并构建多个步骤,使用关键帧添加协调的多轨动画效果,并以独特的方式组合 API,让你的 App 焕发活力。
章节
- 0:00 - Introduction
- 2:23 - Animation phases
- 8:12 - Keyframes
- 15:07 - Tips and tricks
资源
相关视频
WWDC23
-
下载
♪ ♪
Tim:大家好 欢迎来到 “在 SwiftUI 中轻松完成高级动画”讲座 我是 Tim 是 SwiftUI 团队的成员 SwiftUI 提供了一套强大的 动画工具 让你的 App 熠熠生辉 动画支持可中断、 基于物理特性实现逼真运动 并且深度融入整个框架 今天 我们将讨论 一些令人兴奋的新工具 让你的 App 动画达到新的水平 在开始之前 让我们快速简单回顾一下 SwiftUI 中你已经了解的动画工具 你可能在其他讲座中见过这个 App 它允许你投票选出 最喜欢的宠物种类 为了简化这个演示 我去掉了其他选项 因为猫显然是最好的选择 在你的 App 中添加动画非常简单 就像使用“withAnimation”或添加 “animation”修饰符一样简单 这些方法 会自动为你提供出色的动画效果 在 App 的状态发生变化后 SwiftUI 会将动画应用于视图 这些动画会从 前一个状态插值到新的状态 就像生活一样 有时最有价值的经历 发生在你不过于关注自己 来自何处或要去何处的时候 有时候你需要走出常规路径 专注于旅程本身 以创造出特别的东西 有些动画不仅仅是 从一个状态到另一个状态的过渡 今天 我将介绍一些强大的新工具 用于构建复杂的多步动画 这些动画可以 定义多个按顺序发生的步骤 而不是仅在两个状态之间进行动画 它们在两种情况下特别有效: 一是重复动画 在视图可见时持续循环; 二是事件驱动动画 例如在事件发生时视图脉动 在本次讲座中 我将介绍一组新的 API 使得构建这类动画变得更加简单 我将首先向大家介绍动画阶段 它让 SwiftUI 自动 在一组预定状态之间切换 构成你的动画 接着 我会演示如何使用关键帧 将动画推向更高层次 最后 我将展示一些高级技巧 让你充分发挥这些 API 的潜力 我觉得我们已经准备好了 就让我们现在开始吧 当我不编写 Swift 代码时 我喜欢去山间跑步 山地赛跑可以非常漫长 超级马拉松可能需要 一整天 甚至多天才能完成 因此我正在开发一个 App 用于规划即将参加的赛事 并在比赛中帮助我记住重要细节 在山地赛跑中 饮食非常重要 但在比赛后期 由于疲劳很容易忘记进食 我在 App 中添加了一个功能 提醒我在正确的时间进食 现在 屏幕底部的提醒正在告诉我 该吃饭了 但有个问题 在比赛后期 由于疲劳 我可能会忽略这个细微的指示器 我真的不想意外地错过一顿饭 所以我会添加一些 动画效果 使这个提醒更加显眼 让我们来关注这个视图 我们想为它添加 一个动画高亮效果 使它更加醒目 为了让这个视图动画化 我们将应用“.phaseAnimator”修饰符 当你使用 phase animator 修饰符时 你提供了一系列状态 定义了多部分动画中的各个步骤 然后 SwiftUI 会自动在 这些状态之间进行动画过渡 在这种情况下 我们只是在两个状态之间进行动画: 高亮和非高亮 因此我们可以简单地使用布尔值 接下来 我们会应用一些修饰符 根据当前阶段来改变视图的外观 我们将从不透明度修饰符开始: 当高亮时 视图完全不透明 否则为 50% 透明 这样视图就开始动画化了 让我们讨论一下 SwiftUI 在你的帮助下所做的事情 在我们的视图中 我们给 phase animator 修饰符提供了两个阶段: false 和 true 当视图首次出现时 第一个阶段处于活动状态 使得视图为 50% 透明 然后 SwiftUI 立即开始到 下一个阶段的动画过渡 视图完全不透明 当动画完成时 SwiftUI 再次前进 我们只有两个阶段 所以我们循环回到开始 这使得我们的动画 在两个状态之间循环 当然 你也可以定义包含多个阶段 和任意数量其他视图修饰符的动画 稍后我会演示 现在 当视图进行动画时 效果相当微妙 与其改变不透明度 不如尝试改变前景样式 当高亮时 我们使用红色 否则退回到主要前景样式 这样更加显眼了 但是动画有点突兀 默认情况下 SwiftUI 使用弹簧动画 虽然弹簧动画 对处理动态状态变化很有用 但在本例中 我们想要更加平滑、一致的动画 我们可以通过添加一个 尾随的“animation”闭包来改变动画 如果我们想 为每个阶段使用不同的动画 则传入正在动画的阶段 但在这种情况下 我总是希望 使用相同的渐进减速动画 并自定义持续时间 以减慢动画速度 现在 你通常不会在交互状态改变中 使用持续时间为一秒的动画 因为你不会希望 让用户等待动画完成 但在本例中 我们正在构建一种环境效果 所以事物移动得慢一点没关系 就像我错过吃饭的速度一样 现在 我们已经解决了 比赛中期营养的紧急问题 让我们再看一种 使用动画阶段的方法: 通过事件触发的动画 我花了一段时间开发我的 App 并且已经添加了 查看朋友们参加的比赛的功能 表情符号显示他人留下的反应 每个跑步者有时 都会问自己:我为什么要这样做? 我为什么要报名参加这么多里程? 我们的 App 最起码可以通过 在别人喜欢某场比赛时增加一些刺激 来满足外部验证的需求 我们将添加一个 每当有人添加反应时就播放的动画 我们将首先定义我们动画的阶段 与之前在两种状态间 简单交替的示例不同 我们想要一个更复杂的动画 枚举是定义动画步骤列表的好方法 我们已经添加了三种情况: 初始外观情况、 视图上移情况 以及视图放大情况 为了简化我们的视图主体 我们将向这个枚举添加计算属性 定义我们将应用的不同效果 我希望视图在动画期间跳起来 所以我已经添加了 一个计算的垂直偏移属性 我切换枚举 以返回每种情况的正确偏移量 同样 我还添加了 两个附加的计算属性 来确定视图的缩放和前景样式 我不会在这里显示实现 但它们也使用 switch 语句 就像垂直偏移属性一样 现在 让我们回到 我们的视图并添加动画 我们添加了 phaseAnimator 修饰符 但这次 我们给它一个“trigger”值 当我们给 phaseAnimator 修饰符一个触发器值时 它会观察你指定的值进行更改 当发生更改时 phaseAnimator 修饰符 开始按照你指定的阶段进行动画 使用我们在 阶段类型上定义的计算属性 我们对视图应用修改器 从技术上讲 这个动画做得对 但感觉不太好 它有点缓慢 我们将为每个转换自定义动画 以获得我们想要的效果 包括几种不同的弹簧动画 这看起来好多了! 但如果我们想进一步 优化这个动画怎么办? 当有人完成 50 或 100 英里的比赛时 我们想要为他们制作一个动画 让他们在收到一些应得的赞美时 确信这些完成的里程都是值得的 当你需要更多控制权时 还有另一个强大的工具:关键帧 接下来 我将向你展示 如何使用关键帧来定义复杂、 协调的动画 并完全控制时间和移动 首先 让我们讨论一下关键帧 与迄今为止使用的阶段有何不同 阶段定义了一个接一个地 提供给你的视图的离散状态 SwiftUI 使用 你已经了解的相同动画类型 在这些状态之间进行动画处理 对于可以建模为离散状态的动画 这种方法非常有效 当发生状态转换时 所有属性同时进行动画处理 然后 当动画完成时 SwiftUI 动画到下一个状态 在动画的所有阶段中都是如此 但是 如果我们想要 分别动画每个属性怎么办? 这就是关键帧发挥作用的地方 关键帧允许你在 动画中的特定时间点定义值 演示一下 我将从旋转效果 开始对此视图进行动画处理 这里的点表示关键帧: 在动画的每个点上使用的角度 当播放动画时 SwiftUI 在这些关键帧之间插值 然后我们可以使用 这些值来对视图应用修改器 关键帧允许你在相同时间内 通过定义各自独特的时间轴 独立地动画多个效果 这真的很强大 因为你可以使用关键帧 来驱动 SwiftUI 中的任何修饰符 在这个示例中 我们使用关键帧 来驱动几个其他轨道 包括垂直拉伸、缩放和平移 让我们回到我们的视图 看看在代码中是什么样子 我已经有一个动画构建的想法 所以我的第一步是 定义驱动动画的属性 要做到这一点 我将创建一个新的结构 其中包含 所有将被独立动画化的不同属性 关键帧可以动画 任何符合“Animatable”协议的值 请注意 有几个属性使用了“Double” 现在符合“Animatable”规范 与阶段不同 你在模型中为它们建模的是 分离的离散状态 关键帧会生成 你指定类型的插值 当动画正在进行时 SwiftUI 会在每一帧 为你提供这种类型的值 以便你可以更新视图 接下来 我们添加 keyframeAnimator 修饰符 该修饰符类似于 之前使用的 phaseAnimator 但接受关键帧 请注意 我们提供了一个 结构体的实例作为初始值 我们定义的关键帧 将在该值上应用动画 接下来 我们为结构体的 每个属性在视图上应用修饰符 最后 我们开始定义关键帧 正如我之前提到的 关键帧允许你用不同关键帧 为不同属性构建复杂的动画 为了实现这一点 关键帧被组织成轨道 每个轨道控制 你正在进行动画的类型的不同属性 这由你在创建轨道时 提供的键路径指定 在这里 我们为 缩放属性添加关键帧 我们首先添加一个线性关键帧 重复初始缩放值 并保持 0.36 秒 如果你想知道 我是如何确定 0.36 秒的 我是通过尝试不同的值 来改变动画的感觉发现的 这是关于关键帧的一个重要要点 制作适合你 App 的动画 可能需要一些试验 Xcode 中的预览 是调整动画的好方法 接下来 我们添加 一个“SpringKeyframe” 它使用弹簧函数将值拉向目标 并且我们指定了一个持续时间 对于具有固定持续时间的 弹簧关键帧 这意味着弹簧函数 只会在该持续时间内对值进行动画 在那之后 将开始插值到下一个关键帧 最后 我将添加另一个弹簧关键帧 将缩放动画返回到 1.0 不同类型的关键帧 控制着值的插值方式 我们已经看到了 线性关键帧和弹簧关键帧 实际上 有四种不同类型的关键帧 我将解释它们的不同之处: 线性关键帧在向量空间中 对上一个关键帧进行线性插值 弹簧关键帧 如其名称所示 使用弹簧函数来插值到目标值 从上一个关键帧过渡到目标值 立方体关键帧使用立方贝塞尔曲线 在关键帧之间进行插值 如果你在序列中 组合多个立方体关键帧 得到的曲线等效于 Catmull-Rom 样条曲线 最后 移动关键帧 立即跳转到一个值 没有插值 每种类型的关键帧 都支持自定义 让你完全控制 你可以在一个动画中 混合并匹配不同类型的关键帧 SwiftUI 在关键帧之间保持速度 以确保动画保持连续性 回到我们的视图 我们准备添加下一个轨道 在这里 我们使用线性和弹簧关键帧 来动画垂直平移 在视图跳起之前 它会 先向后拉 预示着即将发生的动作 我们使用一个弹簧关键帧来建模 在视图移动之前 它会短暂地向下拉 这看起来很好 但我们还有两个属性需要动画: 垂直拉伸和旋转 我们将从垂直拉伸开始 对此我们将使用立方关键帧 同样 这可能需要 一些试错来达到理想效果 但是不要犹豫尝试 使用关键帧来构建不同类型的动画 挤压和拉伸 为这个动画增添了更多的活力 最后 我们将旋转动画也做出来 这看起来很棒 我们之前看到的那些曲线? 那些是我们 刚刚构建的动画的可视化 你可以添加额外的轨道 来应用任何 SwiftUI 修饰符 我已经乐在其中 探索不同的组合 让我们来回顾一下关键帧的模型 关键帧是预定义的动画 这意味着当 UI 需要流畅和交互时 它们不是替代普通的 SwiftUI 动画的 相反 将关键帧看作 可以播放的视频剪辑 它们给你很多 控制权 但也有一个权衡 因为你确切地 指定了动画的进展方式 关键帧动画无法像 弹簧那样优雅地重新定位 所以一般最好避免 在动画进行时更改关键帧 关键帧动画对你 定义类型的值进行动画处理 然后你可以使用它 来对视图应用修饰符 你可以使用 一个关键帧轨道来驱动一个修饰符 或者组合不同的修饰符 这取决于你 由于动画是根据你定义的值进行的 更新发生在每一帧上 所以在应用关键帧动画时 应避免执行任何昂贵的操作 最后 我将演示 如何使用关键帧做更多的事情 我的 App 包含一个比赛地图 显示每一段赛程的路线 我想要添加一个动画 自动缩放并跟随航线 幸运的是 现在 MapKit 允许我使用关键帧来移动相机 在这里 我使用了“地图”视图来展示航线 我的视图已经有了一条路径 其中包含了比赛中 一段路径上的所有坐标 为了构建我们的导览 我们将添加一个状态属性 和一个按钮来改变它 最后 我们使用了新的 “mapCameraKeyframeAnimator”修饰符 我们给它一个触发值 然后添加关键帧 就像我们在之前示例中 对心形图标所做的一样 每当触发值改变时 地图将使用这些关键帧进行动画 关键帧的最终值决定了 动画结束时使用的相机值 最后 我们按下按钮 导览开始 如果用户 在进行动画时进行手势操作 动画将被取消 用户将完全控制相机 通过独立动画化中心坐标、 方向和距离 我们能够平滑地沿着航线进行动画 然后缩小画面以获得鸟瞰视图 最后 我想演示一下 关键帧如何手动评估 以驱动任何你能想到的效果 我们已经看到了 “keyframeAnimator”修饰符 除了修饰符之外 你可以使用“KeyframeTimeline”类型 来捕获一组关键帧和轨道 你可以使用一个初始值 和定义动画的关键帧轨道 对该类型进行初始化 就像使用视图修饰符一样 KeyframeTimeline 提供的 API 可以为你提供持续时间 该持续时间 等于最长轨道的持续时间 你可以在动画范围内 计算任何时间点的值 这使得可以轻松使用 Swift Charts 来可视化关键帧 我在之前展示的 曲线可视化中也使用过 这也意味着你可以 随心所欲地使用关键帧定义的曲线 或者与其他 API 创造性地结合关键帧 例如 使用几何代理根据滚动位置 来擦除由关键帧驱动的效果 或者使用 “TimelineView”根据时间更新 如果你不确定何时使用这个 没关系 这是一个高级工具 大多数开发者 会选择使用视图修饰符 但它作为一个构建块存在 我很期待 看到你如何将它融入你的 App 我们的探索之旅结束了 我希望你 期待使用这个新的 API 家族 请记住:使用阶段进行链接动画 它们使用 你已经了解的所有现有动画类型 所以你可以快速上手 对于需要完全控制的复杂动画 请使用关键帧 最后 探索愉快 动画的世界令人兴奋 希望这些新工具能够 引导你和你的 App 走向新的领域 谢谢! ♪ ♪
-
-
0:42 - Scale Animation
struct Avatar: View { var petImage: Image @State private var selected: Bool = false var body: some View { petImage .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { withAnimation { selected.toggle() } } } }
-
3:13 - Boolean Phases
OverdueReminderView() .phaseAnimator([false, true]) { content, value in content .foregroundStyle(value ? .red : .primary) } animation: { _ in .easeInOut(duration: 1.0) }
-
6:20 - Custom Phases
ReactionView() .phaseAnimator( Phase.allCases, trigger: reactionCount ) { content, phase in content .scaleEffect(phase.scale) .offset(y: phase.verticalOffset) } animation: { phase in switch phase { case .initial: .smooth case .move: .easeInOut(duration: 0.3) case .scale: .spring( duration: 0.3, bounce: 0.7) } } enum Phase: CaseIterable { case initial case move case scale var verticalOffset: Double { switch self { case .initial: 0 case .move, .scale: -64 } } var scale: Double { switch self { case .initial: 1.0 case .move: 1.1 case .scale: 1.8 } } }
-
9:48 - Keyframes
ReactionView() .keyframeAnimator(initialValue: AnimationValues()) { content, value in content .foregroundStyle(.red) .rotationEffect(value.angle) .scaleEffect(value.scale) .scaleEffect(y: value.verticalStretch) .offset(y: value.verticalTranslation) } keyframes: { _ in KeyframeTrack(\.angle) { CubicKeyframe(.zero, duration: 0.58) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.degrees(-16), duration: 0.125) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.zero, duration: 0.125) } KeyframeTrack(\.verticalStretch) { CubicKeyframe(1.0, duration: 0.1) CubicKeyframe(0.6, duration: 0.15) CubicKeyframe(1.5, duration: 0.1) CubicKeyframe(1.05, duration: 0.15) CubicKeyframe(1.0, duration: 0.88) CubicKeyframe(0.8, duration: 0.1) CubicKeyframe(1.04, duration: 0.4) CubicKeyframe(1.0, duration: 0.22) } KeyframeTrack(\.scale) { LinearKeyframe(1.0, duration: 0.36) SpringKeyframe(1.5, duration: 0.8, spring: .bouncy) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.verticalTranslation) { LinearKeyframe(0.0, duration: 0.1) SpringKeyframe(20.0, duration: 0.15, spring: .bouncy) SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy) SpringKeyframe(0.0, spring: .bouncy) } } struct AnimationValues { var scale = 1.0 var verticalStretch = 1.0 var verticalTranslation = 0.0 var angle = Angle.zero }
-
15:22 - Map Keyframes
struct RaceMap: View { let route: Route @State private var trigger = false var body: some View { Map(initialPosition: .rect(route.rect)) { MapPolyline(coordinates: route.coordinates) .stroke(.orange, lineWidth: 4.0) Marker("Start", coordinate: route.start) .tint(.green) Marker("End", coordinate: route.end) .tint(.red) } .toolbar { Button("Tour") { trigger.toggle() } } .mapCameraKeyframeAnimation(trigger: playTrigger) { initialCamera in KeyframeTrack(\MapCamera.centerCoordinate) { let points = route.points for point in points { CubicKeyframe(point.coordinate, duration: 16.0 / Double(points.count)) } CubicKeyframe(initialCamera.centerCoordinate, duration: 4.0) } KeyframeTrack(\.heading) { CubicKeyframe(heading(from: route.start.coordinate, to: route.end.coordinate), duration: 6.0) CubicKeyframe(heading(from: route.end.coordinate, to: route.end.coordinate), duration: 8.0) CubicKeyframe(initialCamera.heading, duration: 6.0) } KeyframeTrack(\.distance) { CubicKeyframe(24000, duration: 4) CubicKeyframe(18000, duration: 12) CubicKeyframe(initialCamera.distance, duration: 4) } } } }
-
16:26 - KeyframeTimeline
// Keyframes let myKeyframes = KeyframeTimeline(initialValue: CGPoint.zero) { KeyframeTrack(\.x) {...} KeyframeTrack(\.y) {...} } // Duration in seconds let duration: TimeInterval = myKeyframes.duration // Value for time let value = myKeyframes.value(time: 1.2)
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。