iOS App小组件(Widget)显示LottieFiles动画

核心思路

首先要明确:iOS Widget 本身不直接支持完整的 Lottie 动画播放,但可以通过逐帧渲染 Lottie 动画为图片序列,再在 Widget 里播放这个图片序列的方式实现近似效果。核心步骤是:

  1. 用 Lottie 库解析动画文件,导出每一帧为图片;
  2. 将图片序列传给 Widget;
  3. 在 Widget 中用TimerAnimationView(适配 Widget 的轻量版)循环展示这些图片。

一、前置准备

  1. 导入依赖 :在 Xcode 项目中,通过 CocoaPods 或 Swift Package Manager 导入 Lottie 库(建议用官方的lottie-ios):

    bash

    运行

    复制代码
    # CocoaPods方式,在Podfile中添加
    pod 'lottie-ios', '~> 4.0'
  2. 准备 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)
    }
}

三、关键注意事项(新手必看)

  1. 性能控制:Widget 的性能有限,不要导出太多帧(建议单帧图片尺寸≤200x200,总帧数≤30),否则会卡顿或崩溃;
  2. 动画循环 :代码中通过currentFrameIndex % entry.frames.count实现循环播放;
  3. App Group 配置:必须正确配置 App Group,否则 Widget 读不到主 App 的图片数据;
  4. 兼容版本:Lottie-ios 4.0 + 支持 iOS 13+,WidgetKit 要求 iOS 14+,确保你的 Deployment Target≥14.0;
  5. 替代方案 :如果只是简单动画,也可以直接用 Lottie 官方的LottieWidget(需导入LottieWidget模块),但自定义性不如帧序列方式。

总结

  1. iOS Widget 展示 Lottie 动画的核心是将动画转为图片序列,而非直接播放;
  2. 需通过App Group实现主 App 和 Widget 的数据共享;
  3. 控制帧数和图片尺寸是保证 Widget 流畅的关键,避免性能问题。
相关推荐
2501_915909062 小时前
iOS 应用在混淆或修改后,如何完成签名、重签名与安装测试
android·ios·小程序·https·uni-app·iphone·webview
Digitally5 小时前
如何顺利地将手机号码转移到新iPhone
ios·iphone
茅根竹蔗水__20 小时前
iOS应用(App)生命周期、视图控制器(UIViewController)生命周期和视图(UIView)生命周期
ios
毛发浓密的女猴子1 天前
SSE Connect 数据解析详解
ios
2501_915921431 天前
如何将 iOS 应用的 IPA 文件安装到手机进行测试
android·ios·智能手机·小程序·uni-app·iphone·webview
2501_916008891 天前
不连 Xcode,也能把 iPhone App 的实时日志看清楚
android·ios·小程序·https·uni-app·iphone·webview
小CC吃豆子1 天前
uni-app 上架 iOS 时常见的审核被拒原因有哪些?
ios·uni-app
TheNextByte11 天前
如何将 Infinix 手机中的联系人传输到 iPhone
ios·智能手机·iphone