Question Details

No question body available.

Tags

swift swiftui render lottie

Answers (1)

Accepted Answer Available
Accepted Answer
September 8, 2025 Score: 1 Rep: 55 Quality: High Completeness: 80%

Okay, I was able to solve the issue. You can import the librlottie-Xcode and use it for creation and rendering of json lottie animations. Here is full code (UIKit), just change :

private let animations = Stickers.Category.allCases
        .flatMap { $0.pack.map { $0.deletingPathExtension().lastPathComponent } }

to your json file(s) paths.

import UIKit
import librlottie

extension OpaquePointer: @unchecked @retroactive Sendable {}

// MARK: - Animation Manager actor LottieAnimationManager { static let shared = LottieAnimationManager()

private var cachedAnimations: [String: OpaquePointer] = [:]

private init() {}

// Async get with lazy loading func getAnimation(named name: String) async -> OpaquePointer? { if let cached = cachedAnimations[name] { return cached }

// Do file loading off the actor context to avoid blocking return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { var anim: OpaquePointer? = nil if let path = Bundle.main.path(forResource: name, ofType: "json") { anim = lottieanimationfromfile(path) }

Task { @MainActor in continuation.resume(returning: anim) } } } }

// Store animation func cacheAnimation(
anim: OpaquePointer, named name: String) { cachedAnimations[name] = anim }

// Cleanup everything func cleanup() { for (, anim) in cachedAnimations { lottieanimationdestroy(anim) } cachedAnimations.removeAll() }

deinit { cleanup() } }

// MARK: - ViewController class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private var collectionView: UICollectionView! private let animations = Stickers.Category.allCases .flatMap { $0.pack.map { $0.deletingPathExtension().lastPathComponent } } private var displayLink: CADisplayLink?

override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white setupCollectionView() startAnimationLoop() }

override func viewWillDisappear(
animated: Bool) { super.viewWillDisappear(animated) stopAnimationLoop() }

private func setupCollectionView() { let spacing: CGFloat = 10 let layout = UICollectionViewFlowLayout() let itemWidth = (view.bounds.width - spacing 5) / 4 layout.itemSize = CGSize(width: itemWidth, height: itemWidth) layout.minimumLineSpacing = spacing layout.minimumInteritemSpacing = spacing layout.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)

collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.backgroundColor = .white collectionView.dataSource = self collectionView.delegate = self collectionView.register(LottieCell.self, forCellWithReuseIdentifier: LottieCell.identifier)

view.addSubview(collectionView)

collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) }

// MARK: - Animation Loop private func startAnimationLoop() { displayLink = CADisplayLink(target: self, selector: #selector(renderFrame)) displayLink?.add(to: .main, forMode: .common) }

private func stopAnimationLoop() { displayLink?.invalidate() displayLink = nil }

@objc private func renderFrame() { let visibleIndexPaths = collectionView.indexPathsForVisibleItems for indexPath in visibleIndexPaths { if let cell = collectionView.cellForItem(at: indexPath) as? LottieCell { cell.lottieView.renderNextFrame() } } }

// MARK: - UICollectionViewDataSource func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return animations.count }

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: LottieCell.identifier, for: indexPath ) as! LottieCell cell.configure(withAnimationNamed: animations[indexPath.item]) return cell }

// MARK: - Visibility Management func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { (cell as? LottieCell)?.lottieView.setVisible(true) }

func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { (cell as? LottieCell)?.lottieView.setVisible(false) }

deinit { stopAnimationLoop() } }

// MARK: - LottieCell class LottieCell: UICollectionViewCell { static let identifier = "LottieCell" let lottieView = RLLottieView()

override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(lottieView) lottieView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ lottieView.topAnchor.constraint(equalTo: contentView.topAnchor), lottieView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), lottieView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), lottieView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) }

required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

func configure(withAnimationNamed name: String) { lottieView.loadAnimation(named: name) }

override func prepareForReuse() { super.prepareForReuse() lottieView.reset() } }

// MARK: - RLLottieView class RLLottieView: UIView { private var animationName: String? private var animation: OpaquePointer? private var frameNumber: Int = 0 private var totalFrames: Int = 0 private let renderSize = CGSize(width: 200, height: 200) private var buffer: UnsafeMutablePointer? private var isVisible = false private var renderInProgress = false private var startTime: CFTimeInterval = 0 private var randomOffset: TimeInterval = 0

// Cached graphics objects private let cachedColorSpace: CGColorSpace private let cachedBitmapInfo: CGBitmapInfo

private let renderQueue = DispatchQueue(label: "lottie.render.queue", qos: .userInitiated)

override init(frame: CGRect) { cachedColorSpace = CGColorSpaceCreateDeviceRGB() cachedBitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) super.init(frame: frame)

let pixelCount = Int(renderSize.width
renderSize.height) buffer = UnsafeMutablePointer.allocate(capacity: pixelCount) buffer?.initialize(repeating: 0, count: pixelCount) }

required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

// MARK: - Load Animation func loadAnimation(named name: String) { animationName = name animation = nil layer.contents = nil

Task { if let anim = await LottieAnimationManager.shared.getAnimation(named: name) { // Double-check cell is still expecting this animation guard self.animationName == name else { return }

await LottieAnimationManager.shared.cacheAnimation(anim, named: name)

self.animation = anim self.totalFrames = Int(lottieanimationgettotalframe(anim)) self.frameNumber = 0 self.startTime = CACurrentMediaTime() self.randomOffset = TimeInterval.random(in: 0.. 0 else { return }

renderInProgress = true

let elapsed = CACurrentMediaTime() - startTime + randomOffset let duration = Double(totalFrames) / Double(lottie
animationgetframerate(animation)) let progress = fmod(elapsed, duration) / duration let currentFrame = Int(progress Double(totalFrames))

renderQueue.async { [weak self] in guard let self = self, let animation = self.animation, let buffer = self.buffer else { return }

lottie_animation_render(animation, size_t(currentFrame), buffer, size_t(self.renderSize.width), size_t(self.renderSize.height), size_t(Int(self.renderSize.width)
MemoryLayout.size))

self.createAndDisplayImage(from: buffer) DispatchQueue.main.async { self.renderInProgress = false } }

}

private func createAndDisplayImage(from cgBuffer: UnsafeMutableRawPointer) { guard let context = CGContext(data: cgBuffer, width: Int(renderSize.width), height: Int(renderSize.height), bitsPerComponent: 8, bytesPerRow: Int(renderSize.width) * 4, space: cachedColorSpace, bitmapInfo: cachedBitmapInfo.rawValue), let cgImage = context.makeImage() else { return }

DispatchQueue.main.async { [weak self] in self?.layer.contents = cgImage } }

func reset() { isVisible = false renderInProgress = false frameNumber = 0 animationName = nil animation = nil layer.contents = nil }

deinit { buffer?.deallocate() } }