-
-
Notifications
You must be signed in to change notification settings - Fork 3
Lottie Views API Documentation
This document provides comprehensive documentation for the SwiftUI and UIKit view implementations built on top of the LottieRenderer type.
- Architecture Overview
- LottieConfiguration
- LottieViewModel
- LottieView (SwiftUI)
- LottieUIKitView (UIKit)
- Testing
- Usage Examples
The Lottie views implementation consists of three main layers:
┌─────────────────────────────────────┐
│ LottieView / LottieUIKitView │ ← View Layer (SwiftUI/UIKit)
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ LottieViewModel │ ← Business Logic Layer
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ LottieRenderer │ ← Rendering Layer
└─────────────────────────────────────┘
-
External ViewModel Pattern: Views accept a pre-configured
LottieViewModel, giving users complete control over playback and state observation. -
Single Source of Truth: All playback control (
play(),pause(),stop(),seek()) happens through the ViewModel, not the views. -
Configuration-Driven: All rendering behavior is configured through the
LottieConfigurationtype, making it easy to customize animations. -
Reactive: Uses Combine framework for reactive updates, ensuring UI stays in sync with animation state.
The LottieConfiguration struct provides a declarative way to configure Lottie animation rendering and playback behavior.
public enum LoopMode: Equatable {
case playOnce // Play once and stop
case loop // Loop indefinitely
case repeat(count: Int) // Repeat N times
case autoReverse // Play forward, then backward continuously
}public enum ContentMode {
case scaleAspectFit // Fit within view, maintain aspect
case scaleAspectFill // Fill view, maintain aspect, may crop
}Note
Content modes control how the animation is cropped/scaled during rendering, not view-level scaling. For best results, pass a size parameter to LottieViewModel that matches your view's frame dimensions. See Understanding Content Modes and Render Size for details.
| Property | Type | Default | Description |
|---|---|---|---|
loopMode |
LoopMode |
.loop |
Controls how the animation loops |
speed |
Double |
1.0 |
Playback speed multiplier |
contentMode |
ContentMode |
.scaleAspectFit |
How content fits in view |
frameRate |
Double |
30.0 |
Rendering frame rate (fps) |
pixelFormat |
PixelFormat |
.argb |
Pixel format for rendering |
let config = LottieConfiguration(
loopMode: .repeat(count: 3),
speed: 1.5,
contentMode: .scaleAspectFit,
frameRate: 60.0,
pixelFormat: .argb
)Content modes control how the animation is rendered to the buffer, not how the view is displayed on screen:
.scaleAspectFit (default)
- Renders the complete animation
- Maintains aspect ratio
- Best for most use cases where you want to see the entire animation
.scaleAspectFill
- Fills the render buffer completely
- Maintains aspect ratio by cropping content
- Requires the
sizeparameter inLottieViewModelto match your view's display dimensions for predictable cropping
How it works:
// Example 1: Using default intrinsic size (recommended)
let viewModel = LottieViewModel(
lottie: lottie,
configuration: LottieConfiguration(contentMode: .scaleAspectFit)
)
// SwiftUI or UIKit then scales the rendered image to fit your view frame
// Example 2: Using scaleAspectFill with explicit size
let viewModel = LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 150), // Match your view frame
configuration: LottieConfiguration(contentMode: .scaleAspectFill)
)
// Renders at 300x150, cropping to fill this specific sizeThe LottieViewModel is an ObservableObject that manages animation playback state and rendering. Users create and own the ViewModel, then pass it to views.
@Published public private(set) var renderedFrame: UIImage?
@Published public private(set) var playbackState: PlaybackState
@Published public private(set) var error: PlaybackError?
@Published public private(set) var progress: Double // 0.0 to 1.0public enum PlaybackState: Equatable {
case playing
case paused
case stopped
case completed
}public enum PlaybackError: Error, Equatable {
case renderingFailed(String)
case imageCreationFailed
case contextCreationFailed
case invalidFrameIndex
}-
renderingFailed: The underlying ThorVG renderer failed to render a frame -
imageCreationFailed: Failed to create aUIImagefrom the rendered buffer -
contextCreationFailed: Failed to create aCGContextfor rendering -
invalidFrameIndex: Attempted to render an invalid frame index
public func play()
public func pause()
public func stop()// Seek to progress (0.0 to 1.0)
public func seek(to progress: Double)
// Seek to specific frame
public func seek(toFrame frame: Float)public init(
lottie: Lottie,
size: CGSize? = nil,
configuration: LottieConfiguration = .default,
engine: Engine = .main
)Parameters:
-
lottie: The Lottie animation to render -
size: Optional render size. Ifnil, defaults tolottie.frameSize(the animation's intrinsic dimensions) -
configuration: Animation playback and rendering configuration -
engine: The ThorVG rendering engine to use (defaults to.main)
Understanding the size Parameter:
The size parameter controls the render buffer dimensions and interacts with contentMode:
-
When
nil(default): Renders at the animation's intrinsic size. The view layer (SwiftUI/UIKit) handles scaling to fit your UI. Best for most use cases. -
When specified: Renders at the exact dimensions you provide. Useful when:
- Using
.scaleAspectFillto crop content (requires matching the view's display size) - Optimizing performance for very large or very small display sizes
- You know the exact display dimensions ahead of time
- Using
Example:
// Use intrinsic size - SwiftUI handles scaling
let viewModel = LottieViewModel(
lottie: lottie,
configuration: .default
)
// Or specify size for scaleAspectFill cropping
let viewModel = LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 150), // Wide frame
configuration: LottieConfiguration(contentMode: .scaleAspectFill)
)- Thread Safety: All UI updates are posted to the main thread
- Memory Management: Automatically cancels timers on deinit
- Error Handling: Errors are published rather than causing crashes
-
Efficient Rendering:
- Buffer is reused across frames
- CGContext is created once and reused for all frames
-
Time-Based Playback: Uses
CMTimefor precise animation timing -
Decoupled Speed and Frame Rate:
-
speedcontrols playback rate (how fast animation time progresses) -
frameRatecontrols rendering frequency (smoothness of display)
-
A SwiftUI view for displaying Lottie animations. The view accepts an external LottieViewModel for complete control over playback and state observation.
import SwiftUI
import ThorVGSwift
struct ContentView: View {
@StateObject private var viewModel: LottieViewModel
init() {
guard let lottie = try? Lottie(path: "animation.json") else {
fatalError("Failed to load Lottie")
}
// Size is optional - defaults to animation's intrinsic size
_viewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
configuration: .default
))
}
var body: some View {
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
}
}public init(viewModel: LottieViewModel)Parameters:
-
viewModel: The view model managing animation state and rendering. Create using@StateObjectto maintain ownership.
Since you create the ViewModel externally, you have direct access to all its properties:
struct ContentView: View {
@StateObject private var viewModel: LottieViewModel
init(lottie: Lottie) {
_viewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 300)
))
}
var body: some View {
VStack {
LottieView(viewModel: viewModel)
.frame(width: 300, height: 300)
// Direct access to ViewModel properties
Text("Progress: \(Int(viewModel.progress * 100))%")
Text("State: \(String(describing: viewModel.playbackState))")
if let error = viewModel.error {
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
}
// Direct playback control
HStack {
Button("Play") { viewModel.play() }
Button("Pause") { viewModel.pause() }
Button("Stop") { viewModel.stop() }
}
}
.onChange(of: viewModel.playbackState) { _, state in
print("Playback state changed: \(state)")
}
}
}- onDisappear: Automatically pauses playback to conserve resources
A UIKit view for displaying Lottie animations. Like the SwiftUI view, it accepts an external LottieViewModel.
import UIKit
import ThorVGSwift
class ViewController: UIViewController {
private var viewModel: LottieViewModel!
private var lottieView: LottieUIKitView!
override func viewDidLoad() {
super.viewDidLoad()
guard let lottie = try? Lottie(path: "animation.json") else { return }
viewModel = LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 300),
configuration: .default
)
lottieView = LottieUIKitView(viewModel: viewModel)
view.addSubview(lottieView)
// Setup constraints...
viewModel.play()
}
}public init(viewModel: LottieViewModel)Parameters:
-
viewModel: The view model managing animation state and rendering.
// Access to the ViewModel
public let viewModel: LottieViewModel
// Callbacks (optional, for convenience)
public var onPlaybackStateChanged: ((LottieViewModel.PlaybackState) -> Void)?
public var onError: ((LottieViewModel.PlaybackError) -> Void)?
public var onProgressChanged: ((Double) -> Void)?All playback control happens through the ViewModel:
// Directly on ViewModel
viewModel.play()
viewModel.pause()
viewModel.stop()
viewModel.seek(to: 0.5)
viewModel.seek(toFrame: 10)let config = LottieConfiguration(loopMode: .playOnce)
let viewModel = LottieViewModel(
lottie: myLottie,
size: CGSize(width: 300, height: 300),
configuration: config
)
let lottieView = LottieUIKitView(viewModel: viewModel)
lottieView.onPlaybackStateChanged = { state in
if state == .completed {
print("Animation finished!")
}
}
lottieView.onProgressChanged = { progress in
progressBar.progress = Float(progress)
}
viewModel.play()The LottieUIKitView contains a single UIImageView subview that displays the rendered frames. The image view is constrained to fill the parent view and uses Auto Layout.
Comprehensive test suites are provided for all components:
Tests for the ViewModel layer covering:
- Initialization with various configurations
- Playback control (play, pause, stop)
- Seeking functionality
- Loop modes (playOnce, loop, repeat, autoReverse)
- Published property updates
- Error handling
- Speed control (independent of frame rate)
- Frame rate control (rendering frequency)
- Content modes
- Memory management
Location: swift-tests/LottieViewModelTests.swift
Tests for the configuration struct covering:
- Default configuration values
- Custom configuration initialization
- Partial configuration with defaults
Location: swift-tests/LottieConfigurationTests.swift
Tests for the core Lottie model covering:
- Frame count and duration validation
-
frameDurationcalculation - Initialization from path and string
- Error handling
Location: swift-tests/LottieTests.swift
View layer testing is accomplished through SwiftUI Previews which allow for:
- Visual verification of rendering
- Interactive testing of playback controls
- Testing various configurations and loop modes
- Testing different speeds (0.5x, 1x, 2x)
- Testing different frame rates (30fps, 60fps)
- Real-time debugging
Location: Preview implementations in LottieView.swift and LottieUIKitView.swift
Since this is an iOS-only package, tests should be run on iOS Simulator:
cd /path/to/thorvg.swift
swift test --destination 'platform=iOS Simulator'Or use Xcode's Canvas debugger for interactive testing with Previews.
// SwiftUI
struct SimpleAnimationView: View {
@StateObject private var viewModel: LottieViewModel
init() {
guard let lottie = try? Lottie(path: "loader.json") else {
fatalError("Failed to load Lottie")
}
_viewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
configuration: .default
))
}
var body: some View {
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
}
}
// UIKit
guard let lottie = try? Lottie(path: "loader.json") else {
fatalError("Failed to load Lottie")
}
let viewModel = LottieViewModel(lottie: lottie)
let lottieView = LottieUIKitView(viewModel: viewModel)
view.addSubview(lottieView)
viewModel.play()// SwiftUI
struct OneShotAnimationView: View {
@StateObject private var viewModel: LottieViewModel
@State private var isComplete = false
init(lottie: Lottie) {
let config = LottieConfiguration(loopMode: .playOnce)
_viewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
configuration: config
))
}
var body: some View {
LottieView(viewModel: viewModel)
.onChange(of: viewModel.playbackState) { _, state in
if state == .completed {
isComplete = true
// Navigate away or show next screen
}
}
.onAppear { viewModel.play() }
}
}
// UIKit
let config = LottieConfiguration(loopMode: .playOnce)
let viewModel = LottieViewModel(lottie: lottie, configuration: config)
let lottieView = LottieUIKitView(viewModel: viewModel)
lottieView.onPlaybackStateChanged = { state in
if state == .completed {
// Navigate away or show next screen
}
}
viewModel.play()// SwiftUI
struct ControlledAnimationView: View {
@StateObject private var viewModel: LottieViewModel
init(lottie: Lottie) {
_viewModel = StateObject(wrappedValue: LottieViewModel(lottie: lottie))
}
var body: some View {
VStack {
LottieView(viewModel: viewModel)
HStack {
Button("Play") { viewModel.play() }
Button("Pause") { viewModel.pause() }
Button("Stop") { viewModel.stop() }
}
}
}
}
// UIKit
let viewModel = LottieViewModel(lottie: lottie)
let lottieView = LottieUIKitView(viewModel: viewModel)
// Control buttons
playButton.addTarget(self, action: #selector(play), for: .touchUpInside)
pauseButton.addTarget(self, action: #selector(pause), for: .touchUpInside)
stopButton.addTarget(self, action: #selector(stop), for: .touchUpInside)
@objc func play() { viewModel.play() }
@objc func pause() { viewModel.pause() }
@objc func stop() { viewModel.stop() }// SwiftUI
struct ProgressAnimationView: View {
@StateObject private var viewModel: LottieViewModel
init(lottie: Lottie) {
_viewModel = StateObject(wrappedValue: LottieViewModel(lottie: lottie))
}
var body: some View {
VStack {
LottieView(viewModel: viewModel)
Slider(value: Binding(
get: { viewModel.progress },
set: { viewModel.seek(to: $0) }
), in: 0...1)
Text("Progress: \(Int(viewModel.progress * 100))%")
}
}
}
// UIKit
let viewModel = LottieViewModel(lottie: lottie)
let lottieView = LottieUIKitView(viewModel: viewModel)
lottieView.onProgressChanged = { progress in
progressSlider.value = Float(progress)
progressLabel.text = "\(Int(progress * 100))%"
}
@objc func sliderValueChanged(_ slider: UISlider) {
viewModel.seek(to: Double(slider.value))
}let config = LottieConfiguration(
loopMode: .loop,
speed: 2.0 // 2x speed
)
// SwiftUI
@StateObject var viewModel = LottieViewModel(
lottie: lottie,
configuration: config
)
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
// UIKit
let viewModel = LottieViewModel(lottie: lottie, configuration: config)
let lottieView = LottieUIKitView(viewModel: viewModel)
viewModel.play()let config = LottieConfiguration(
loopMode: .loop,
frameRate: 60.0 // Smoother rendering
)
// SwiftUI
@StateObject var viewModel = LottieViewModel(
lottie: lottie,
configuration: config
)
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
// UIKit
let viewModel = LottieViewModel(lottie: lottie, configuration: config)
let lottieView = LottieUIKitView(viewModel: viewModel)
viewModel.play()let config = LottieConfiguration(loopMode: .autoReverse)
// SwiftUI
@StateObject var viewModel = LottieViewModel(
lottie: lottie,
configuration: config
)
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
// UIKit
let viewModel = LottieViewModel(lottie: lottie, configuration: config)
let lottieView = LottieUIKitView(viewModel: viewModel)
viewModel.play()// SwiftUI
struct SafeAnimationView: View {
@StateObject private var viewModel: LottieViewModel
init(lottie: Lottie) {
_viewModel = StateObject(wrappedValue: LottieViewModel(lottie: lottie))
}
var body: some View {
VStack {
LottieView(viewModel: viewModel)
if let error = viewModel.error {
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
}
}
.onAppear { viewModel.play() }
.onChange(of: viewModel.error) { _, error in
if let error = error {
print("Animation error occurred: \(error)")
}
}
}
}
// UIKit
let viewModel = LottieViewModel(lottie: lottie)
let lottieView = LottieUIKitView(viewModel: viewModel)
lottieView.onError = { [weak self] error in
let alert = UIAlertController(
title: "Animation Error",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true)
}
viewModel.play()This example shows a more advanced seeking implementation where the animation pauses while dragging the slider:
// SwiftUI
struct InteractiveSeekView: View {
@StateObject private var viewModel: LottieViewModel
@State private var sliderValue: Double = 0.0
@State private var isDragging: Bool = false
init(lottie: Lottie) {
_viewModel = StateObject(wrappedValue: LottieViewModel(lottie: lottie))
}
var body: some View {
VStack(spacing: 16) {
LottieView(viewModel: viewModel)
VStack(spacing: 8) {
Slider(
value: $sliderValue,
in: 0...1,
onEditingChanged: { editing in
if editing {
// Pause when user starts dragging
isDragging = true
viewModel.pause()
} else {
// Resume when user stops dragging
isDragging = false
viewModel.play()
}
}
)
.onChange(of: sliderValue) { newValue in
// Only seek while dragging to avoid feedback loops
if isDragging {
viewModel.seek(to: newValue)
}
}
HStack {
Button(viewModel.playbackState == .playing ? "Pause" : "Play") {
if viewModel.playbackState == .playing {
viewModel.pause()
} else {
viewModel.play()
}
}
.buttonStyle(.borderedProminent)
Spacer()
Text("\(Int(viewModel.progress * 100))%")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.horizontal)
}
.padding()
.onChange(of: viewModel.progress) { newProgress in
// Update slider to reflect animation progress (but not while dragging)
if !isDragging {
sliderValue = newProgress
}
}
.onAppear { viewModel.play() }
}
}
// UIKit
class InteractiveSeekViewController: UIViewController {
private var viewModel: LottieViewModel!
private var lottieView: LottieUIKitView!
private var progressSlider: UISlider!
private var progressLabel: UILabel!
private var isDragging = false
override func viewDidLoad() {
super.viewDidLoad()
guard let lottie = try? Lottie(path: "animation.json") else { return }
viewModel = LottieViewModel(lottie: lottie)
lottieView = LottieUIKitView(viewModel: viewModel)
progressSlider = UISlider()
progressSlider.addTarget(self, action: #selector(sliderTouchDown), for: .touchDown)
progressSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
progressSlider.addTarget(self, action: #selector(sliderTouchUp), for: [.touchUpInside, .touchUpOutside])
// Setup UI hierarchy and constraints...
lottieView.onProgressChanged = { [weak self] progress in
guard let self = self, !self.isDragging else { return }
self.progressSlider.value = Float(progress)
self.progressLabel.text = "\(Int(progress * 100))%"
}
viewModel.play()
}
@objc private func sliderTouchDown() {
isDragging = true
viewModel.pause()
}
@objc private func sliderValueChanged(_ slider: UISlider) {
if isDragging {
viewModel.seek(to: Double(slider.value))
}
}
@objc private func sliderTouchUp() {
isDragging = false
viewModel.play()
}
}This example demonstrates .scaleAspectFill content mode, which crops the animation to fill the view while maintaining aspect ratio:
// SwiftUI
struct AspectFillAnimationView: View {
@StateObject private var fillViewModel: LottieViewModel
@StateObject private var fitViewModel: LottieViewModel
init(lottie: Lottie) {
// scaleAspectFill: Wide frame - will crop top/bottom
let fillConfig = LottieConfiguration(
loopMode: .loop,
contentMode: .scaleAspectFill
)
_fillViewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 150), // Explicit size for predictable cropping
configuration: fillConfig
))
// scaleAspectFit: Shows full animation (default)
let fitConfig = LottieConfiguration(
loopMode: .loop,
contentMode: .scaleAspectFit
)
_fitViewModel = StateObject(wrappedValue: LottieViewModel(
lottie: lottie,
configuration: fitConfig
))
}
var body: some View {
ScrollView {
VStack(spacing: 32) {
VStack(spacing: 8) {
Text("Scale Aspect Fill")
.font(.headline)
Text("Wide frame - crops content to fill")
.font(.caption)
.foregroundColor(.secondary)
LottieView(viewModel: fillViewModel)
.frame(width: 300, height: 150)
.background(Color.blue.opacity(0.1))
.border(Color.blue, width: 2)
}
VStack(spacing: 8) {
Text("Scale Aspect Fit")
.font(.headline)
Text("Shows full animation")
.font(.caption)
.foregroundColor(.secondary)
LottieView(viewModel: fitViewModel)
.frame(width: 300, height: 150)
.background(Color.green.opacity(0.1))
.border(Color.green, width: 2)
}
}
.padding()
}
.onAppear {
fillViewModel.play()
fitViewModel.play()
}
}
}
// UIKit
class AspectFillViewController: UIViewController {
private var fillViewModel: LottieViewModel!
private var fitViewModel: LottieViewModel!
override func viewDidLoad() {
super.viewDidLoad()
guard let lottie = try? Lottie(path: "animation.json") else { return }
// Create viewModel with scaleAspectFill
let fillConfig = LottieConfiguration(
loopMode: .loop,
contentMode: .scaleAspectFill
)
fillViewModel = LottieViewModel(
lottie: lottie,
size: CGSize(width: 300, height: 150), // Match view frame
configuration: fillConfig
)
let fillView = LottieUIKitView(viewModel: fillViewModel)
fillView.frame = CGRect(x: 20, y: 100, width: 300, height: 150)
fillView.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
fillView.layer.borderColor = UIColor.systemBlue.cgColor
fillView.layer.borderWidth = 2
view.addSubview(fillView)
// Create viewModel with scaleAspectFit (default)
let fitConfig = LottieConfiguration(
loopMode: .loop,
contentMode: .scaleAspectFit
)
fitViewModel = LottieViewModel(lottie: lottie, configuration: fitConfig)
let fitView = LottieUIKitView(viewModel: fitViewModel)
fitView.frame = CGRect(x: 20, y: 280, width: 300, height: 150)
fitView.backgroundColor = UIColor.systemGreen.withAlphaComponent(0.1)
fitView.layer.borderColor = UIColor.systemGreen.cgColor
fitView.layer.borderWidth = 2
view.addSubview(fitView)
// Start playback
fillViewModel.play()
fitViewModel.play()
}
}Key Points for Scale Aspect Fill:
- Always specify the
sizeparameter inLottieViewModelto match your view's display dimensions - The animation will be cropped to fill the specified size while maintaining its aspect ratio
- Content at the edges may be cut off depending on the aspect ratio difference
- Use
.scaleAspectFit(default) if you need to see the entire animation
Always ensure Lottie animations are properly loaded and handle errors:
guard let url = Bundle.main.url(forResource: "animation", withExtension: "json") else {
// Handle missing resource
return
}
do {
let lottie = try Lottie(path: url.path)
// Use lottie
} catch {
// Handle loading error
}Always use @StateObject in SwiftUI to maintain ViewModel ownership:
// ✅ Good: StateObject maintains ownership
@StateObject private var viewModel = LottieViewModel(...)
// ❌ Bad: ObservedObject doesn't maintain ownership
@ObservedObject private var viewModel = LottieViewModel(...)The size parameter is optional and defaults to the animation's intrinsic size:
// ✅ Recommended: Use default size (animation's intrinsic dimensions)
// Let SwiftUI/UIKit handle scaling to your desired frame
LottieViewModel(lottie: lottie)
// ✅ Also valid: Specify size when using .scaleAspectFill
// or when you need to optimize for a specific display size
LottieViewModel(lottie: lottie, size: CGSize(width: 300, height: 300))For most use cases, omit the size parameter and use SwiftUI's .frame() or UIKit's frame property to control the display size.
Start playback explicitly when appropriate:
// SwiftUI
LottieView(viewModel: viewModel)
.onAppear { viewModel.play() }
// UIKit
viewModel.play()Views automatically clean up resources, but for long-lived animations:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
viewModel.stop() // Stop and reset
}Create reusable configurations:
extension LottieConfiguration {
static let onboarding = LottieConfiguration(
loopMode: .playOnce,
speed: 1.0
)
static let loader = LottieConfiguration(
loopMode: .loop,
speed: 1.5,
frameRate: 30.0
)
static let highQuality = LottieConfiguration(
loopMode: .loop,
frameRate: 60.0
)
}
// Usage
let viewModel = LottieViewModel(lottie: lottie, configuration: .onboarding)- Platform: iOS only
-
Minimum iOS Version: iOS 13.0 (ViewModel and UIKit views), iOS 14.0 (SwiftUI views due to
@StateObject) - Minimum Swift Version: Swift 5.9
-
Testing: Run tests with
swift test --destination 'platform=iOS Simulator'
This package is designed exclusively for iOS and requires UIKit.
Solution: Check that:
- The Lottie file is valid and loads successfully
- You've called
viewModel.play()manually - The view is added to the view hierarchy (UIKit) or appears (SwiftUI)
Solution:
- Reduce the rendering size
- Lower the frame rate (default 30fps is usually sufficient)
- Consider using a simpler animation
Solution: Check the contentMode configuration:
- Use
.scaleAspectFit(default) to show the full animation while maintaining aspect ratio - Use
.scaleAspectFillto fill the view while maintaining aspect ratio (may crop content) - When using
.scaleAspectFill, ensure thesizeparameter matches your view's display dimensions
Solution: This is working correctly! frameRate and speed are decoupled:
-
frameRatecontrols rendering smoothness (30fps vs 60fps) -
speedcontrols playback rate (1.0x, 2.0x, etc.)
Solution:
- Ensure you're not creating too many simultaneous animations
- Stop animations when views are off-screen
- Use appropriate sizes (avoid rendering at unnecessarily high resolutions)
This implementation is part of the ThorVGSwift library. See LICENSE file for details.