【Lottie】让设计稿上的动效直接"活"在 App 里
iOS三方库精读 · 第 5 期
一、一句话介绍
Lottie 是由 Airbnb 开源的跨平台动画库,它让 Adobe After Effects 导出的 JSON 动效文件在 iOS / Android / Web 上以矢量方式实时渲染,彻底消灭"设计交付 → 开发还原"之间的信息损耗。
| 属性 | 详情 |
|---|---|
| ⭐ Stars | 25k+(GitHub) |
| 最新稳定版 | 4.5.x |
| License | Apache 2.0 |
| 支持平台 | iOS 14+ / macOS 11+ / tvOS 14+ / visionOS 1+ |
| SwiftUI 原生支持 | ✅(4.0 起) |
二、为什么选择它
原始痛点
在没有 Lottie 之前,设计师在 After Effects 中做好一个 3 秒的加载动画,开发要干这些事:
- 看着动效视频,逐帧拆解关键帧参数
- 用
CAKeyframeAnimation/CAAnimationGroup手写每一条时间曲线 - 颜色稍有偏差,回去对着设计稿截图像素级校准
- 动效改版?从头重写
这套流程不仅耗时(一个中等复杂动效需 1~3 天还原),还存在不可避免的还原偏差。
核心优势
- 零还原成本 :AE 安装 bodymovin 插件,导出 JSON,开发侧
LottieView(animation: .named("xxx"))一行接入,100% 还原 - 矢量渲染,无损缩放:JSON 中存储的是贝塞尔曲线参数,任意分辨率下锐利清晰,不像 GIF 有马赛克
- 极小体积:同等视觉效果的动效,Lottie JSON 通常比 GIF 小 80%~90%
- 运行时动态换色 :通过
DynamicPropertyAPI,在不修改 JSON 文件的前提下替换任意图层的颜色、图片、文字,支持深色模式适配 - 精细帧控制:可播放任意帧区间、设置播放速度、绑定手势进度,实现交互式动画
三、核心功能速览
基础层 新手必读:环境配置与基础播放
集成方式
Swift Package Manager(推荐)
在 project.yml(XcodeGen)中添加:
yaml
packages:
lottie-ios:
url: https://github.com/airbnb/lottie-ios.git
minorVersion: 4.5.0
dependencies:
- package: lottie-ios
product: Lottie
或在 Xcode → File → Add Package Dependencies 搜索:
arduino
https://github.com/airbnb/lottie-ios
CocoaPods
ruby
pod 'lottie-ios', '~> 4.5'
准备动画 JSON
- 在 After Effects 中安装 bodymovin 插件
- 渲染导出 → 选择 JSON 格式
- 将
xxx.json拖入 Xcode 工程(确保勾选 Target Membership) - 或从 LottieFiles.com 下载社区免费素材
SwiftUI 基础用法
swift
// Swift 5.9+ / iOS 17+
import SwiftUI
import Lottie
struct ContentView: View {
var body: some View {
LottieView(animation: .named("loading")) // 对应 loading.json
.playing(.fromProgress(0, toProgress: 1, loopMode: .loop))
.resizable()
.scaledToFit()
.frame(height: 200)
}
}
UIKit 基础用法
swift
import Lottie
let animationView = LottieAnimationView(name: "loading")
animationView.loopMode = .loop
animationView.contentMode = .scaleAspectFit
animationView.play()
view.addSubview(animationView)
进阶层 最佳实践:常用 API 与核心配置
LottieView 常用修饰符(SwiftUI)
swift
LottieView(animation: .named("confetti"))
// 播放控制
.playing(.fromProgress(0, toProgress: 1, loopMode: .playOnce))
.animationSpeed(1.5) // 1.5 倍速
// 完成回调
.animationDidFinish { completed in
print("播放完成: \(completed)")
}
// 动态换色
.valueProvider(
ColorValueProvider(LottieColor(r: 1, g: 0.8, b: 0, a: 1)),
for: AnimationKeypath(keypath: "**.Color")
)
LottieAnimationView 常用 API(UIKit)
swift
let av = LottieAnimationView(name: "success")
// 播放控制
av.play() // 从当前进度播放到末尾
av.pause() // 暂停
av.stop() // 停止并重置到开头
// 帧区间播放
av.play(fromFrame: 0, toFrame: 60, loopMode: .playOnce) { finished in
// finished = true 表示正常播放完毕,false 表示被中断
}
// 手动控制进度(绑定手势)
av.currentProgress = 0.5 // 跳到 50% 位置
// 速度与循环
av.animationSpeed = 2.0
av.loopMode = .loop // .playOnce / .loop / .autoReverse / .repeat(3)
播放状态枚举速查
swift
// SwiftUI LottieView.play() 参数
.playing() // 无限循环
.playing(.fromProgress(0, toProgress: 1, loopMode: .loop))
.playing(.paused) // 暂停
.playing(.paused(at: .progress(0.5))) // 暂停在 50%
.playing(.paused(at: .frame(30))) // 暂停在第 30 帧
深入层 源码视角:渲染架构与关键模块
渲染器选择
Lottie 4.x 提供三种渲染器,可在初始化时指定:
swift
// 默认:Core Animation 渲染器,GPU 友好,支持大部分 AE 特性
LottieConfiguration.shared.renderingEngine = .automatic
// 强制 Core Animation(推荐生产环境)
LottieConfiguration.shared.renderingEngine = .coreAnimation
// 主线程渲染器(兼容性最佳,性能较差,4.x 已逐步弃用)
LottieConfiguration.shared.renderingEngine = .mainThread
关键模块职责
| 模块 | 职责 |
|---|---|
LottieAnimation |
JSON 解析,将 bodymovin 数据映射为内存模型 |
AnimationLayer |
CALayer 树构建器,将动画模型转为 Core Animation 结构 |
AnimationContext |
时间线管理,处理帧率、时间缩放、循环逻辑 |
ValueProvider |
动态属性注入点,DynamicProperty 系统的核心抽象 |
四、实战演示
场景:电商 App 加载页 + 下单成功动效,含动态换色适配品牌主色
swift
import SwiftUI
import Lottie
// MARK: - 加载页动效(带品牌色动态换色)
struct BrandLoadingView: View {
@Environment(\.colorScheme) var colorScheme
/// 品牌主色(浅色/深色模式自适应)
var brandColor: LottieColor {
colorScheme == .dark
? LottieColor(r: 1.0, g: 0.85, b: 0.0, a: 1.0) // 深色:亮黄
: LottieColor(r: 0.9, g: 0.6, b: 0.0, a: 1.0) // 浅色:金黄
}
var body: some View {
ZStack {
Color(.systemBackground).ignoresSafeArea()
LottieView(animation: .named("loading_ring"))
.playing(.fromProgress(0, toProgress: 1, loopMode: .loop))
.animationSpeed(0.8)
// 替换 JSON 中所有名为 "Primary Color" 图层的颜色
.valueProvider(
ColorValueProvider(brandColor),
for: AnimationKeypath(keypath: "**.Primary Color.Color")
)
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
Text("加载中...")
.font(.subheadline)
.foregroundStyle(.secondary)
.offset(y: 80)
}
}
}
// MARK: - 下单成功动效(单次播放 + 完成回调)
struct OrderSuccessView: View {
@State private var showContent = false
@Binding var isPresented: Bool
var body: some View {
VStack(spacing: 20) {
LottieView(animation: .named("success_checkmark"))
.playing(.fromProgress(0, toProgress: 1, loopMode: .playOnce))
.animationDidFinish { _ in
// 动效播放完毕后展示订单详情
withAnimation(.easeIn(duration: 0.3)) { showContent = true }
}
.resizable()
.scaledToFit()
.frame(height: 160)
if showContent {
VStack(spacing: 8) {
Text("下单成功!")
.font(.title2).bold()
Text("预计 3~5 个工作日送达")
.font(.subheadline)
.foregroundStyle(.secondary)
Button("查看订单") { isPresented = false }
.buttonStyle(.borderedProminent)
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.padding(32)
}
}
要点说明:
AnimationKeypath(keypath: "**.Primary Color.Color")中**是通配符,匹配任意深度路径- 图层名称需与 AE 中一致,设计师导出前应统一命名规范
animationDidFinish在 SwiftUI 中是 View 修饰符,回调在主线程执行
五、源码亮点
进阶层 值得借鉴的用法
链式 ValueProvider 叠加
多个 valueProvider 可链式叠加,分别控制不同图层:
swift
LottieView(animation: .named("badge"))
.playing()
.valueProvider(
ColorValueProvider(LottieColor(r: 1, g: 0.3, b: 0.3, a: 1)),
for: AnimationKeypath(keypath: "Background.Color")
)
.valueProvider(
ColorValueProvider(LottieColor(r: 1, g: 1, b: 1, a: 1)),
for: AnimationKeypath(keypath: "Icon.**.Color")
)
.valueProvider(
TextValueProvider("99+"),
for: AnimationKeypath(keypath: "Badge.Text")
)
手势驱动进度(类似 pull-to-refresh)
swift
struct GestureDrivenAnimation: View {
@GestureState private var dragOffset: CGFloat = 0
@State private var progress: Double = 0
var body: some View {
LottieView(animation: .named("pull_refresh"))
.playing(.paused(at: .progress(progress)))
.resizable().scaledToFit().frame(height: 80)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in state = value.translation.height }
.onChange(of: dragOffset) { _, new in
progress = Double(min(max(new / 120, 0), 1))
}
)
}
}
深入层 设计思想解析
1. Protocol-Oriented ValueProvider
ValueProvider 是一个协议,内部通过 AnyValueProvider 做类型擦除,使得颜色、数值、文字、图片等完全不同的类型可以共享一套注入 API:
swift
// 库内抽象(简化版)
public protocol AnyValueProvider {
var valueType: Any.Type { get }
func hasUpdate(frame: AnimationFrameTime) -> Bool
func value(frame: AnimationFrameTime) -> Any
}
// 使用侧无感知具体类型
animationView.setValueProvider(colorProvider, keypath: keypath)
animationView.setValueProvider(textProvider, keypath: keypath)
2. Keypath 通配符系统
类似 KVC,但专为 AE 图层树设计,支持 **(任意路径深度)和 *(单层通配):
css
"Button.Background.Color" → 精确匹配
"**.Color" → 所有名为 Color 的属性
"Button.*.Color" → Button 子级的任意图层的 Color
3. Core Animation 渲染器的零主线程原则
Lottie 4.x 的 Core Animation 渲染器将所有动画帧的计算预烘焙为 CAAnimation 关键帧,提交给 Render Server 后完全在主线程之外运行,即使主线程卡顿也不会影响动效流畅度------这正是它相比 mainThread 渲染器的核心优势。
六、踩坑记录
问题 1:JSON 加载返回 nil,动效不显示
原因:JSON 文件未加入 Target Membership,或文件名拼写错误(大小写敏感)。
解决:选中 JSON 文件 → Xcode 右侧 File Inspector → 勾选对应 Target;或用 URL 方式加载并捕获错误:
swift
let animation = LottieAnimation.named("loading") // 返回 Optional
// 或
if let url = Bundle.main.url(forResource: "loading", withExtension: "json") {
let animation = try? LottieAnimation.loadedFrom(url: url)
}
问题 2:DynamicProperty 换色不生效
原因:Keypath 中的图层名称与 AE 中不一致(导出时 bodymovin 会对图层名做 URL 编码);或使用了 Core Animation 渲染器但该属性不支持动态修改。
解决:启用调试日志查看所有可用 Keypath:
swift
// 打印动画内所有可被覆盖的属性路径
if let animation = LottieAnimation.named("badge") {
let paths = animation.keypaths(for: .init(keypath: "**"))
paths.forEach { print($0) }
}
问题 3:Swift 6 / Sendable 编译报错
原因 :Lottie 4.4 以前部分类型未标注
@MainActor,在 Swift 6 严格并发检查下会报Sendable违规。解决:升级到 Lottie 4.5+(已系统性修复 Swift 6 合规问题);或临时在模块级别关闭严格检查:
swift
// 临时方案(不推荐长期保留)
import Lottie
nonisolated(unsafe) let sharedAnimation = LottieAnimation.named("loading")
问题 4:在 List / ScrollView 中大量 LottieView 导致卡顿
原因 :每个
LottieView初始化时都会同步解析 JSON 并构建 Layer 树,Cell 复用时重复创建开销大。解决 :预加载并缓存
LottieAnimation对象,复用时只更新播放状态:
swift
// 在 ViewModel / 缓存层提前加载
final class AnimationCache {
static let shared = AnimationCache()
private var cache: [String: LottieAnimation] = [:]
func animation(named name: String) -> LottieAnimation? {
if let cached = cache[name] { return cached }
let anim = LottieAnimation.named(name)
cache[name] = anim
return anim
}
}
// 使用
LottieView(animation: AnimationCache.shared.animation(named: "like_button"))
.playing()
问题 5:autoReverse 循环模式在 SwiftUI 中反向播放后卡住
原因 :
.autoReverse在部分版本的LottieView中有已知 Bug,正向→反向后停在第 0 帧不再循环。解决 :用
.loop替代,并在animationDidFinish中手动反转进度,或升级到最新 Lottie 版本。
问题 6:从网络 URL 加载动效时闪烁
原因 :网络请求完成前
LottieView已渲染了空状态,数据到来后重新布局导致闪烁。解决 :使用
LottieView的 URL 加载重载 +showPlaceholder搭配:
swift
LottieView {
try await LottieAnimation.loadedFrom(
url: URL(string: "https://example.com/fireworks.json")!
)
}
.playing()
.background { ProgressView() } // 加载中占位
七、延伸思考
Lottie vs 主流动画方案横向对比
| 维度 | Lottie | Rive | SwiftUI 原生动画 | CAAnimation |
|---|---|---|---|---|
| 文件格式 | JSON (bodymovin) | .riv (专有) | 代码 | 代码 |
| 设计协作 | AE 直出,零交接 | Rive 编辑器 | 开发手写 | 开发手写 |
| 交互状态机 | ⚠️ 有限 | ✅ 内建 | ⚠️ 有限 | ❌ |
| 渲染性能 | ✅ GPU 加速 | ✅ 极佳 | ✅ | ✅ |
| 动态换色 | ✅ DynamicProperty | ✅ 输入驱动 | ✅ | ❌ |
| 包体积(库本身) | ~4 MB | ~2 MB | 0 | 0 |
| 社区素材库 | ✅ LottieFiles 海量 | ⚠️ 较少 | ❌ | ❌ |
| 维护状态 | 活跃 | 活跃 | Apple 官方 | Apple 官方 |
| 学习曲线 | 低 | 中 | 低~中 | 高 |
推荐使用 Lottie 的场景
- Splash Screen / 启动动画
- Loading / 空状态 / 错误状态插画动效
- 点赞、收藏、成功等一次性触发的微交互动效
- 设计团队已有 AE 工作流,动效资产丰富
- 需要在 LottieFiles 快速取用社区素材
不推荐使用 Lottie 的场景
- 动效极简(仅 opacity / scale / translate)→ SwiftUI
.animation()即可 - 需要复杂交互状态机(手势联动多个状态跳转)→ 考虑 Rive
- 需要 3D 变换效果 → SceneKit 或 RealityKit
- 超大 JSON(> 2MB)在列表中大量实例化 → 需谨慎评估性能
八、参考资源
- GitHub 仓库 :github.com/airbnb/lott...
- 官方文档 :airbnb.io/lottie
- LottieFiles 素材库 :lottiefiles.com(数十万免费/付费 JSON 动效)
- bodymovin AE 插件 :aescripts.com/bodymovin
- Lottie 4.0 重大变化 :Migration Guide
- Swift Concurrency 适配说明 :Swift 6 Compatibility
- 系列 Demo 仓库 :本项目
HelloWorld/Features/iOSLibraries/LottieDetailView.swift
九、本期互动
小作业
在你自己的项目(或 Demo 工程)中实现一个点赞按钮:
- 点击前显示灰色心形(未点赞状态,可用 SF Symbol 或 Lottie JSON)
- 点击时播放 Lottie 爱心爆炸动效(建议从 LottieFiles 搜索 "like" 下载)
- 播放完成后停留在点赞完成帧
- 再次点击恢复未点赞状态
完成标准:能在真机或模拟器上稳定运行,按钮不会出现状态错乱。欢迎在评论区贴出实现思路或截图!
思考题
Lottie 的 DynamicProperty 机制允许在运行时"注入"新的值覆盖 JSON 中预设的属性。这种控制反转(IoC)的设计思路,在 iOS 开发中还有哪些类似的应用?你会如何把这种思路用在自己的业务组件设计上?
读者征集
下一期选题投票正在进行!同时:你在使用 Lottie 时踩过哪些坑? 欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新
✅ 第1期:Alamofire · ✅ 第2期:Kingfisher · ✅ 第3期:MarkdownUI · ✅ 第4期:SnapKit · ➡️ 第5期:Lottie · ○ 第6期:待定