ryrosenbaum
Left Brain, Right Brain 🧠

Left Brain, Right Brain 🧠

How to make Lottie Animations work on WatchOS using SwiftUI

Photo by Mitchell Hollander on Unsplash

How to make Lottie Animations work on WatchOS using SwiftUI

ryrosenbaum's photo
ryrosenbaum
·Apr 11, 2022·

You can find my example project here

Lottie for the Watch

I started my career as a software engineer with such a visual idea of coding. I needed bright graphics and intuitive animations to make things feel alive and work well for the user, even if I was just using gum to stick things together in the background.

As our development craft expands, we find ways around some problems to create something really defining to your work compared to the industry. This is one of those circumstances

The Problem

I was building an application for WatchOS that needed the added benefit of providing educational content to the user. 3-step walk-throughs and introductions to the theories behind why they are using the application were so vital to the experience that it had to be prioritized.

At this point in time, most engineers have heard of the Lottie animation framework developed by the good folks at Airbnb, but I don't think a ton of people know that it doesn't work on the Apple Watch.

Warning, this is due to the performance hit that WatchOS takes with looping animation. SwiftUI has created a really effective way to control this, but it's still a risk to be well aware of.

The Solution

Now that we've defined the problem, let's find a solution. How do we implement Lottie in WatchOS without bogging down the entire application? Well, I'll show you.

1) Use SPM (Swift Package Manager) to install SDWebImageLottieCoder

Screen Shot 2022-04-11 at 3.13.44 PM.png

2) Find a JSON Lottie animation (I use IconScout)

Remember to download the JSON Lottie file to your image. I use a service called IconScout that I absolutely love. Has a great collection of 3D images, logos, illustrations, and of course Lottie Animations.

For this example, I will use a random exercise-themed Lottie to present a scenario where you have to educate a user on a movement.

Screen Shot 2022-04-11 at 3.14.41 PM.png

3) Build a LottieFileManager

This manager was inspired by Github user eharrison implementation of a similar project name.

import SwiftUI
import UIKit
import SDWebImageLottieCoder

class LottieFileManager: ObservableObject {
    @Published private(set) var image: UIImage = UIImage(named: "defaultIcon")!

    // MARK: - Animation

    private var coder: SDImageLottieCoder?
    private var animationTimer: Timer?
    private var currentFrame: UInt = 0
    private var playing: Bool = false
    private var speed: Double = 1.0

    /// Loads animation data
    /// - Parameter url: url of animation JSON
    func loadAnimation(url: URL) {
        let session = URLSession.shared
        let dataTask = session.dataTask(with: URLRequest(url: url)) { (data, response, error) in
            guard let data = data else { return }
            DispatchQueue.main.async {
                self.setupAnimation(with: data)
            }
        }
        dataTask.resume()
    }

    /// Loads animation data from local file
    /// - Parameter filename: name of the local Lottie file
    func loadAnimationFromFile(filename: String) {
        let url = Bundle.main.url(forResource: filename, withExtension: "json")!
        let data = try! Data(contentsOf: url)
        DispatchQueue.main.async {
            self.setupAnimation(with: data)
        }
    }

    /// Decodify animation with given data
    /// - Parameter data: data of animation
    private func setupAnimation(with data: Data) {
        coder = SDImageLottieCoder(animatedImageData: data, options: [SDImageCoderOption.decodeLottieResourcePath: Bundle.main.resourcePath!])

        // resets to first frame
        currentFrame = 0
        setImage(frame: currentFrame)

        play()
    }

    /// Set current animation
    /// - Parameter frame: Set image for given frame
    private func setImage(frame: UInt) {
        guard let coder = coder,
              let uiImage = coder.animatedImageFrame(at: frame) else { return }
        self.image = uiImage
    }

    /// Replace current frame with next one
    private func nextFrame() {
        guard let coder = coder else { return }

        currentFrame += 1
        // make sure that current frame is within frame count
        // if reaches the end, we set it back to 0 so it loops
        if currentFrame >= coder.animatedImageFrameCount {
            currentFrame = 0
        }

        setImage(frame: currentFrame)
    }

    /// Start playing animation
    private func play() {
        playing = true

        animationTimer?.invalidate()
        animationTimer = Timer.scheduledTimer(withTimeInterval: 0.05/speed, repeats: true, block: { (timer) in
            guard self.playing else {
                timer.invalidate()
                return
            }
            self.nextFrame()
        })
    }

    /// Pauses animation
    private func pause() {
        playing = false
        animationTimer?.invalidate()
    }
}

4) Confirm the addition of the framework by checking the Plist info.

This is a common area to trip on when the package manager doesn't link to the project correctly. Check this page to confirm this is working.

Screen Shot 2022-04-11 at 3.18.39 PM.png

5) Build the View

Last but not least its time to build our view. I chose a pretty simple scenario of use for this example so we will use a hip thrust guided workout as the example.


import SwiftUI

struct MainView: View {
    @State var lottieFile: String = "feet-on-ball-hip-thrust-workout"
    @ObservedObject var viewModel: LottieFileManager = .init()

    var body: some View {
        NavigationView {
            ZStack {
                Color.white.edgesIgnoringSafeArea(.all)

                VStack {
                    Image(uiImage: viewModel.image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .edgesIgnoringSafeArea(.all)
                }
                .edgesIgnoringSafeArea(.all)
                VStack{

                    Text("Fitness Challenge")
                        .foregroundColor(.black)
                        .font(
                            .system(size: 16, weight: .medium, design: .rounded))

                    Text("15 reps of Hip Thrust")
                        .font(.system(size: 8))
                        .foregroundColor(.blue)
                        .fontWeight(.regular)
                }
                .offset(y: 45)
            }

        }
        .navigationBarBackButtonHidden(true).navigationBarHidden(true)
        .onAppear {
            self.viewModel.loadAnimationFromFile(filename: lottieFile)
        }
    }
}

5) Set a default icon to fallback on

I chose a simple check.png from IconJar shape that I had on hand but you can put a company logo or anything you'd like there.

6) Load the json into the project

Screen Shot 2022-04-11 at 3.39.44 PM.png

7) You're Good to go

via GIPHY

If you followed all of these steps correctly, you should be seeing a working Lottie animation on your Apple Watch simulator or transferred to your watch upon building to that destination. It looks slick and beautiful.

A few tips from this point forward

  • Play with the difference between
    .aspectRatio(contentMode: .fit)
    .aspectRatio(contentMode: .fill)
    

Thanks for reading and good luck implementing!

 
Share this