Skip to content

Commit

Permalink
Add support for loading .lottie archives (#1785)
Browse files Browse the repository at this point in the history
  • Loading branch information
eharrison authored Nov 1, 2022
1 parent 4be0499 commit 41b05dd
Show file tree
Hide file tree
Showing 31 changed files with 2,501 additions and 18 deletions.
10 changes: 8 additions & 2 deletions Example/iOS/ViewControllers/AnimationPreviewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ class AnimationPreviewViewController: UIViewController {
super.viewDidLoad()
view.backgroundColor = .systemBackground

let animation = LottieAnimation.named(animationName)
if let animation = LottieAnimation.named(animationName) {
animationView.animation = animation
} else {
DotLottieFile.named(animationName) { [animationView] result in
guard case Result.success(let lottie) = result else { return }
animationView.loadAnimation(from: lottie)
}
}

animationView.animation = animation
animationView.contentMode = .scaleAspectFit
view.addSubview(animationView)

Expand Down
4 changes: 3 additions & 1 deletion Example/iOS/ViewControllers/SampleListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ final class SampleListViewController: CollectionViewController {

@ItemModelBuilder
private var sampleAnimationLinks: [ItemModeling] {
(Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: directory) ?? [])
(
(Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: directory) ?? []) +
(Bundle.main.urls(forResourcesWithExtension: "lottie", subdirectory: directory) ?? []))
.map { $0.lastPathComponent.replacingOccurrences(of: ".json", with: "") }
.sorted(by: { $0.localizedCompare($1) == .orderedAscending })
.map { (name: $0, path: "\(directory)/\($0)") }
Expand Down
15 changes: 12 additions & 3 deletions Example/iOS/Views/LinkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,18 @@ final class LinkView: UIView, EpoxyableView {
return animationView
},
setContent: { context, _ in
context.constrainable.animation = .named(animationName)
context.constrainable.contentMode = .scaleAspectFit
context.constrainable.currentProgress = 0.5
if let animation = LottieAnimation.named(animationName) {
context.constrainable.animation = animation
context.constrainable.contentMode = .scaleAspectFit
context.constrainable.currentProgress = 0.5
} else {
DotLottieFile.named(animationName) { result in
guard case Result.success(let lottie) = result else { return }
context.constrainable.loadAnimation(from: lottie)
context.constrainable.contentMode = .scaleAspectFit
context.constrainable.currentProgress = 0.5
}
}
})
}

Expand Down
166 changes: 164 additions & 2 deletions Lottie.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ let package = Package(
name: "Lottie",
platforms: [.iOS("11.0"), .macOS("10.10"), .tvOS("11.0")],
products: [.library(name: "Lottie", targets: ["Lottie"])],
targets: [.target(name: "Lottie", path: "Sources")])
targets: [.target(name: "Lottie", path: "Sources")])
52 changes: 52 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieAnimation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// DotLottieAnimation.swift
// Pods
//
// Created by Evandro Harrison Hoffmann on 28/06/2021.
//

import Foundation

struct DotLottieAnimation: Codable {
/// Id of Animation
var id: String

/// Loop enabled
var loop: Bool

// appearance color in HEX
var themeColor: String

/// Animation Playback Speed
var speed: Double

/// 1 or -1
var direction: Int? = 1

/// mode - "bounce" | "normal"
var mode: String? = "normal"

/// URL to animation, to be set internally
var animationUrl: URL?

/// Loop mode for animation
var loopMode: LottieLoopMode {
mode == "bounce" ? .autoReverse : (loop ? .loop : .playOnce)
}

/// Animation speed
var animationSpeed: Double {
speed * Double(direction ?? 1)
}

/// Loads `LottieAnimation` from `animationUrl`
/// - Returns: Deserialized `LottieAnimation`. Optional.
func animation() throws -> LottieAnimation {
guard let animationUrl = animationUrl else {
throw DotLottieError.animationNotAvailable
}
let data = try Data(contentsOf: animationUrl)
return try LottieAnimation.from(data: data)
}

}
15 changes: 15 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// DotLottieSettings.swift
// Lottie
//
// Created by Evandro Hoffmann on 19/10/22.
//

import Foundation

struct DotLottieConfiguration {
var id: String
var imageProvider: AnimationImageProvider?
var loopMode: LottieLoopMode
var speed: Double
}
68 changes: 68 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieImageProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// DotLottieImageProvider.swift
// Lottie
//
// Created by Evandro Hoffmann on 20/10/22.
//

import Foundation
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
import UIKit
#elseif os(macOS)
import AppKit
#endif

// MARK: - DotLottieImageProvider

/// Provides an image for a lottie animation from a provided Bundle.
class DotLottieImageProvider: AnimationImageProvider {

// MARK: Lifecycle

/// Initializes an image provider with a specific filepath.
///
/// - Parameter filepath: The absolute filepath containing the images.
///
init(filepath: String) {
self.filepath = URL(fileURLWithPath: filepath)
loadImages()
}

init(filepath: URL) {
self.filepath = filepath
loadImages()
}

// MARK: Internal

let filepath: URL

func imageForAsset(asset: ImageAsset) -> CGImage? {
imageCache.object(forKey: asset.name as NSString)
}

// MARK: Private

private var imageCache: NSCache<NSString, CGImage> = .init()

private func loadImages() {
filepath.urls.forEach {
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
if
let data = try? Data(contentsOf: $0),
let image = UIImage(data: data)?.cgImage
{
imageCache.setObject(image, forKey: $0.lastPathComponent as NSString)
}
#elseif os(macOS)
if
let data = try? Data(contentsOf: $0),
let image = NSImage(data: data)?.lottie_CGImage
{
imageCache.setObject(image, forKey: $0.lastPathComponent as NSString)
}
#endif
}
}

}
53 changes: 53 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieManifest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// DotLottieManifest.swift
// Lottie
//
// Created by Evandro Harrison Hoffmann on 27/06/2020.
//

import Foundation

/// Manifest model for .lottie File
struct DotLottieManifest: Codable {

// MARK: Lifecycle

init(animations: [DotLottieAnimation], version: String, author: String, generator: String) {
self.animations = animations
self.version = version
self.author = author
self.generator = generator
}

// MARK: Internal

var animations: [DotLottieAnimation]
var version: String
var author: String
var generator: String

/// Decodes data to Manifest model
/// - Parameter data: Data to decode
/// - Throws: Error
/// - Returns: .lottie Manifest model
static func decode(from data: Data) throws -> DotLottieManifest {
try JSONDecoder().decode(DotLottieManifest.self, from: data)
}

/// Loads manifest from given URL
/// - Parameter path: URL path to Manifest
/// - Returns: Manifest Model
static func load(from url: URL) throws -> DotLottieManifest {
let data = try Data(contentsOf: url)
return try decode(from: data)
}

/// Encodes to data
/// - Parameter encoder: JSONEncoder
/// - Throws: Error
/// - Returns: encoded Data
func encode(with encoder: JSONEncoder = JSONEncoder()) throws -> Data {
try encoder.encode(self)
}

}
58 changes: 58 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// DotLottieUtils.swift
// Lottie
//
// Created by Evandro Harrison Hoffmann on 27/06/2020.
//

import Foundation

// MARK: - DotLottieUtils

struct DotLottieUtils {
static let dotLottieExtension = "lottie"
static let jsonExtension = "json"

/// Temp folder to app directory
static var tempDirectoryURL: URL {
if #available(iOS 10.0, *) {
return FileManager.default.temporaryDirectory
}
return URL(fileURLWithPath: NSTemporaryDirectory())
}
}

extension URL {
/// Checks if url is a lottie file
var isDotLottie: Bool {
pathExtension == DotLottieUtils.dotLottieExtension
}

/// Checks if url is a json file
var isJsonFile: Bool {
pathExtension == DotLottieUtils.jsonExtension
}

var urls: [URL] {
FileManager.default.urls(for: self) ?? []
}
}

extension FileManager {
/// Lists urls for all files in a directory
/// - Parameters:
/// - url: URL of directory to search
/// - skipsHiddenFiles: If should or not show hidden files
/// - Returns: Returns urls of all files matching criteria in the directory
func urls(for url: URL, skipsHiddenFiles: Bool = true) -> [URL]? {
try? contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: skipsHiddenFiles ? .skipsHiddenFiles : [])
}
}

// MARK: - DotLottieError

public enum DotLottieError: Error {
case invalidFileFormat
case invalidData
case animationNotAvailable
}
Loading

0 comments on commit 41b05dd

Please sign in to comment.