Introducing Lottie Animations to SwiftUI

Francisco Gindre
Nerd For Tech
Published in
4 min readJun 11, 2021

--

How to make Lottie Animations play nicely in SwiftUI… or so.

Animations are part of every App’s user experience. The Zcash ECC Wallet is no exception. Don’t get me wrong, SwiftUI is a really powerful tool to make animations. But let’s face the music, if you are not a specialist, you are far better off with tools that do all the heavy lifting for you.

First Step: Googling….

I didn’t get much back then in the early 2020, and honestly did not get much before posting this article either. There’s some basic approach which I ended up starting back then in this article. But This has many problems.

https://gist.githubusercontent.com/spencerfeng/a59106419f0593560dedf88fc39b7662/raw/3233d0b1e46c939648b17379a164290543903dca/LottieView.swift

For starters you won’t get much than a “play once” animation. Also, any updates are ignored. That’s probably not the author’s fault. The original SwiftUI docs basically portrait this kind of code.

Second Step: getting to know how UIRepresentable really works

Probably you are googling to get out of some urgency and you got here. But let me tell you, if you can spare some minutes, read this very good article from Vadim Bulavin, it will give you the knowledge you need to understand what’s going on and even improve the code I’m going to describe.

UIViewRepresentable is a way to tell SwiftUI that the View struct has to manage a UIKit view. As you should know, View structs, get recreated and refreshed all the time by the SwiftUI rendering engine and for good reasons you should never know, assume or even care what’s the concrete implementation that’s being used to represent your view on screen.

Which is not the case with UIKit Views. You do care about those. And that’s why UIViewRepresentable let’s you define you UIView on the makeUIView and updateUIView functions.

func makeUIView(context: UIViewRepresentableContext<LottieAnimation>) -> AnimationView {
// your instantiation code
}
func updateUIView(_ uiView: AnimationView, context: UIViewRepresentableContext<LottieAnimation>) {
//leaving this empty is probably a bad idea.
}

What you really want to do is to actually tell SwiftUI how to make the UIKit Lottie animation view to feel at home. For that there’s the Coordinator associated type that UIRepresentable uses to delegate that host/hostess role to make things flow between your View struct and your UIView subclass.

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}

Step 3: The Glue Code

What we are going to do, is to make a UIRepresentable View struct called LottieAnimation, which is going to bring the Lottie UIView and mix and match it with what UIRepresentable expects so that we can do these things:
- Play an animation in loop
- play a portion of an animation
- Progress through an animation

enum AnimationType {case progress(progress: Float)case frameProgress(startFrame: Float, endFrame: Float, progress: Float, loop: Bool)case circularLoop}

In my case I have a single animation file that has many sections that I may want to loop or progress through. So I’m going to create a pause and resume animation from lottie

func makeUIView(context: UIViewRepresentableContext<LottieAnimation>) -> AnimationView {let animationView = AnimationView()let animation = Lottie.Animation.named(filename)animationView.backgroundBehavior = .pauseAndRestoreanimationView.animation = animationanimationView.contentMode = .scaleAspectFitreturn animationView}

And for a smoother animation I will need to get the last progress step. So my Coordinator is going to know about that

class Coordinator: NSObject {
var lastProgress: Float
var parent: LottieAnimation

init(parent: LottieAnimation) {
self.parent = parent

if case AnimationType.frameProgress(let startFrame,_,_,_) = self.parent.animationType {
self.lastProgress = startFrame
} else {
self.lastProgress = 0
}
}
}

Then the updateUIView is going to do the rest of the work. This piece of code is weird and it’s fine that it is, since it’s where the imperative ocean of UIKit meets the declarative river of SwiftUI

func updateUIView(_ uiView: AnimationView, context: UIViewRepresentableContext<LottieAnimation>) {
guard isPlaying else {
uiView.stop()
return
}

switch self.animationType {

case .circularLoop:
if !uiView.isAnimationPlaying {
uiView.play(fromProgress: 0, toProgress: 1, loopMode: .loop, completion: nil)
}
case .progress(let progress):
uiView.currentProgress = AnimationProgressTime(progress)
if !uiView.isAnimationPlaying {
uiView.play(fromProgress: 0, toProgress: 1, loopMode: .loop, completion: nil)
}
case .frameProgress(let startFrame, let endFrame, let progress, let loop):
let progressTimeFrame = AnimationFrameTime(startFrame + (progress * (endFrame - startFrame)))
uiView.play(fromFrame: context.coordinator.lastProgress, toFrame: progressTimeFrame, loopMode: loop ? .loop : .none, completion: nil)
context.coordinator.lastProgress = progress
}
}

To see the full code go to this link on the project

Last thing: Use LottieAnimation View

now we can actually make use of it, like I do with the SyncingButton of my App!

struct SyncingButton<Content: View>: View {

var label: Content
var animationType: LottieAnimation.AnimationType
init(animationType: LottieAnimation.AnimationType, @ViewBuilder content: () -> Content) {
self.label = content()
self.animationType = animationType
}

var body: some View {
ZStack {
LottieAnimation(isPlaying: true, filename: "lottie_button_loading_new", animationType: self.animationType)
label
}
}
}

My SyncingButton View just creates a ZStack layer so that I can overlay text with the status the parent view wants to convey. Like this

case .downloading(let progress):
SyncingButton(animationType: .frameProgress(startFrame: 0, endFrame: 100, progress: 1.0, loop: true)) {
Text("Downloading \(Int(progress.progress * 100))%")
.foregroundColor(.white)
}

If you are looking for an Open Source project that uses SwiftUI to contribute to, don’t this wonderful SwiftUI project https://github.com/zcash/zcash-ios-wallet

Thanks for reading!

--

--