Skip to content

Commit

Permalink
Add snapshot tests for dot lottie animations, and other cleanup (#1794)
Browse files Browse the repository at this point in the history
  • Loading branch information
calda authored Nov 2, 2022
1 parent 982a8f1 commit 0c43942
Show file tree
Hide file tree
Showing 48 changed files with 280 additions and 168 deletions.
4 changes: 2 additions & 2 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2682,7 +2682,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.airbnb.LottieTests;
Expand All @@ -2701,7 +2701,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.airbnb.LottieTests;
Expand Down
15 changes: 10 additions & 5 deletions Sources/Private/Model/DotLottie/DotLottieImageProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import AppKit

// MARK: - DotLottieImageProvider

/// Provides an image for a lottie animation from a provided Bundle.
/// An image provider that loads the images from a DotLottieFile into memory
class DotLottieImageProvider: AnimationImageProvider {

// MARK: Lifecycle
Expand All @@ -38,12 +38,17 @@ class DotLottieImageProvider: AnimationImageProvider {
let filepath: URL

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

// MARK: Private

private var imageCache: NSCache<NSString, CGImage> = .init()
/// This is intentionally a Dictionary instead of an NSCache. Files from a decompressed dotLottie zip archive
/// are only valid are deleted after being read into memory. If we used an NSCache then the OS could evict
/// the cache entries when under memory pressure, and we would have no way to reload them later.
/// - Ideally we would have a way to remove image data when under memory pressure, but this would require
/// re-decompressing the dotLottie file when requesting an image that has been loaded but then removed.
private var images = [String: CGImage]()

private func loadImages() {
filepath.urls.forEach {
Expand All @@ -52,14 +57,14 @@ class DotLottieImageProvider: AnimationImageProvider {
let data = try? Data(contentsOf: $0),
let image = UIImage(data: data)?.cgImage
{
imageCache.setObject(image, forKey: $0.lastPathComponent as NSString)
images[$0.lastPathComponent] = image
}
#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)
images[$0.lastPathComponent] = image
}
#endif
}
Expand Down
3 changes: 0 additions & 3 deletions Sources/Public/Animation/LottieAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,4 @@ public final class LottieAnimation: Codable, DictionaryInitializable {
/// Markers
let markers: [Marker]?
let markerMap: [String: Marker]?

/// DotLottie configuration to setup the player
var dotLottieConfiguration: DotLottieConfiguration?
}
23 changes: 23 additions & 0 deletions Sources/Public/Animation/LottieAnimationHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ extension LottieAnimation {
}
}

/// Loads a Lottie animation asynchronously from the URL.
///
/// - Parameter url: The url to load the animation from.
/// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LottieAnimationCache.shared`. Optional.
///
@available(iOS 13.0, macOS 10.15, tvOS 13.0, *)
public static func loadedFrom(
url: URL,
session: URLSession = .shared,
animationCache: AnimationCacheProvider? = LottieAnimationCache.shared) async
-> LottieAnimation?
{
await withCheckedContinuation { continuation in
LottieAnimation.loadedFrom(
url: url,
session: session,
closure: { result in
continuation.resume(returning: result)
},
animationCache: animationCache)
}
}

/// Loads a Lottie animation asynchronously from the URL.
///
/// - Parameter url: The url to load the animation from.
Expand Down
22 changes: 12 additions & 10 deletions Sources/Public/Animation/LottieAnimationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,17 @@ final public class LottieAnimationView: LottieAnimationViewBase {
configuration: LottieConfiguration = .shared,
logger: LottieLogger = .shared)
{
animation = dotLottie?.animation(for: animationId)
let dotLottieAnimation = dotLottie?.animation(for: animationId)
animation = dotLottieAnimation?.animation
imageProvider = dotLottie?.imageProvider ?? BundleImageProvider(bundle: Bundle.main, searchPath: nil)
self.textProvider = textProvider
self.fontProvider = fontProvider
self.configuration = configuration
self.logger = logger
super.init(frame: .zero)
commonInit()
loopMode = animation?.dotLottieConfiguration?.loopMode ?? .playOnce
animationSpeed = CGFloat(animation?.dotLottieConfiguration?.speed ?? 1)
loopMode = dotLottieAnimation?.configuration.loopMode ?? .playOnce
animationSpeed = CGFloat(dotLottieAnimation?.configuration.speed ?? 1)
makeAnimationLayer(usingEngine: configuration.renderingEngine)
if let animation = animation {
frame = animation.bounds
Expand Down Expand Up @@ -440,15 +441,16 @@ final public class LottieAnimationView: LottieAnimationViewBase {
_ animationId: String? = nil,
from dotLottieFile: DotLottieFile)
{
guard let animation = dotLottieFile.animation(for: animationId) else { return }
if let configuration = animation.dotLottieConfiguration {
loopMode = configuration.loopMode
animationSpeed = CGFloat(configuration.speed)
}
if let imageProvider = dotLottieFile.imageProvider {
guard let dotLottieAnimation = dotLottieFile.animation(for: animationId) else { return }

loopMode = dotLottieAnimation.configuration.loopMode
animationSpeed = CGFloat(dotLottieAnimation.configuration.speed)

if let imageProvider = dotLottieAnimation.configuration.imageProvider {
self.imageProvider = imageProvider
}
self.animation = animation

animation = dotLottieAnimation.animation
}

/// Plays the animation from its current state to the end.
Expand Down
16 changes: 8 additions & 8 deletions Sources/Public/Animation/LottieAnimationViewInitializers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ extension LottieAnimationView {
configuration: LottieConfiguration = .shared)
{
self.init(dotLottie: nil, animationId: animationId, configuration: configuration)
DotLottieFile.named(name, bundle: bundle, subdirectory: nil, closure: { result in
DotLottieFile.named(name, bundle: bundle, dotLottieCache: dotLottieCache) { result in
guard case Result.success(let lottie) = result else { return }
self.loadAnimation(animationId, from: lottie)
}, dotLottieCache: dotLottieCache)
}
}

/// Loads a Lottie from a .lottie file in a specific path on disk.
Expand All @@ -132,10 +132,10 @@ extension LottieAnimationView {
configuration: LottieConfiguration = .shared)
{
self.init(dotLottie: nil, animationId: animationId, configuration: configuration)
DotLottieFile.filepath(filePath, closure: { result in
DotLottieFile.loadedFrom(filepath: filePath, dotLottieCache: dotLottieCache) { result in
guard case Result.success(let lottie) = result else { return }
self.loadAnimation(animationId, from: lottie)
}, dotLottieCache: dotLottieCache)
}
}

/// Loads a Lottie file asynchronously from the URL
Expand All @@ -156,14 +156,14 @@ extension LottieAnimationView {
closure(nil)
} else {
self.init(dotLottie: nil, configuration: configuration)
DotLottieFile.loadedFrom(url: url, closure: { result in
DotLottieFile.loadedFrom(url: url, dotLottieCache: dotLottieCache) { result in
switch result {
case .success(let lottie):
self.loadAnimation(animationId, from: lottie)
case .failure(let error):
closure(error)
}
}, dotLottieCache: dotLottieCache)
}
}
}

Expand All @@ -181,10 +181,10 @@ extension LottieAnimationView {
configuration: LottieConfiguration = .shared)
{
self.init(dotLottie: nil, animationId: animationId, configuration: configuration)
DotLottieFile.asset(name, bundle: bundle, closure: { result in
DotLottieFile.asset(named: name, bundle: bundle, dotLottieCache: dotLottieCache) { result in
guard case Result.success(let lottie) = result else { return }
self.loadAnimation(animationId, from: lottie)
}, dotLottieCache: dotLottieCache)
}
}

// MARK: Public
Expand Down
47 changes: 38 additions & 9 deletions Sources/Public/DotLottie/DotLottieFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ public final class DotLottieFile {

// MARK: Internal

/// Definition for a single animation within a `DotLottieFile`
struct Animation {
let animation: LottieAnimation
let configuration: DotLottieConfiguration
}

/// List of `LottieAnimation` in the file
var animations: [LottieAnimation] = []
private(set) var animations: [Animation] = []

/// Image provider for animations
var imageProvider: AnimationImageProvider?
private(set) var imageProvider: AnimationImageProvider?

/// Manifest.json file loading
lazy var manifest: DotLottieManifest? = {
Expand Down Expand Up @@ -62,6 +68,15 @@ public final class DotLottieFile {
FileManager.default.urls(for: imagesUrl) ?? []
}()

/// The `LottieAnimation` and `DotLottieConfiguration` for the given animation ID in this file
func animation(for id: String? = nil) -> DotLottieFile.Animation? {
if let id = id {
return animations.first(where: { $0.configuration.id == id })
} else {
return animations.first
}
}

// MARK: Private

private static let manifestFileName = "manifest.json"
Expand All @@ -70,6 +85,14 @@ public final class DotLottieFile {

private let fileUrl: URL

private var dotLottieAnimations: [DotLottieAnimation] {
manifest?.animations.map({
var animation = $0
animation.animationUrl = animationsUrl.appendingPathComponent("\($0.id).json")
return animation
}) ?? []
}

/// Decompresses .lottie file from `URL` and saves to local temp folder
///
/// - Parameters:
Expand Down Expand Up @@ -100,14 +123,20 @@ public final class DotLottieFile {
private func loadContent() {
imageProvider = DotLottieImageProvider(filepath: imagesUrl)

animations = dotLottieAnimations.compactMap {
let animation = try? $0.animation()
animation?.dotLottieConfiguration = DotLottieConfiguration(
id: $0.id,
animations = dotLottieAnimations.compactMap { dotLottieAnimation -> DotLottieFile.Animation? in
guard let animation = try? dotLottieAnimation.animation() else {
return nil
}

let configuration = DotLottieConfiguration(
id: dotLottieAnimation.id,
imageProvider: imageProvider,
loopMode: $0.loopMode,
speed: $0.animationSpeed)
return animation
loopMode: dotLottieAnimation.loopMode,
speed: dotLottieAnimation.animationSpeed)

return DotLottieFile.Animation(
animation: animation,
configuration: configuration)
}
}
}
Expand Down
Loading

0 comments on commit 0c43942

Please sign in to comment.