```html SwiftUI: How to Lottie Animation (iOS 17+, 2026)

How to implement Lottie animation in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: Lottie / UIViewRepresentable Updated: May 11, 2026
TL;DR

SwiftUI has no native Lottie view, so bridge LottieAnimationView into SwiftUI using UIViewRepresentable. Pass your .json filename, loop mode, and an isPlaying binding — the wrapper handles the rest.

import Lottie
import SwiftUI

struct LottieView: UIViewRepresentable {
    let name: String
    var loopMode: LottieLoopMode = .loop
    var isPlaying: Bool = true

    func makeUIView(context: Context) -> LottieAnimationView {
        let view = LottieAnimationView(name: name)
        view.loopMode = loopMode
        view.contentMode = .scaleAspectFit
        if isPlaying { view.play() }
        return view
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        isPlaying ? uiView.play() : uiView.pause()
    }
}

Full implementation

The wrapper below adds speed and a completion callback so you can chain animations or respond when a one-shot finishes. A parent ContentView toggles playback with a button, demonstrating full declarative control.

import Lottie
import SwiftUI

// MARK: - UIViewRepresentable wrapper

struct LottieView: UIViewRepresentable {
    let name: String
    var loopMode: LottieLoopMode = .loop
    var speed: CGFloat = 1.0
    @Binding var isPlaying: Bool
    var onComplete: (() -> Void)?

    func makeUIView(context: Context) -> LottieAnimationView {
        let view = LottieAnimationView(name: name)
        view.loopMode       = loopMode
        view.animationSpeed = speed
        view.contentMode    = .scaleAspectFit
        view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
        view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)
        play(view)
        return view
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        uiView.animationSpeed = speed
        uiView.loopMode       = loopMode
        if isPlaying {
            guard !uiView.isAnimationPlaying else { return }
            play(uiView)
        } else {
            uiView.pause()
        }
    }

    // MARK: Private helpers

    private func play(_ view: LottieAnimationView) {
        guard isPlaying else { return }
        if loopMode == .playOnce {
            view.play { finished in
                if finished {
                    DispatchQueue.main.async {
                        isPlaying = false
                        onComplete?()
                    }
                }
            }
        } else {
            view.play()
        }
    }
}

// MARK: - Demo parent view

struct LottieAnimationDemoView: View {
    @State private var isPlaying = true
    @State private var speed: CGFloat = 1.0

    var body: some View {
        VStack(spacing: 24) {
            LottieView(
                name: "confetti",          // confetti.json in main bundle
                loopMode: .loop,
                speed: speed,
                isPlaying: $isPlaying
            )
            .frame(width: 260, height: 260)
            .accessibilityLabel("Confetti animation")
            .accessibilityValue(isPlaying ? "Playing" : "Paused")

            HStack(spacing: 20) {
                Button(isPlaying ? "Pause" : "Play") {
                    isPlaying.toggle()
                }
                .buttonStyle(.borderedProminent)

                Slider(value: $speed, in: 0.25...3.0) {
                    Text("Speed")
                } minimumValueLabel: {
                    Text("0.25×").font(.caption)
                } maximumValueLabel: {
                    Text("3×").font(.caption)
                }
                .frame(maxWidth: 200)
            }

            Text("Speed: \(speed, specifier: "%.2f")×")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .padding()
        .navigationTitle("Lottie Demo")
    }
}

// MARK: - Preview

#Preview {
    NavigationStack {
        LottieAnimationDemoView()
    }
}

How it works

  1. makeUIView — called once when SwiftUI first places the view. Here we create the LottieAnimationView, configure loopMode, contentMode, and start playback. Setting both compression-resistance priorities to .fittingSizeLevel lets SwiftUI's .frame modifier win over Lottie's intrinsic size.
  2. updateUIView — called on every SwiftUI re-render. The guard !uiView.isAnimationPlaying prevents restarting a looping animation from the beginning each time an unrelated state change triggers a re-render.
  3. play(_:) with completion — for .playOnce mode, the Lottie callback fires on the main thread, flips isPlaying back to false, and calls the optional onComplete closure so the caller can navigate or mutate state.
  4. @Binding isPlaying — two-way binding means both the parent view (via the Play/Pause button) and the wrapper itself (via the completion handler) can update playback state without tight coupling.
  5. AccessibilityaccessibilityLabel names the animation and accessibilityValue announces the current playback state so VoiceOver users get meaningful feedback.

Variants

Load animation from a remote URL

Lottie 4 supports async URL loading natively. Swap LottieAnimationView(name:) for the URL initialiser and configure on load:

struct RemoteLottieView: UIViewRepresentable {
    let url: URL
    @Binding var isPlaying: Bool

    func makeUIView(context: Context) -> LottieAnimationView {
        let view = LottieAnimationView()
        view.contentMode = .scaleAspectFit
        view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
        view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)
        LottieAnimation.loadedFrom(url: url, closure: { animation in
            view.animation = animation
            if self.isPlaying { view.play() }
        }, animationCache: DefaultAnimationCache.sharedCache)
        return view
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        isPlaying ? uiView.play() : uiView.pause()
    }
}

// Usage:
RemoteLottieView(
    url: URL(string: "https://assets.soarias.com/animations/success.json")!,
    isPlaying: $isPlaying
)
.frame(width: 200, height: 200)

Scrub to a specific progress frame

Instead of playing the full timeline, drive the animation manually with a @Binding var progress: AnimationProgressTime and set uiView.currentProgress = progress inside updateUIView. Pair this with a Slider for a scrubber UI, or tie it to a scroll offset for scroll-driven animation effects similar to Lottie's play(fromProgress:toProgress:).

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement lottie animation in SwiftUI for iOS 17+.
Use Lottie/UIViewRepresentable.
Wrap LottieAnimationView with UIViewRepresentable, expose
loopMode, speed, and an isPlaying Binding.
Make it accessible (VoiceOver labels for play/pause state).
Add a #Preview with realistic sample data.

Drop this prompt into the Soarias Build phase after your screen mockups are locked — Claude Code will scaffold the wrapper, wire it to your SwiftData model, and surface playback controls without leaving your project.

Related

FAQ

Does this work on iOS 16?

Lottie 4.x supports iOS 14+ at the package level, so yes — but the new Metal/CoreAnimation rendering pipeline introduced in Lottie 4.3 performs best on iOS 16+. If you need iOS 15 support, set LottieConfiguration.shared.renderingEngine = .mainThread at app launch as a fallback; be aware this is CPU-bound and will hurt battery on complex animations.

How do I play only part of a Lottie animation?

Use view.play(fromProgress: 0.0, toProgress: 0.5, loopMode: .playOnce) inside play(_:). Lottie progress values range from 0.0 to 1.0. You can also use frame numbers via play(fromFrame:toFrame:) if your motion designer exports named markers.

What's the UIKit equivalent?

In UIKit you use LottieAnimationView directly — add it as a subview, pin it with Auto Layout, then call animationView.play(). The UIViewRepresentable wrapper shown on this page is essentially that UIKit setup packaged for SwiftUI's view lifecycle.

Last reviewed: 2026-05-11 by the Soarias team.

```