// // DotLottieFileHelpers.swift // Lottie // // Created by Evandro Hoffmann on 20/10/22. // import Foundation extension DotLottieFile { public enum SynchronouslyBlockingCurrentThread { /// Loads an DotLottie from a specific filepath synchronously. Returns a `Result` /// Please use the asynchronous methods whenever possible. This operation will block the Thread it is running in. /// /// - Parameter filepath: The absolute filepath of the lottie to load. EG "/User/Me/starAnimation.lottie" /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. public static func loadedFrom( filepath: String, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) -> Result { LottieLogger.shared.assert( !Thread.isMainThread, "`DotLottieFile.SynchronouslyBlockingCurrentThread` methods shouldn't be called on the main thread.") /// Check cache for lottie if let dotLottieCache, let lottie = dotLottieCache.file(forKey: filepath) { return .success(lottie) } do { /// Decode the lottie. let url = URL(fileURLWithPath: filepath) let data = try Data(contentsOf: url) let lottie = try DotLottieFile(data: data, filename: url.deletingPathExtension().lastPathComponent) dotLottieCache?.setFile(lottie, forKey: filepath) return .success(lottie) } catch { /// Decoding Error. return .failure(error) } } /// Loads a DotLottie model from a bundle by its name synchronously. Returns a `Result` /// Please use the asynchronous methods whenever possible. This operation will block the Thread it is running in. /// /// - Parameter name: The name of the lottie file without the lottie extension. EG "StarAnimation" /// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main` /// - Parameter subdirectory: A subdirectory in the bundle in which the lottie is located. Optional. /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. public static func named( _ name: String, bundle: Bundle = Bundle.main, subdirectory: String? = nil, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) -> Result { LottieLogger.shared.assert( !Thread.isMainThread, "`DotLottieFile.SynchronouslyBlockingCurrentThread` methods shouldn't be called on the main thread.") /// Create a cache key for the lottie. let cacheKey = bundle.bundlePath + (subdirectory ?? "") + "/" + name /// Check cache for lottie if let dotLottieCache, let lottie = dotLottieCache.file(forKey: cacheKey) { return .success(lottie) } do { /// Decode animation. let data = try bundle.dotLottieData(name, subdirectory: subdirectory) let lottie = try DotLottieFile(data: data, filename: name) dotLottieCache?.setFile(lottie, forKey: cacheKey) return .success(lottie) } catch { /// Decoding error. LottieLogger.shared.warn("Error when decoding lottie \"\(name)\": \(error)") return .failure(error) } } /// Loads an DotLottie from a data synchronously. Returns a `Result` /// /// Please use the asynchronous methods whenever possible. This operation will block the Thread it is running in. /// /// - Parameters: /// - data: The data(`Foundation.Data`) object to load DotLottie from /// - filename: The name of the lottie file without the lottie extension. eg. "StarAnimation" public static func loadedFrom( data: Data, filename: String) -> Result { LottieLogger.shared.assert( !Thread.isMainThread, "`DotLottieFile.SynchronouslyBlockingCurrentThread` methods shouldn't be called on the main thread.") do { let dotLottieFile = try DotLottieFile(data: data, filename: filename) return .success(dotLottieFile) } catch { return .failure(error) } } } /// Loads a DotLottie model from a bundle by its name. Returns `nil` if a file is not found. /// /// - Parameter name: The name of the lottie file without the lottie extension. EG "StarAnimation" /// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main` /// - Parameter subdirectory: A subdirectory in the bundle in which the lottie is located. Optional. /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public static func named( _ name: String, bundle: Bundle = Bundle.main, subdirectory: String? = nil, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) async throws -> DotLottieFile { try await withCheckedThrowingContinuation { continuation in DotLottieFile.named(name, bundle: bundle, subdirectory: subdirectory, dotLottieCache: dotLottieCache) { result in continuation.resume(with: result) } } } /// Loads a DotLottie model from a bundle by its name. Returns `nil` if a file is not found. /// /// - Parameter name: The name of the lottie file without the lottie extension. EG "StarAnimation" /// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main` /// - Parameter subdirectory: A subdirectory in the bundle in which the lottie is located. Optional. /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. /// - Parameter dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional. /// - Parameter handleResult: A closure to be called when the file has loaded. public static func named( _ name: String, bundle: Bundle = Bundle.main, subdirectory: String? = nil, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache, dispatchQueue: DispatchQueue = .dotLottie, handleResult: @escaping (Result) -> Void) { dispatchQueue.async { let result = SynchronouslyBlockingCurrentThread.named( name, bundle: bundle, subdirectory: subdirectory, dotLottieCache: dotLottieCache) DispatchQueue.main.async { handleResult(result) } } } /// Loads an DotLottie from a specific filepath. /// - Parameter filepath: The absolute filepath of the lottie to load. EG "/User/Me/starAnimation.lottie" /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public static func loadedFrom( filepath: String, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) async throws -> DotLottieFile { try await withCheckedThrowingContinuation { continuation in DotLottieFile.loadedFrom(filepath: filepath, dotLottieCache: dotLottieCache) { result in continuation.resume(with: result) } } } /// Loads an DotLottie from a specific filepath. /// - Parameter filepath: The absolute filepath of the lottie to load. EG "/User/Me/starAnimation.lottie" /// - Parameter dotLottieCache: A cache for holding loaded lotties. Defaults to `LRUDotLottieCache.sharedCache`. Optional. /// - Parameter dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional. /// - Parameter handleResult: A closure to be called when the file has loaded. public static func loadedFrom( filepath: String, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache, dispatchQueue: DispatchQueue = .dotLottie, handleResult: @escaping (Result) -> Void) { dispatchQueue.async { let result = SynchronouslyBlockingCurrentThread.loadedFrom( filepath: filepath, dotLottieCache: dotLottieCache) DispatchQueue.main.async { handleResult(result) } } } /// Loads a DotLottie model from the asset catalog by its name. Returns `nil` if a lottie is not found. /// - Parameter name: The name of the lottie file in the asset catalog. EG "StarAnimation" /// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main` /// - Parameter dotLottieCache: A cache for holding loaded lottie files. Defaults to `LRUDotLottieCache.sharedCache` Optional. @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public static func asset( named name: String, bundle: Bundle = Bundle.main, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) async throws -> DotLottieFile { try await withCheckedThrowingContinuation { continuation in DotLottieFile.asset(named: name, bundle: bundle, dotLottieCache: dotLottieCache) { result in continuation.resume(with: result) } } } /// Loads a DotLottie model from the asset catalog by its name. Returns `nil` if a lottie is not found. /// - Parameter name: The name of the lottie file in the asset catalog. EG "StarAnimation" /// - Parameter bundle: The bundle in which the lottie is located. Defaults to `Bundle.main` /// - Parameter dotLottieCache: A cache for holding loaded lottie files. Defaults to `LRUDotLottieCache.sharedCache` Optional. /// - Parameter dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional. /// - Parameter handleResult: A closure to be called when the file has loaded. public static func asset( named name: String, bundle: Bundle = Bundle.main, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache, dispatchQueue: DispatchQueue = .dotLottie, handleResult: @escaping (Result) -> Void) { dispatchQueue.async { /// Create a cache key for the lottie. let cacheKey = bundle.bundlePath + "/" + name /// Check cache for lottie if let dotLottieCache, let lottie = dotLottieCache.file(forKey: cacheKey) { /// If found, return the lottie. DispatchQueue.main.async { handleResult(.success(lottie)) } return } do { /// Load data from Asset let data = try Data(assetName: name, in: bundle) /// Decode lottie. let lottie = try DotLottieFile(data: data, filename: name) dotLottieCache?.setFile(lottie, forKey: cacheKey) DispatchQueue.main.async { handleResult(.success(lottie)) } } catch { /// Decoding error. DispatchQueue.main.async { handleResult(.failure(error)) } } } } /// Loads a DotLottie animation asynchronously from the URL. /// /// - Parameter url: The url to load the animation from. /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LRUAnimationCache.sharedCache`. Optional. @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public static func loadedFrom( url: URL, session: URLSession = .shared, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache) async throws -> DotLottieFile { try await withCheckedThrowingContinuation { continuation in DotLottieFile.loadedFrom(url: url, session: session, dotLottieCache: dotLottieCache) { result in continuation.resume(with: result) } } } /// Loads a DotLottie animation asynchronously from the URL. /// /// - Parameter url: The url to load the animation from. /// - Parameter animationCache: A cache for holding loaded animations. Defaults to `LRUAnimationCache.sharedCache`. Optional. /// - Parameter handleResult: A closure to be called when the animation has loaded. public static func loadedFrom( url: URL, session: URLSession = .shared, dotLottieCache: DotLottieCacheProvider? = DotLottieCache.sharedCache, handleResult: @escaping (Result) -> Void) { if let dotLottieCache, let lottie = dotLottieCache.file(forKey: url.absoluteString) { handleResult(.success(lottie)) } else { let task = session.dataTask(with: url) { data, _, error in do { if let error { throw error } guard let data else { throw DotLottieError.noDataLoaded } let lottie = try DotLottieFile(data: data, filename: url.deletingPathExtension().lastPathComponent) DispatchQueue.main.async { dotLottieCache?.setFile(lottie, forKey: url.absoluteString) handleResult(.success(lottie)) } } catch { DispatchQueue.main.async { handleResult(.failure(error)) } } } task.resume() } } /// Loads an DotLottie from a data asynchronously. /// /// - Parameters: /// - data: The data(`Foundation.Data`) object to load DotLottie from /// - filename: The name of the lottie file without the lottie extension. eg. "StarAnimation" /// - dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional. /// - handleResult: A closure to be called when the file has loaded. public static func loadedFrom( data: Data, filename: String, dispatchQueue: DispatchQueue = .dotLottie, handleResult: @escaping (Result) -> Void) { dispatchQueue.async { do { let dotLottie = try DotLottieFile(data: data, filename: filename) DispatchQueue.main.async { handleResult(.success(dotLottie)) } } catch { DispatchQueue.main.async { handleResult(.failure(error)) } } } } /// Loads an DotLottie from a data asynchronously. /// /// - Parameters: /// - data: The data(`Foundation.Data`) object to load DotLottie from /// - filename: The name of the lottie file without the lottie extension. eg. "StarAnimation" /// - dispatchQueue: A dispatch queue used to load animations. Defaults to `DispatchQueue.global()`. Optional. @available(iOS 13.0, macOS 10.15, tvOS 13.0, *) public static func loadedFrom( data: Data, filename: String, dispatchQueue: DispatchQueue = .dotLottie) async throws -> DotLottieFile { try await withCheckedThrowingContinuation { continuation in loadedFrom(data: data, filename: filename, dispatchQueue: dispatchQueue) { result in continuation.resume(with: result) } } } } extension DispatchQueue { /// A serial dispatch queue ensures that IO related to loading dot Lottie files don't overlap, /// which can trigger file loading errors due to concurrent unzipping on a single archive. public static let dotLottie = DispatchQueue( label: "com.airbnb.lottie.dot-lottie", qos: .userInitiated) }