1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187//
// DotLottie.swift
// Lottie
//
// Created by Evandro Harrison Hoffmann on 27/06/2020.
//
import Foundation
// MARK: - DotLottieFile
/// Detailed .lottie file structure
public final class DotLottieFile {
// MARK: Lifecycle
/// Loads `DotLottie` from `Data` object containing a compressed animation.
///
/// - Parameters:
/// - data: Data of .lottie file
/// - filename: Name of .lottie file
/// - Returns: Deserialized `DotLottie`. Optional.
init(data: Data, filename: String) throws {
fileUrl = DotLottieUtils.tempDirectoryURL.appendingPathComponent(filename.asFilename())
try decompress(data: data, to: fileUrl)
}
// MARK: Public
/// Definition for a single animation within a `DotLottieFile`
public struct Animation {
public let animation: LottieAnimation
public let configuration: DotLottieConfiguration
}
/// List of `LottieAnimation` in the file
public private(set) var animations = [Animation]()
// MARK: Internal
/// Image provider for animations
private(set) var imageProvider: DotLottieImageProvider?
/// Animations folder url
///
/// - Parameters:
/// - version: version of .lottie file
func animationsUrl(for version: String?) -> URL {
switch Int(version ?? "1") ?? 1 {
case 2...:
fileUrl.appendingPathComponent("a")
default:
fileUrl.appendingPathComponent("animations")
}
}
/// All files in animations folder
///
/// - Parameters:
/// - version: version of .lottie file
func animationUrls(for version: String?) -> [URL] {
FileManager.default.urls(for: animationsUrl(for: version)) ?? []
}
/// Images folder url
///
/// - Parameters:
/// - version: version of .lottie file
func imagesUrl(for version: String?) -> URL {
switch Int(version ?? "1") ?? 1 {
case 2...:
fileUrl.appendingPathComponent("i")
default:
fileUrl.appendingPathComponent("images")
}
}
/// All images in images folder
///
/// - Parameters:
/// - version: version of .lottie file
func imageUrls(for version: String?) -> [URL] {
FileManager.default.urls(for: imagesUrl(for: version)) ?? []
}
/// The `LottieAnimation` and `DotLottieConfiguration` for the given animation ID in this file
func animation(for id: String? = nil) -> DotLottieFile.Animation? {
if let id {
animations.first(where: { $0.configuration.id == id })
} else {
animations.first
}
}
/// The `LottieAnimation` and `DotLottieConfiguration` for the given animation index in this file
func animation(at index: Int) -> DotLottieFile.Animation? {
guard index < animations.count else { return nil }
return animations[index]
}
// MARK: Private
private static let manifestFileName = "manifest.json"
private let fileUrl: URL
/// Decompresses .lottie file from `URL` and saves to local temp folder
///
/// - Parameters:
/// - url: url to .lottie file
/// - destinationURL: url to destination of decompression contents
private func decompress(from url: URL, to destinationURL: URL) throws {
try? FileManager.default.removeItem(at: destinationURL)
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
try FileManager.default.unzipItem(at: url, to: destinationURL)
try loadContent()
try? FileManager.default.removeItem(at: destinationURL)
try? FileManager.default.removeItem(at: url)
}
/// Decompresses .lottie file from `Data` and saves to local temp folder
///
/// - Parameters:
/// - url: url to .lottie file
/// - destinationURL: url to destination of decompression contents
private func decompress(data: Data, to destinationURL: URL) throws {
let url = destinationURL.appendingPathExtension("lottie")
try FileManager.default.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
try data.write(to: url)
try decompress(from: url, to: destinationURL)
}
/// Loads file content to memory
private func loadContent() throws {
let manifest = try loadManifest()
imageProvider = DotLottieImageProvider(filepath: imagesUrl(for: manifest.version))
animations = try manifest.animations.map { dotLottieAnimation in
let animation = try dotLottieAnimation.animation(url: animationsUrl(for: manifest.version))
let configuration = DotLottieConfiguration(
id: dotLottieAnimation.id,
loopMode: dotLottieAnimation.loopMode,
speed: dotLottieAnimation.animationSpeed,
dotLottieImageProvider: imageProvider
)
return DotLottieFile.Animation(
animation: animation,
configuration: configuration
)
}
}
private func loadManifest() throws -> DotLottieManifest {
let path = fileUrl.appendingPathComponent(DotLottieFile.manifestFileName)
return try DotLottieManifest.load(from: path)
}
}
extension String {
// MARK: Fileprivate
fileprivate func asFilename() -> String {
lastPathComponent().removingPathExtension()
}
// MARK: Private
private func lastPathComponent() -> String {
(self as NSString).lastPathComponent
}
private func removingPathExtension() -> String {
(self as NSString).deletingPathExtension
}
}
// MARK: - DotLottieFile + @unchecked Sendable
// Mark `DotLottieFile` as `@unchecked Sendable` to allow it to be used when strict concurrency is enabled.
// In the future, it may be necessary to make changes to the internal implementation of `DotLottieFile`
// to make it truly thread-safe.
// swiftlint:disable:next no_unchecked_sendable
extension DotLottieFile: @unchecked Sendable { }