核心思路
首先要明确:iOS Widget 本身不直接支持完整的 Lottie 动画播放,但可以通过逐帧渲染 Lottie 动画为图片序列,再在 Widget 里播放这个图片序列的方式实现近似效果。核心步骤是:
- 用 Lottie 库解析动画文件,导出每一帧为图片;
- 将图片序列传给 Widget;
- 在 Widget 中用
Timer或AnimationView(适配 Widget 的轻量版)循环展示这些图片。
一、前置准备
-
导入依赖 :在 Xcode 项目中,通过 CocoaPods 或 Swift Package Manager 导入 Lottie 库(建议用官方的lottie-ios):
bash
运行
# CocoaPods方式,在Podfile中添加 pod 'lottie-ios', '~> 4.0' -
准备 Lottie 文件 :将你的
.json格式 Lottie 动画文件加入项目(记得勾选 Target)。
二、完整实现代码
1. 第一步:在 App 主工程中解析 Lottie 动画,导出帧图片
swift
import Lottie
import UIKit
// 工具类:解析Lottie并导出帧图片
class LottieFrameExporter {
/// 解析Lottie动画,返回每一帧的UIImage数组
/// - Parameters:
/// - animationName: Lottie文件名称(不带.json)
/// - frameCount: 要导出的帧数(建议根据动画时长和帧率定,比如10帧/秒)
static func exportFrames(from animationName: String, frameCount: Int) -> [UIImage] {
// 加载Lottie动画
guard let animation = LottieAnimation.named(animationName) else {
return []
}
let animationView = LottieAnimationView(animation: animation)
animationView.frame = CGRect(x: 0, y: 0, width: 200, height: 200) // Widget常用尺寸
var frames: [UIImage] = []
// 逐帧渲染并导出图片
for frame in 0..<frameCount {
// 计算当前帧的进度
let progress = CGFloat(frame) / CGFloat(frameCount)
animationView.currentProgress = progress
animationView.layoutIfNeeded()
// 将当前帧渲染为图片
UIGraphicsBeginImageContextWithOptions(animationView.bounds.size, false, UIScreen.main.scale)
animationView.layer.render(in: UIGraphicsGetCurrentContext()!)
if let image = UIGraphicsGetImageFromCurrentImageContext() {
frames.append(image)
}
UIGraphicsEndImageContext()
}
return frames
}
}
2. 第二步:将帧图片传给 Widget(用 App Group 共享数据)
Widget 和主 App 是两个进程,需要通过 App Group 共享图片数据:
swift
// 1. 先在Xcode中配置App Group:Target -> Signing & Capabilities -> +App Groups -> 填写group.xxx.xxx
let appGroupIdentifier = "group.com.yourapp.widget"
// 2. 存储帧图片到App Group
func saveFramesToAppGroup(frames: [UIImage]) {
guard let sharedDefaults = UserDefaults(suiteName: appGroupIdentifier) else { return }
// 将UIImage转为Data数组存储
let frameDataArray = frames.compactMap { $0.pngData() }
sharedDefaults.set(frameDataArray, forKey: "lottie_frames")
sharedDefaults.synchronize()
}
// 3. 在主App启动时调用(示例)
func applicationDidFinishLaunching() {
// 解析Lottie动画,导出20帧(假设动画2秒,10帧/秒)
let lottieFrames = LottieFrameExporter.exportFrames(from: "your_lottie_file", frameCount: 20)
// 保存到App Group供Widget读取
saveFramesToAppGroup(frames: lottieFrames)
}
3. 第三步:Widget 中播放帧图片序列
swift
import WidgetKit
import SwiftUI
// Widget主结构体
struct LottieWidget: Widget {
let kind: String = "LottieWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LottieWidgetProvider()) { entry in
LottieWidgetView(entry: entry)
}
.configurationDisplayName("Lottie动画小组件")
.description("展示Lottie动画的小组件")
.supportedFamilies([.systemSmall, .systemMedium]) // 支持的Widget尺寸
}
}
// 数据提供者
struct LottieWidgetProvider: TimelineProvider {
// 读取App Group中的帧图片
func getFrames() -> [UIImage] {
guard let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.widget"),
let frameDataArray = sharedDefaults.array(forKey: "lottie_frames") as? [Data] else {
return []
}
return frameDataArray.compactMap { UIImage(data: $0) }
}
func placeholder(in context: Context) -> LottieWidgetEntry {
LottieWidgetEntry(date: Date(), frames: getFrames(), currentFrameIndex: 0)
}
func getSnapshot(in context: Context, completion: @escaping (LottieWidgetEntry) -> ()) {
let entry = LottieWidgetEntry(date: Date(), frames: getFrames(), currentFrameIndex: 0)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [LottieWidgetEntry] = []
let frames = getFrames()
guard !frames.isEmpty else {
completion(Timeline(entries: [], policy: .never))
return
}
// 每0.1秒切换一帧(10帧/秒),循环播放
let frameDuration = 0.1
var currentDate = Date()
for index in 0..<frames.count {
let entry = LottieWidgetEntry(date: currentDate, frames: frames, currentFrameIndex: index)
entries.append(entry)
currentDate = Calendar.current.date(byAdding: .second, value: Int(frameDuration * 1000), to: currentDate)!
}
// 循环:最后一帧播放完后重新请求Timeline
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// Widget数据模型
struct LottieWidgetEntry: TimelineEntry {
let date: Date
let frames: [UIImage]
let currentFrameIndex: Int
}
// Widget视图
struct LottieWidgetView: View {
var entry: LottieWidgetProvider.Entry
var body: some View {
ZStack {
Color.white // 背景色
if !entry.frames.isEmpty {
// 显示当前帧图片
Image(uiImage: entry.frames[entry.currentFrameIndex % entry.frames.count])
.resizable()
.scaledToFit()
} else {
Text("暂无动画")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
三、关键注意事项(新手必看)
- 性能控制:Widget 的性能有限,不要导出太多帧(建议单帧图片尺寸≤200x200,总帧数≤30),否则会卡顿或崩溃;
- 动画循环 :代码中通过
currentFrameIndex % entry.frames.count实现循环播放; - App Group 配置:必须正确配置 App Group,否则 Widget 读不到主 App 的图片数据;
- 兼容版本:Lottie-ios 4.0 + 支持 iOS 13+,WidgetKit 要求 iOS 14+,确保你的 Deployment Target≥14.0;
- 替代方案 :如果只是简单动画,也可以直接用 Lottie 官方的
LottieWidget(需导入LottieWidget模块),但自定义性不如帧序列方式。
总结
- iOS Widget 展示 Lottie 动画的核心是将动画转为图片序列,而非直接播放;
- 需通过App Group实现主 App 和 Widget 的数据共享;
- 控制帧数和图片尺寸是保证 Widget 流畅的关键,避免性能问题。