大多数浏览器和
Developer App 均支持流媒体播放。
-
提升 UI 动画和过渡效果
探索如何在导航和演示中采用缩放过渡效果,以提升 App 中的连续感,并了解如何使用 SwiftUI 动画功能为 UIKit 视图创建动画效果,以便更轻松地构建具有连续感的动画。
章节
- 0:00 - Introduction
- 1:34 - New transitions!
- 2:07 - Zoom transitions in SwiftUI
- 3:02 - Zoom transitions in UIKit
- 4:15 - UIKit view controller life cycle and callbacks
- 7:02 - Additional tips for UIKit
- 8:10 - SwiftUI animation
- 9:46 - Animating representables
- 11:20 - Gesture-driven animations
资源
相关视频
WWDC23
-
下载
欢迎来到 “提升 UI 动画和过渡效果” 在这里我很高兴地告诉大家 SwiftUI、UIKit 和 AppKit 动画 相互之间能够比以往更加友好地融合 为了庆祝这种友好融合 我制作了一些象征友好融合的手链 借助这种友好融合的 强大力量 我今天才能与大家分享 一些卓越的新功能和 API 如果你想要了解如何在 App 中 添加炫酷的全新 流畅缩放过渡效果 让你轻点的单元格 放大填满屏幕… …或者使用 SwiftUI 动画 来打造流畅的互动 甚至使用 UIKit 或 AppKit 视图 那么本次讲座内容将非常适合你! 请注意 本视频中的所有动画 都已放慢为半速 为了展示这些 API 我开发了这个 App 来规划象征友好融合的手链设计 每个手链都使用 API 名称中各单词的 首字母对我喜欢的 API 进行编码 这个代表 SwiftUI.Animation.spring.repeatForever() 现在让我们来详细了解在哪些因素的 作用下实现了如此紧密的友好融合 首先 我们将讨论 用于导航和呈现的 一些新的高级过渡 接下来 我们将探讨*底层逻辑* 来了解 SwiftUI 动画 与 UIKit 和 AppKit 之间的 一些新集成 它们能为实现这些新的过渡提供助力 然后 我们将讨论如何 通过混合层次结构中的 Representable 类型 来桥接 SwiftUI 动画 最后我们将讨论 当涉及连续手势时 为 UIViews 和 NSViews 制作动画 功能如何变得*额外*强大 让我们来了解一下新的过渡! iOS 18 中新增了一种缩放过渡 有了这种新的过渡 你轻点的单元格会变为新视图 它不仅采用全新的视觉外观 还具有持续互动性 让你能够在开始阶段 或过渡过程中 抓取并拖移它 在 App 的某些部分 你需要针对大型单元格实现过渡 缩放过渡 可以在整个过渡过程中 始终在屏幕上显示相同的 UI 元素 从而增加 App 的连贯感 让我们在代码中采用这一过渡 这里是我已经为 NavigationLink 编写的一些代码 作用是显示手链预览 并在轻点时将手链 推送到完整的手链编辑器 这段代码的效果是 实现默认的滑动过渡 即新视图 从后缘滑入 但这种情况非常适合 使用缩放过渡
要选择使用缩放过渡 我们必须完成两项操作 首先 声明我们需要缩放过渡! 这意味着要在呈现的视图中 添加 navigationTransitionStyle 修饰符 并指定缩放过渡 其次 我们要将这个修饰符 连接到源视图 这样系统就知道 要对哪个视图进行缩放 在这两处 我们指定了 相同的标识符和命名空间 这样 SwiftUI 就能知道 哪个预览对应哪个呈现的视图 现在我们开始缩放!
通过 UIKit 来采用 缩放过渡的过程与上述过程类似 在这里我编写了一些代码 作用是当轻点某个手链预览时 为这个手链创建一个编辑器 并将它推送到 当前的导航控制器
在这里 要采用缩放 请首先在被推送的视图控制器上 指定要进行缩放 其次 提供视图作为 缩放过渡的源 这样就可以了!
请注意 传递给缩放过渡的 闭包将在放大时运行 并在缩小时再次运行 而且它会捕获一个稳定的标识符 在本例中 手链模型对象 可用于获取视图 而不是直接捕获视图 在源视图可能被重复使用的情况下 例如在集合视图中 这一点非常重要
现在 我还可以在手链之间 轻扫而不离开编辑器 这意味着编辑器中的手链 可能会改变 要处理这种情况 并对正确的手链预览进行缩放 可以使用传递到闭包的上下文 从编辑器中 检索当前手链
顺便说一下 在 SwiftUI 和 UIKit 中 与 fullScreenCover 和 Sheet Presentation API 配合使用的 API 是相同的 现在 对于 UIKit App 我们将花点时间深入探讨 了解这些新的 流畅过渡如何 与视图控制器生命周期 和外观回调配合使用 对于使用 SwiftUI 的朋友来说 你将简单了解在 UIKit 中 使用回调时我们要做些什么
在整个示例中 我们将考虑被推送 手链编辑器视图控制器的状态 首先 我将依次阐述 各种情况来展示 系统是如何工作的
红点代表 编辑器的当前状态 当这一状态通过回调机制 发生改变时便会调用方法 正常情况下 如果推送在没有用户 进行互动的情况下运行完成 那么编辑器一开始 就会处于 Disappeared 状态
然后在过渡期间它会经历 Appearing 状态来调用 viewWillAppear、 isAppearing 和 didAppear 并在过渡完成时 以 Appeared 结束 同样 如果弹出运行完成 编辑器会在过渡期间 经历 Disappearing 状态 然后在结束时 恢复 Disappeared 状态 无论弹出是通过轻点返回按钮 还是通过交互式轻扫启动 情况都是如此
现在倒回去 因为我想看一下 取消弹出是如何工作的 如果我只是稍微拖移 并按住不放 编辑器会在弹出过渡开始时 进入 Disappearing 状态 然后 如果我抬起手指 以取消弹出 动画就会运行完成 但在结束时 视图控制器 会直接进入 Appearing 状态 最后在一个运行循环周期内 进入 Appeared 状态 好了 以上就是现有 过渡场景中的回调时序
现在 我将展示当你实际测试 这些新过渡的流畅性时 会发生什么 我们将回到开始时编辑器 处于 Disappeared 状态的情况 我启动推送 现在编辑器 处于 Appearing 状态 现在让我们考虑一下 如果我在推送过程中启动弹出 无论通过轻点返回按钮 还是向后轻扫 会发生什么情况 在这种情况下 推送不会取消 相反 推送会立即完成 因此编辑器会 直接进入 Appeared 状态 然后在同一个运行循环周期内 弹出过渡启动 编辑器进入 Disappearing 状态 从这时开始将变为 正常的弹出过渡 它可能完成 也可能被取消
取消推送与取消弹出的 工作原理不同 这是有意为之 从概念上讲 系统绝对不会 取消中断的推送 相反 推送总是会 转换为弹出 从被推送视图 控制器的角度来看 它始终处于 Appeared 状态 这意味着系统 将始终运行完整的 回调循环 呼呼! 我想我需要另一个象征着 友好融合的手链来提醒我 外观回调对我来说 真是太有用了
这项新功能在很大程度上 能够与现有代码兼容 我为 UIKit App 提供了额外的提示 以确保代码在这个新环境中完美运行 因为在这里推送和弹出过渡 随时可能开始 做好准备迎接 随时开始的新过渡 不要试图 将过渡状态 与非过渡状态 区别对待 在这里 如果过渡正在运行 则轻点处理程序无法调用 push 请直接调用 push 而无需考虑过渡是否正在运行
尽可能减少 临时过渡状态
状态越少 其他代码依赖于 过渡状态的可能性越小 而且也少了一项清理工作
但如果你确实需要 在过渡期间跟踪状态 可以通过 viewDidAppear 或 viewDidDisappear 来重置状态 它们在过渡结束时 才会被调用 如果你使用的是 导航控制器的委托方法 will 和 didShowViewController 也同样适用 最后 将 SwiftUI 整合到你的 App 中 SwiftUI 更多地采用功能性编程理念 而不是命令式编程 因此 通常能更好地适应不断变化的世界 现在 为了给这些新的过渡提供助力 推出了一些令人惊叹的新底层 API 它们用于使用 SwiftUI 动画 为 UIKit 和 AppKit 视图制作动画 让我们来看看 如何使用它们构建自定 UI 这是我的手链编辑器 我可以轻点 底部盒子中的珠子 将它添加到手链末端
如果使用 UIKit 或 AppKit 实现这个 UI 我们将使用现有的动画 API 在调用的参数中 描述弹簧 然后在闭包中 更新视图属性 SwiftUI 也有类似的语法 我们使用 SwiftUI 动画类型来描述动画 然后在闭包中更新状态 但是 如果你能充分发挥 两者的优势 那岂不是太棒了? 现在在 iOS 18 中 你可以做到了! 你可以使用 SwiftUI 动画类型 为 UIKit 和 AppKit 视图制作动画 这样一来 你就能利用包括 SwiftUI CustomAnimations 在内的 全套 SwiftUI 动画类型 来为 UIKit 视图制作动画!
如果你的代码与 CALayers 配合使用 那么在使用这个新 API 时 需要考虑几个方面的影响 现有 UIKit API 会生成 CAAnimation 然后将它添加到视图层中 但是 SwiftUI 动画 不会创建 CAAnimation 而是直接对视图层的 呈现值进行动画处理 这些呈现值仍会 反映在呈现层中
现在 我们已经讨论了如何为 UIKit 和 AppKit 视图制作动画 接下来讨论在 UIViewRepresentable 等 Representable 类型的 上下文中为视图制作动画的方法 我已经为我的珠盒设计了 UIView 它名为 BeadBox 我会使用这个 representable 包装器将它嵌入我的 SwiftUI App 它有一个盖子 我可以通过这个 “isOpen”绑定来打开或合上盖子 现在盖子只是在我打开和合上 珠盒时出现和消失 但我想要制作动画效果!
要在不知道 BeadBoxWrapper 是否使用 UIKit 实现的情况下 实现这种效果 使用的方法自然是 为绑定添加 animated 修饰符 如果 BeadBoxWrapper 是 使用 SwiftUI 实现的 这种方法也行得通! 但是 由于 BeadBoxWrapper 是 在 lid 下使用 UIKit 实现的 因此我们需要自行桥接动画
在这里 我在 context 中使用了 新的“animate”方法 这样我就能将与这一更新相关的 任何事务处理动画应用于 我在“updateUIView”方法中 对 UIView 所做的所有更改 它会抓取事务处理存在的 所有 SwiftUI 动画 并桥接到 UIView.animate 方法 来上下滑动盖子
成功了!
如果当前事务处理 不带动画效果 则会立即以内联方式 调用动画和完成 因此无论更新是否带动画效果 这段代码都能正常工作 值得注意的是 在 SwiftUI 视图和 UIViews 中 运行的单个动画可以完美同步!
现在我们已经讨论了 如何运行动画 来响应不同的操作 接下来让我们来看看 这些相同的 API 如何 在运行以响应连续手势时 发挥更强大的作用 返回珠盒 我想使用平移手势 将一颗珠子从珠盒中拖出 然后使珠子 飞到手链的末端
这里是一些 UIKit 代码 用于响应平移手势 以将一颗珠子从珠盒中拖出 当平移手势发生变化时 珠子的中心会基于 手势的过渡情况进行更新 当手势结束时 珠子会被放置到最终位置 为了制作这一动画 我们需要根据 珠子的当前速度 计算弹簧的 初始速度 并且我们必须用珠子 从当前位置到最终位置的距离 执行除法运算 来转换为单位速度 但是 如果我们不必这样做 不是更简单吗? 是的! SwiftUI 动画已经可以 通过在做出手势期间 合并速度信息 从而在手势结束时 保留这些信息 如这段等效的 SwiftUI 代码所示 这些代码可以实现相同的效果 手势结束时 不需要计算初始速度
现在 也可以运用同样的技术 来为 UIView 制作动画 在这种情况下同样的 SwiftUI 动画 也能传递给新 UIView animate 方法
当我在屏幕上拖移时 手势会随着我手指的移动 不断触发更改事件 而每个事件都会创建一个新的 “.interactiveSpring”动画 每个新动画都会 重定向上一个动画 然后当手势结束时 会创建最后一个 非交互式弹簧动画 这个弹簧使用 interactiveSprings 的速度 以连贯的速度 推进动画 你难道不喜欢连贯的速度吗? 它可以带来最佳体验! 有关动画和过渡的介绍 到此结束 现在看你的了! 在需要缩放大型单元格的地方 采用缩放过渡 以增强整个 App 的 视觉连贯性 缩放过渡 具有持续互动性 因此请确保你的代码已做好准备 来迎接随时开始的过渡! 开始使用 SwiftUI.Animation 为 UIKit 和 AppKit 视图制作动画 尤其是在 UI 中 保持连贯的速度非常重要 这将大大简化你的代码 并让你的动画带来更顺畅的感受! 想要进一步了解 全套 SwiftUI 动画 我朋友 Kyle 的视频“探索 SwiftUI 动画”中包含你需要的所有信息 想要深入了解弹簧 请观看我朋友 Jacob 的视频 “用弹簧制作动画” 将这些知识分享给你所有的朋友! 更有创意的做法是对这些知识 进行编码 制成时尚手链与大家分享!
-
-
2:10 - Zoom transition in SwiftUI
NavigationLink { BraceletEditor(bracelet) .navigationTransitionStyle( .zoom( sourceID: bracelet.id, in: braceletList ) ) } label: { BraceletPreview(bracelet) } .matchedTransitionSource( id: bracelet.id, in: braceletList )
-
3:02 - Zoom transition in UIKit
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) }
-
8:39 - Animate UIView with SwiftUI animation
UIView.animate(.spring(duration: 0.5)) { bead.center = endOfBracelet }
-
9:56 - Animating representables
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() } } }
-
11:39 - Gesture-driven animations
switch gesture.state { case .changed: UIView.animate(.interactiveSpring) { bead.center = gesture.translation } case .ended: UIView.animate(.spring) { bead.center = endOfBracelet } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。