Skip to content

Lottie Views API Documentation

Hermet Park edited this page Nov 17, 2025 · 1 revision

Lottie Views API Documentation

This document provides comprehensive documentation for the SwiftUI and UIKit view implementations built on top of the LottieRenderer type.

Table of Contents

  1. Architecture Overview
  2. LottieConfiguration
  3. LottieViewModel
  4. LottieView (SwiftUI)
  5. LottieUIKitView (UIKit)
  6. Testing
  7. Usage Examples

Architecture Overview

The Lottie views implementation consists of three main layers:

┌─────────────────────────────────────┐
│   LottieView / LottieUIKitView      │  ← View Layer (SwiftUI/UIKit)
└─────────────┬───────────────────────┘
              │
┌─────────────▼───────────────────────┐
│       LottieViewModel               │  ← Business Logic Layer
└─────────────┬───────────────────────┘
              │
┌─────────────▼───────────────────────┐
│       LottieRenderer                │  ← Rendering Layer
└─────────────────────────────────────┘

Key Design Principles

  1. External ViewModel Pattern: Views accept a pre-configured LottieViewModel, giving users complete control over playback and state observation.

  2. Single Source of Truth: All playback control (play(), pause(), stop(), seek()) happens through the ViewModel, not the views.

  3. Configuration-Driven: All rendering behavior is configured through the LottieConfiguration type, making it easy to customize animations.

  4. Reactive: Uses Combine framework for reactive updates, ensuring UI stays in sync with animation state.


LottieConfiguration

The LottieConfiguration struct provides a declarative way to configure Lottie animation rendering and playback behavior.

Properties

Loop Mode

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
}

Content Mode

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.

All Configuration Options

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

Example

let config = LottieConfiguration(
    loopMode: .repeat(count: 3),
    speed: 1.5,
    contentMode: .scaleAspectFit,
    frameRate: 60.0,
    pixelFormat: .argb
)

Understanding Content Modes and Render Size

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 size parameter in LottieViewModel to 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 size

LottieViewModel

The LottieViewModel is an ObservableObject that manages animation playback state and rendering. Users create and own the ViewModel, then pass it to views.

Published Properties

@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.0

Playback State

public enum PlaybackState: Equatable {
    case playing
    case paused
    case stopped
    case completed
}

Errors

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 a UIImage from the rendered buffer
  • contextCreationFailed: Failed to create a CGContext for rendering
  • invalidFrameIndex: Attempted to render an invalid frame index

Public Methods

Playback Control

public func play()
public func pause()
public func stop()

Seeking

// Seek to progress (0.0 to 1.0)
public func seek(to progress: Double)

// Seek to specific frame
public func seek(toFrame frame: Float)

Initialization

public init(
    lottie: Lottie,
    size: CGSize? = nil,
    configuration: LottieConfiguration = .default,
    engine: Engine = .main
)

Parameters:

  • lottie: The Lottie animation to render
  • size: Optional render size. If nil, defaults to lottie.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 .scaleAspectFill to 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

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)
)

Implementation Details

  • 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 CMTime for precise animation timing
  • Decoupled Speed and Frame Rate:
    • speed controls playback rate (how fast animation time progresses)
    • frameRate controls rendering frequency (smoothness of display)

LottieView (SwiftUI)

A SwiftUI view for displaying Lottie animations. The view accepts an external LottieViewModel for complete control over playback and state observation.

Basic Usage

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() }
    }
}

Initialization

public init(viewModel: LottieViewModel)

Parameters:

  • viewModel: The view model managing animation state and rendering. Create using @StateObject to maintain ownership.

Accessing ViewModel Properties

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)")
        }
    }
}

Lifecycle

  • onDisappear: Automatically pauses playback to conserve resources

LottieUIKitView (UIKit)

A UIKit view for displaying Lottie animations. Like the SwiftUI view, it accepts an external LottieViewModel.

Basic Usage

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()
    }
}

Initialization

public init(viewModel: LottieViewModel)

Parameters:

  • viewModel: The view model managing animation state and rendering.

Public Properties

// 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)?

Playback Control

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)

Example with Callbacks

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()

View Hierarchy

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.


Testing

Comprehensive test suites are provided for all components:

LottieViewModelTests

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

LottieConfigurationTests

Tests for the configuration struct covering:

  • Default configuration values
  • Custom configuration initialization
  • Partial configuration with defaults

Location: swift-tests/LottieConfigurationTests.swift

LottieTests

Tests for the core Lottie model covering:

  • Frame count and duration validation
  • frameDuration calculation
  • Initialization from path and string
  • Error handling

Location: swift-tests/LottieTests.swift

View Testing

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

Running Tests

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.


Usage Examples

Example 1: Simple Looping Animation

// 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()

Example 2: Play Once with Completion Handler

// 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()

Example 3: Manual Playback Control

// 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() }

Example 4: Progress Tracking with Slider

// 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))
}

Example 5: Custom Speed

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()

Example 6: High Frame Rate

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()

Example 7: Auto-Reverse Animation

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()

Example 8: Error Handling

// 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()

Example 9: Interactive Seeking with Slider

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()
    }
}

Example 10: Using Scale Aspect Fill Content Mode

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 size parameter in LottieViewModel to 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

Best Practices

1. Resource Management

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
}

2. ViewModel Ownership

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(...)

3. Size Considerations

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.

4. Explicit Playback Control

Start playback explicitly when appropriate:

// SwiftUI
LottieView(viewModel: viewModel)
    .onAppear { viewModel.play() }

// UIKit
viewModel.play()

5. Memory Management

Views automatically clean up resources, but for long-lived animations:

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    viewModel.stop()  // Stop and reset
}

6. Configuration Reuse

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 Support

  • 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.


Troubleshooting

Issue: Animation not playing

Solution: Check that:

  1. The Lottie file is valid and loads successfully
  2. You've called viewModel.play() manually
  3. The view is added to the view hierarchy (UIKit) or appears (SwiftUI)

Issue: Poor performance

Solution:

  1. Reduce the rendering size
  2. Lower the frame rate (default 30fps is usually sufficient)
  3. Consider using a simpler animation

Issue: Animation appears distorted or cropped

Solution: Check the contentMode configuration:

  • Use .scaleAspectFit (default) to show the full animation while maintaining aspect ratio
  • Use .scaleAspectFill to fill the view while maintaining aspect ratio (may crop content)
  • When using .scaleAspectFill, ensure the size parameter matches your view's display dimensions

Issue: Animation plays too fast/slow at higher frame rates

Solution: This is working correctly! frameRate and speed are decoupled:

  • frameRate controls rendering smoothness (30fps vs 60fps)
  • speed controls playback rate (1.0x, 2.0x, etc.)

Issue: Memory warnings

Solution:

  1. Ensure you're not creating too many simultaneous animations
  2. Stop animations when views are off-screen
  3. Use appropriate sizes (avoid rendering at unnecessarily high resolutions)

License

This implementation is part of the ThorVGSwift library. See LICENSE file for details.

Clone this wiki locally