apple/iOS, UIKit, Documentation

iOS Lottie 알아보기 (.json, .lottie)

lgvv 2022. 2. 19. 03:13

 

iOS Lottie 알아보기

 

모든 걸 코드로 구현할 수 없어서 적당히 로티를 사용하는 걸로 하자.

 

변경로그

  • 2022년 2월 19일 03시 13분 
    • 최초 포스팅
  • 2022년 4월 29일 15시 05분
    • Lottie AnimationView width, height 적용이 되지 않는 문제에 대한 고찰
    • 기존 샘플 코드에서 잘못된 레이아웃 수정
  • 2022년 4월 30일 16시 39분
    • 로티 이미지 색상 변경
  • 2024년 11월 15일 03시 01분
    • dot-lottie 오픈소스 사용시 주의할 점

 

Lottie_iOS.zip
0.83MB

 

 

 

로티 오픈소스 사이트

https://github.com/airbnb/lottie-ios/tree/master/Example/iOS/ViewControllers

 

GitHub - airbnb/lottie-ios: An iOS library to natively render After Effects vector animations

An iOS library to natively render After Effects vector animations - GitHub - airbnb/lottie-ios: An iOS library to natively render After Effects vector animations

github.com

 

그냥 여담인데, 어느 글에서 본건데 aribnb에서 large scale이라서 코드 로드하는 시간만 2분씩 걸리고 했다는데, 이걸 개편하는 작업을 진행했다더고 함.

 

설치는 SPM이나 cocoapod 사용해서 하면 됨.

 

로티 관련 파일을 다운로드 받아서 사용할 수 있는 사이트

https://lottiefiles.com/featured

 

Featured animations from our community

Featured collection of Free Lottie Animations created with Bodymovin.

lottiefiles.com

 

 

 

 

json형식 말고 .lottie 파일도 있는데 메모리 사용량이 압도적으로 많아서 json 권장, 오픈소스 사이트에 issue를 올려두기도 했는데 메모리 너무 크게 잡아먹음.

  • 2KB(.json) -> 2MB
  • 7KB(.lottie) -> 20MB

 

코드 예제

 

ViewController 파일

//
//  ViewController.swift
//  Lottie_iOS
//
//  Created by Hamlit Jason on 2022/02/18.
//

import UIKit
import Lottie
import SnapKit
import Then
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    
    fileprivate var bag = DisposeBag()
    
    let animationView: AnimationView = .init(name: "95171-colors")
    let stackView = UIStackView().then {
        $0.distribution = .fillEqually
        $0.layer.borderColor = UIColor.black.cgColor
        $0.layer.borderWidth = 2
    }
    let playBtn = UIButton().then {
        $0.setTitle("play", for: .normal)
        $0.backgroundColor = .blue
    }
    let loopSelectBtn = UIButton().then {
        $0.setTitle("loopSelect", for: .normal)
    }
    let stopBtn = UIButton().then {
        $0.setTitle("stopBtn", for: .normal)
        $0.backgroundColor = .red
    }
    let speedFrameSelectBtn = UIButton().then {
        $0.setTitle("speedFrameSelectBtn", for: .normal)
        $0.backgroundColor = .gray
    }
    let progressSelectBtn = UIButton().then {
        $0.setTitle("progressSelectBtn", for: .normal)
        $0.backgroundColor = .purple
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupViews()
        bind()
    }
    
    fileprivate func loopAlert() {
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let playOnce = UIAlertAction(title: "playOnce", style: .default) { [weak self] _ in self?.animationView.loopMode = .playOnce }
        let repeat2 = UIAlertAction(title: "repeat2", style: .default) { [weak self] _ in self?.animationView.loopMode = .repeat(2) }
        let loop = UIAlertAction(title: "loop", style: .default) { [weak self] _ in self?.animationView.loopMode = .loop }
        let repeatBackwards = UIAlertAction(title: "repeatBackwards", style: .default) { [weak self] _ in self?.animationView.loopMode = .repeatBackwards(1) }
        let autoReverse = UIAlertAction(title: "autoReverse", style: .default) { [weak self] _ in self?.animationView.loopMode = .autoReverse }
        let acncelBtn = UIAlertAction(title: "나가기😆", style: .cancel)
        [playOnce, repeat2, loop, repeatBackwards, autoReverse, acncelBtn].forEach{ alert.addAction($0) }
        
        self.present(alert, animated: true)
    }
    
    fileprivate func speedFrameAlert() {
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let speedDouble = UIAlertAction(title: "speed_Double", style: .default) { [weak self] _ in self?.animationView.animationSpeed = 2 }
        let speedHalf = UIAlertAction(title: "speed_half", style: .default) { [weak self] _ in self?.animationView.animationSpeed = 0.5 }
        let endFrame: String = "\(animationView.animation?.endFrame ?? -1)"
        // 유의할 점: frame10To90 이후 frame40To140 실행시 이전 값부터 90-140까지만 수행된다.
        let frame10To90 = UIAlertAction(title: "10-90 end:\(endFrame)", style: .default) { [weak self] _ in self?.animationView.play(fromFrame: 10, toFrame: 90) }
        let frame40To140 = UIAlertAction(title: "40-140 end:\(endFrame)", style: .default) { [weak self] _ in self?.animationView.play(fromFrame: 40, toFrame: 140) }
        let acncelBtn = UIAlertAction(title: "나가기😆", style: .cancel)
        [speedDouble, speedHalf, frame10To90 ,frame40To140, acncelBtn].forEach { alert.addAction($0) }
        self.present(alert, animated: true)
    }
    
    fileprivate func progressAlert() {
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let progress_2_6 = UIAlertAction(title: "0.2-0.6", style: .default) { [weak self] _ in self?.animationView.play(fromProgress: 0.2, toProgress: 0.6) }
        let progress_4_9 = UIAlertAction(title: "0.4-0.9", style: .default) { [weak self] _ in self?.animationView.play(fromProgress: 0.4, toProgress: 0.9) }
        let progress_3_8 = UIAlertAction(title: "0.3-0.8 루프모드 및 컴플리션핸들러 파라미터", style: .default) { [weak self] _ in self?.animationView.play(fromProgress: 0.4, toProgress: 0.9, loopMode: .loop, completion: nil) }
        let acncelBtn = UIAlertAction(title: "나가기😆", style: .cancel)
        let progress_5 = UIAlertAction(title: "progress_0.5", style: .default) { [weak self] _ in self?.animationView.currentProgress = 0.5 }
        let frame_190 = UIAlertAction(title: "frame_190", style: .default) { [weak self] _ in self?.animationView.currentFrame = 190 }
        let time_1 = UIAlertAction(title: "time_1", style: .default) { [weak self] _ in self?.animationView.currentTime = 1 }
        [progress_2_6, progress_4_9, progress_3_8, progress_5, frame_190, acncelBtn, time_1 ].forEach { alert.addAction($0) }
        self.present(alert, animated: true)
    }
    
    fileprivate func bind() {
        playBtn.rx.tap
            .bind { [weak self] _ in self?.animationView.play() }
            .disposed(by: bag)
        
        loopSelectBtn.rx.tap
            .bind { [weak self] _ in self?.loopAlert() }
            .disposed(by: bag)
        
        stopBtn.rx.tap
            .bind { [weak self] _ in self?.animationView.stop() }
            .disposed(by: bag)
        
        speedFrameSelectBtn.rx.tap
            .bind { [weak self] _ in self?.speedFrameAlert() }
            .disposed(by: bag)
        
        progressSelectBtn.rx.tap
            .bind { [weak self] _ in self?.progressAlert() }
            .disposed(by: bag)
        
    }
    
    fileprivate func setupViews() {
        // 레이아웃 잡는 코드와 같다.
//        animationView.frame = self.view.bounds
//        animationView.center = self.view.center
//        animationView.contentMode = .scaleAspectFit
        
        [animationView].forEach{ view.addSubview($0) }
        [stackView].forEach{ animationView.addSubview($0) }
        [playBtn, loopSelectBtn, stopBtn, speedFrameSelectBtn, progressSelectBtn].forEach{
            stackView.addArrangedSubview($0)
            $0.setTitleColor(.black, for: .normal)
        }
        
        animationView.snp.makeConstraints {
            $0.edges.equalTo(view.safeAreaLayoutGuide)
        }
        stackView.snp.makeConstraints {
            $0.top.leading.trailing.equalTo(animationView.safeAreaLayoutGuide)
            $0.height.equalTo(48)
        }
        
    }
}


위의 코드를 천천히 읽어보면 별 다른 설명 없어도 다 이해할 수 있다.

궁금하면 직접 파일 다운받아서 사용해보자.

 

 

(update1) Lottie AnimationView width, height 적용이 되지 않는 문제에 대한 고찰

 

결론 : AnimationView를 사용하는 경우에는 width와 height가 적용되지 않으므로 inset및 offset을 통해서 사이즈를 조정해야 함.

 

(가정) lottie Json file에 적용된 값들

로티 JSON 파일의 가장 위의 부분
로티 JSON 파일의 가장 위의 부분

 

어떻게 JSON 파일을 만들어져서 배포되는지 모르겠으나, 여러 파일을 확인한 결과 위 처럼 width와 height 부분에 정해져 있는 것을 확인할 수 있음. stackOverflow를 참고해보면 다른 사람들도 동일한 문제를 다수 겪음.

 

let animationView: AnimationView = .init(name: {Your lottie JSON file name})

animationView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
animationView.snp.makeConstraints {
	$0.center.equalTo(view.safeAreaLayoutGuide)
}

 

하지만 저는 로티 애니메이션을 갈아 끼우더라도 view에서 동일한 사이즈로 나타나길 원함

let animationView: AnimationView = .init(name: {Your lottie JSON file name})

animationView.snp.makeConstraints {
	$0.width.equalTo(100)
	$0.height.equalTo(100)
	$0.center.equalTo(view.safeAreaLayoutGuide)
}
// or

let animationView: AnimationView = .init(name: {Your lottie JSON file name})

animationView.snp.makeConstraints {
	$0.center.equalTo(view.safeAreaLayoutGuide).inset(100)
}

 

 

(update2) 로티의 이미지 색을 변경하고 싶을 때

https://edit.lottiefiles.com/?src=https%3A%2F%2Fassets7.lottiefiles.com%2Fpackages%2Flf20_hszjy67x.json 

 

Lottie Editor

Edit Lottie JSON Animations Online

edit.lottiefiles.com

 

 

dot-lottie 오픈소스 사용시 주의할 점

메모리 10배도 넘게 많이 먹어서 사용하기 꺼려짐

깃에 문의해서 답변을 받긴 했는데, 해당 부분으로 해결되지 않았음.