diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index a011989167..31b06deed8 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -29,8 +29,6 @@ 2E9E77AF27602BD400C84BA3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9E77AE27602BD400C84BA3 /* AppDelegate.swift */; }; 2EC6E5082763D981002E091C /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC6E5072763D981002E091C /* LinkView.swift */; }; 2EC6E5102763E79F002E091C /* AnimationPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC6E50F2763E79F002E091C /* AnimationPreviewViewController.swift */; }; - AB3278112A71A86E00A9C9F1 /* RemoteAnimationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */; }; - AB82EA9B2A7090B400AEBB48 /* RemoteAnimationDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -69,7 +67,6 @@ 2E9E77BC27602BD400C84BA3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2EC6E5072763D981002E091C /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; }; 2EC6E50F2763E79F002E091C /* AnimationPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationPreviewViewController.swift; sourceTree = ""; }; - AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnimationDemoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -104,7 +101,6 @@ 08E359932A55FFC400141956 /* LottieViewLayoutDemoView.swift */, 08E359972A55FFC600141956 /* Example.entitlements */, 607FACD11AFB9204008FA782 /* Products */, - AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */, ); path = Example; sourceTree = ""; @@ -288,7 +284,6 @@ 08E359922A55FFC400141956 /* ExampleApp.swift in Sources */, 085D97872A5E0DB600C78D18 /* AnimationPreviewView.swift in Sources */, 085D97852A5DF94C00C78D18 /* AnimationListView.swift in Sources */, - AB3278112A71A86E00A9C9F1 /* RemoteAnimationDemoView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -300,7 +295,6 @@ 2E1670CC2784F9C1009CDED3 /* AnimatedSwitchRow.swift in Sources */, 2E97E3052767E7C600FE22C3 /* Configuration.swift in Sources */, 2E1670C32784F009009CDED3 /* ControlsDemoViewController.swift in Sources */, - AB82EA9B2A7090B400AEBB48 /* RemoteAnimationDemoView.swift in Sources */, 2E1670CA2784F123009CDED3 /* AnimatedButtonRow.swift in Sources */, 2E362A1E2762BA06006AE7D2 /* SampleListViewController.swift in Sources */, 2E9E77AF27602BD400C84BA3 /* AppDelegate.swift in Sources */, diff --git a/Example/Example/AnimationListView.swift b/Example/Example/AnimationListView.swift index 407d905df6..1c19902372 100644 --- a/Example/Example/AnimationListView.swift +++ b/Example/Example/AnimationListView.swift @@ -10,29 +10,33 @@ struct AnimationListView: View { // MARK: Internal - let directory: String + enum Content: Hashable, Sendable { + case directory(_ directory: String) + case custom(name: String, items: [Item]) + } + + var content: Content var body: some View { List { ForEach(items, id: \.self) { item in NavigationLink(value: item) { switch item { - case .animation(let animationName, _): + case .animation, .remoteAnimations: HStack { - LottieView(animation: .named(animationName, subdirectory: directory)) - .currentProgress(0.5) - .imageProvider(.exampleAppSampleImages) - .frame(width: 50, height: 50) - .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) - - Text(animationName) + LottieView { + try await makeThumbnailAnimation(for: item) + } + .currentProgress(0.5) + .imageProvider(.exampleAppSampleImages) + .frame(width: 50, height: 50) + .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) + + Text(item.name) } - case .subdirectory(let subdirectoryURL): - Text(subdirectoryURL.lastPathComponent) - .frame(height: 50) - case .remoteDemo: - Text("Remote animations") + case .animationList: + Text(item.name) .frame(height: 50) } } @@ -40,16 +44,33 @@ struct AnimationListView: View { switch item { case .animation(_, let animationPath): AnimationPreviewView(animationSource: .local(animationPath: animationPath)) - case .subdirectory(let subdirectoryURL): - AnimationListView(directory: "\(directory)/\(subdirectoryURL.lastPathComponent)") - case .remoteDemo: - // View is already contained in a nav stack - RemoteAnimationsDemoView(wrapInNavStack: false) + case .remoteAnimations(let name, let urls): + AnimationPreviewView(animationSource: .remote(urls: urls, name: name)) + case .animationList(let listContent): + AnimationListView(content: listContent) } } } } - .navigationTitle(directory) + .navigationTitle(content.name) + } + + func makeThumbnailAnimation(for item: Item) async throws -> LottieAnimationSource? { + switch item { + case .animation(let animationName, _): + if animationName.hasSuffix(".lottie") { + return try await DotLottieFile.named(animationName, subdirectory: directory).animationSource + } else { + return LottieAnimation.named(animationName, subdirectory: directory)?.animationSource + } + + case .remoteAnimations(_, let urls): + guard let url = urls.first else { return nil } + return await LottieAnimation.loadedFrom(url: url)?.animationSource + + case .animationList: + return nil + } } // MARK: Private @@ -58,22 +79,37 @@ struct AnimationListView: View { directory == "Samples" } + private var directory: String { + switch content { + case .directory(let directory): + return directory + case .custom: + return "n/a" + } + } + } extension AnimationListView { // MARK: Internal - enum Item: Hashable { - case subdirectory(URL) + enum Item: Hashable, Sendable { + case animationList(AnimationListView.Content) case animation(name: String, path: String) - case remoteDemo + case remoteAnimations(name: String, urls: [URL]) } var items: [Item] { - animations.map { .animation(name: $0.name, path: $0.path) } - + subdirectoryURLs.map { .subdirectory($0) } - + customDemos + switch content { + case .directory: + return animations.map { .animation(name: $0.name, path: $0.path) } + + subdirectoryURLs.map { .animationList(.directory("\(directory)/\($0.lastPathComponent)")) } + + customDemos + + case .custom(_, let items): + return items + } } // MARK: Private @@ -111,6 +147,48 @@ extension AnimationListView { } private var customDemos: [Item] { - isTopLevel ? [.remoteDemo] : [] + guard isTopLevel else { return [] } + + return [ + .animationList(.remoteAnimationsDemo), + ] + } +} + +extension AnimationListView.Item { + var name: String { + switch self { + case .animation(let animationName, _), .remoteAnimations(let animationName, _): + return animationName + case .animationList(let content): + return content.name + } + } +} + +extension AnimationListView.Content { + static var remoteAnimationsDemo: AnimationListView.Content { + .custom( + name: "Remote Animations", + items: [ + .remoteAnimations( + name: "Rooms Animation", + urls: [URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!]), + .remoteAnimations( + name: "Multiple Animations", + urls: [ + URL(string: "https://a0.muscache.com/pictures/a7c140ee-6818-4a8a-b3b1-0c785054a611.json")!, + URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!, + ]), + ]) + } + + var name: String { + switch self { + case .directory(let directory): + return directory.components(separatedBy: "/").last ?? directory + case .custom(let name, _): + return name + } } } diff --git a/Example/Example/AnimationPreviewView.swift b/Example/Example/AnimationPreviewView.swift index b6559740d2..835ae6ceee 100644 --- a/Example/Example/AnimationPreviewView.swift +++ b/Example/Example/AnimationPreviewView.swift @@ -45,14 +45,14 @@ struct AnimationPreviewView: View { var body: some View { VStack { LottieView { - try await lottieSource() + try await loadAnimation() } placeholder: { LoadingIndicator() .frame(width: 50, height: 50) } .imageProvider(.exampleAppSampleImages) .resizable() - .loadAnimationTrigger($currentURLIndex) + .reloadAnimationTrigger(currentURLIndex, showPlaceholder: false) .looping() .currentProgress(animationPlaying ? nil : sliderValue) .getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil) @@ -86,18 +86,17 @@ struct AnimationPreviewView: View { @State private var sliderValue: AnimationProgressTime = 0 @State private var currentURLIndex: Int - private func lottieSource() async throws -> LottieAnimationSource? { + private func loadAnimation() async throws -> LottieAnimationSource? { switch animationSource { case .local(let name): - if let animation = LottieAnimation.named(name) { - return .lottieAnimation(animation) + if name.hasSuffix(".lottie") { + return try await DotLottieFile.named(name).animationSource } else { - let lottie = try await DotLottieFile.named(name) - return .dotLottieFile(lottie) + return LottieAnimation.named(name)?.animationSource } + case .remote: - let animation = await LottieAnimation.loadedFrom(url: urls[currentURLIndex]) - return animation.map(LottieAnimationSource.lottieAnimation) + return await LottieAnimation.loadedFrom(url: urls[currentURLIndex])?.animationSource } } diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift index b9d7a7c020..c6228ce82b 100644 --- a/Example/Example/ExampleApp.swift +++ b/Example/Example/ExampleApp.swift @@ -8,7 +8,7 @@ struct ExampleApp: App { var body: some Scene { WindowGroup { NavigationStack { - AnimationListView(directory: "Samples") + AnimationListView(content: .directory("Samples")) } } } diff --git a/Example/Example/RemoteAnimationDemoView.swift b/Example/Example/RemoteAnimationDemoView.swift deleted file mode 100644 index 488e362994..0000000000 --- a/Example/Example/RemoteAnimationDemoView.swift +++ /dev/null @@ -1,70 +0,0 @@ -// Created by miguel_jimenez on 7/25/23. -// Copyright © 2023 Airbnb Inc. All rights reserved. - -import Lottie -import SwiftUI - -// MARK: - AnimationListView - -@MainActor -struct RemoteAnimationsDemoView: View { - - struct Item: Hashable { - let name: String - let urls: [URL] - } - - let wrapInNavStack: Bool - - var body: some View { - if wrapInNavStack { - NavigationStack { - listBody - } - } else { - listBody - } - } - - var listBody: some View { - List { - ForEach(items, id: \.self) { item in - NavigationLink(value: item) { - HStack { - LottieView { - await LottieAnimation.loadedFrom(url: item.urls.first!) - } placeholder: { - LoadingIndicator() - } - .currentProgress(0.5) - .imageProvider(.exampleAppSampleImages) - .frame(width: 50, height: 50) - .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) - - Text(item.name) - } - } - .navigationDestination(for: Item.self) { item in - AnimationPreviewView(animationSource: .remote(urls: item.urls, name: item.name)) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .navigationTitle("Remote Animations") - } - } - - var items: [Item] { - [ - Item( - name: "Rooms Animation", - urls: [URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!]), - Item( - name: "Multiple Animations", - urls: [ - URL(string: "https://a0.muscache.com/pictures/a7c140ee-6818-4a8a-b3b1-0c785054a611.json")!, - URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!, - ]), - ] - } - -} diff --git a/Example/iOS/ViewControllers/SampleListViewController.swift b/Example/iOS/ViewControllers/SampleListViewController.swift index a98e213a6f..ea30478dcb 100644 --- a/Example/iOS/ViewControllers/SampleListViewController.swift +++ b/Example/iOS/ViewControllers/SampleListViewController.swift @@ -59,7 +59,6 @@ final class SampleListViewController: CollectionViewController { if isTopLevel { demoLinks - remoteAnimationLinks } } @@ -125,17 +124,6 @@ final class SampleListViewController: CollectionViewController { } } - @ItemModelBuilder - private var remoteAnimationLinks: [ItemModeling] { - LinkView.itemModel( - dataID: "Remote animations", - content: .init(animationName: nil, title: "Remote animations")) - .didSelect { [weak self] _ in - let remoteAnimationsDemo = UIHostingController(rootView: RemoteAnimationsDemoView(wrapInNavStack: true)) - self?.present(remoteAnimationsDemo, animated: true) - } - } - private func configureSettingsMenu() { navigationItem.rightBarButtonItem = UIBarButtonItem( title: "Settings", diff --git a/Lottie.xcodeproj/project.pbxproj b/Lottie.xcodeproj/project.pbxproj index ecf635e999..904ccc1f2f 100644 --- a/Lottie.xcodeproj/project.pbxproj +++ b/Lottie.xcodeproj/project.pbxproj @@ -822,6 +822,9 @@ AB87F02E2A72FA3A0091D7B8 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */; }; AB87F02F2A72FA3A0091D7B8 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */; }; AB87F0302A72FA3A0091D7B8 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */; }; + ABF033B42A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */; }; + ABF033B52A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */; }; + ABF033B62A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */; }; D453D8AB28FE6EE300D3F49C /* LottieAnimationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D453D8AA28FE6EE300D3F49C /* LottieAnimationCache.swift */; }; D453D8AC28FE6EE300D3F49C /* LottieAnimationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D453D8AA28FE6EE300D3F49C /* LottieAnimationCache.swift */; }; D453D8AD28FE6EE300D3F49C /* LottieAnimationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D453D8AA28FE6EE300D3F49C /* LottieAnimationCache.swift */; }; @@ -1132,6 +1135,7 @@ A40460582832C52B00ACFEDC /* BlendMode+Filter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BlendMode+Filter.swift"; sourceTree = ""; }; AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ValueChanged.swift"; sourceTree = ""; }; AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = ""; }; + ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEquatable.swift; sourceTree = ""; }; D453D8AA28FE6EE300D3F49C /* LottieAnimationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationCache.swift; sourceTree = ""; }; D453D8AE28FF9BC600D3F49C /* AnimationCacheProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationCacheProviderTests.swift; sourceTree = ""; }; D453D8B028FF9E3A00D3F49C /* DefaultAnimationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAnimationCache.swift; sourceTree = ""; }; @@ -1829,6 +1833,7 @@ AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */, 2E9C95D22822F43100677516 /* AnimationContext.swift */, AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */, + ABF033B32A7B0ABA00F8C228 /* AnyEquatable.swift */, ); path = Helpers; sourceTree = ""; @@ -2464,6 +2469,7 @@ 2EAF5AC227A0798700E00531 /* BundleImageProvider.swift in Sources */, 2E9C976B2822F43100677516 /* InterpolatableExtensions.swift in Sources */, 2E9C96ED2822F43100677516 /* ShapeItemLayer.swift in Sources */, + ABF033B42A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */, 2EAF5ACE27A0798700E00531 /* AnimationSubview.swift in Sources */, 08E207572A56014E002DCE17 /* EpoxyModelProperty.swift in Sources */, 2E9C96302822F43100677516 /* TextAnimator.swift in Sources */, @@ -2767,6 +2773,7 @@ 2E9C96CD2822F43100677516 /* ShapeRenderLayer.swift in Sources */, 5721092029119F3100169699 /* BezierPathRoundExtension.swift in Sources */, 08E207132A56014E002DCE17 /* EpoxySwiftUIHostingController.swift in Sources */, + ABF033B52A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */, 08E2070A2A56014E002DCE17 /* EpoxyableView+SwiftUIView.swift in Sources */, 6C48780328FF20140005AF07 /* DotLottieFile.swift in Sources */, 08E2070D2A56014E002DCE17 /* SwiftUIMeasurementContainer.swift in Sources */, @@ -3035,6 +3042,7 @@ 2E9C96EF2822F43100677516 /* ShapeItemLayer.swift in Sources */, 2EAF5AD027A0798700E00531 /* AnimationSubview.swift in Sources */, 2E9C96322822F43100677516 /* TextAnimator.swift in Sources */, + ABF033B62A7B0ABA00F8C228 /* AnyEquatable.swift in Sources */, 2E9C96E92822F43100677516 /* ImageLayer.swift in Sources */, 08E207592A56014E002DCE17 /* EpoxyModelProperty.swift in Sources */, 2E9C972E2822F43100677516 /* StarAnimation.swift in Sources */, diff --git a/Sources/Private/Utility/Helpers/AnyEquatable.swift b/Sources/Private/Utility/Helpers/AnyEquatable.swift new file mode 100644 index 0000000000..40e01c059f --- /dev/null +++ b/Sources/Private/Utility/Helpers/AnyEquatable.swift @@ -0,0 +1,24 @@ +// Created by miguel_jimenez on 8/2/23. +// Copyright © 2023 Airbnb Inc. All rights reserved. + +import Foundation + +// MARK: - AnyEquatable + +struct AnyEquatable { + private let value: Any + private let equals: (Any) -> Bool + + init(_ value: T) { + self.value = value + equals = { $0 as? T == value } + } +} + +// MARK: Equatable + +extension AnyEquatable: Equatable { + static func ==(lhs: AnyEquatable, rhs: AnyEquatable) -> Bool { + lhs.equals(rhs.value) + } +} diff --git a/Sources/Private/Utility/LottieAnimationSource.swift b/Sources/Private/Utility/LottieAnimationSource.swift index 1d3d86e617..744b68bb91 100644 --- a/Sources/Private/Utility/LottieAnimationSource.swift +++ b/Sources/Private/Utility/LottieAnimationSource.swift @@ -1,11 +1,21 @@ // Created by Cal Stephens on 7/26/23. // Copyright © 2023 Airbnb Inc. All rights reserved. +// MARK: - LottieAnimationSource + +/// A data source for a Lottie animation. +/// Either a `LottieAnimation` loaded from a `.json` file, +/// or a `DotLottieFile` loaded from a `.lottie` file. public enum LottieAnimationSource: Sendable { + /// A `LottieAnimation` loaded from a `.json` file case lottieAnimation(LottieAnimation) + + /// A `DotLottieFile` loaded from a `.lottie` file case dotLottieFile(DotLottieFile) +} - /// The animation displayed by this data source +extension LottieAnimationSource { + /// The default animation displayed by this data source var animation: LottieAnimation? { switch self { case .lottieAnimation(let animation): @@ -15,3 +25,17 @@ public enum LottieAnimationSource: Sendable { } } } + +extension LottieAnimation { + /// This animation represented as a `LottieAnimationSource` + public var animationSource: LottieAnimationSource { + .lottieAnimation(self) + } +} + +extension DotLottieFile { + /// This animation represented as a `LottieAnimationSource` + public var animationSource: LottieAnimationSource { + .dotLottieFile(self) + } +} diff --git a/Sources/Public/Animation/LottieView.swift b/Sources/Public/Animation/LottieView.swift index 1fa3d28806..816c9fb94d 100644 --- a/Sources/Public/Animation/LottieView.swift +++ b/Sources/Public/Animation/LottieView.swift @@ -17,7 +17,16 @@ public struct LottieView: UIViewConfiguringSwiftUIView { placeholder = nil } - /// Creates a `LottieView` that displays the given `DotLottieFile` + /// Initializes a `LottieView` with the provided `DotLottieFile` for display. + /// + /// - Important: Avoid using this initializer with the `SynchronouslyBlockingCurrentThread` APIs. + /// If decompression of a `.lottie` file is necessary, prefer using the `.init(_ loadAnimation:)` + /// initializer, which takes an asynchronous closure: + /// ``` + /// LottieView { + /// try await DotLottieFile.named(name) + /// } + /// ``` public init(dotLottieFile: DotLottieFile?) where Placeholder == EmptyView { _animationSource = State(initialValue: dotLottieFile.map(LottieAnimationSource.dotLottieFile)) placeholder = nil @@ -25,14 +34,14 @@ public struct LottieView: UIViewConfiguringSwiftUIView { /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`. /// The `loadAnimation` closure is called exactly once in `onAppear`. - /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. public init(_ loadAnimation: @escaping () async throws -> LottieAnimation?) where Placeholder == EmptyView { self.init(loadAnimation, placeholder: EmptyView.init) } /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimation`. /// The `loadAnimation` closure is called exactly once in `onAppear`. - /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`. public init( _ loadAnimation: @escaping () async throws -> LottieAnimation?, @@ -47,15 +56,29 @@ public struct LottieView: UIViewConfiguringSwiftUIView { /// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`. /// The `loadDotLottieFile` closure is called exactly once in `onAppear`. - /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. + /// You can use the `DotLottieFile` static methods API which use Swift concurrency to load your `.lottie` files: + /// ``` + /// LottieView { + /// try await DotLottieFile.named(name) + /// } + /// ``` public init(_ loadDotLottieFile: @escaping () async throws -> DotLottieFile?) where Placeholder == EmptyView { self.init(loadDotLottieFile, placeholder: EmptyView.init) } /// Creates a `LottieView` that asynchronously loads and displays the given `DotLottieFile`. /// The `loadDotLottieFile` closure is called exactly once in `onAppear`. - /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`. + /// You can use the `DotLottieFile` static methods API which use Swift concurrency to load your `.lottie` files: + /// ``` + /// LottieView { + /// try await DotLottieFile.named(name) + /// } placeholder: { + /// LoadingView() + /// } + /// ``` public init( _ loadDotLottieFile: @escaping () async throws -> DotLottieFile?, @ViewBuilder placeholder: @escaping (() -> Placeholder)) @@ -69,10 +92,18 @@ public struct LottieView: UIViewConfiguringSwiftUIView { /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimationSource`. /// The `loadAnimation` closure is called exactly once in `onAppear`. - /// If you wish to call `loadAnimation` again at a different time, you can use `.loadAnimationTrigger(...)`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. + /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`. + public init(_ loadAnimation: @escaping () async throws -> LottieAnimationSource?) where Placeholder == EmptyView { + self.init(loadAnimation, placeholder: EmptyView.init) + } + + /// Creates a `LottieView` that asynchronously loads and displays the given `LottieAnimationSource`. + /// The `loadAnimation` closure is called exactly once in `onAppear`. + /// If you wish to call `loadAnimation` again at a different time, you can use `.reloadAnimationTrigger(...)`. /// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`. public init( - loadAnimation: @escaping () async throws -> LottieAnimationSource?, + _ loadAnimation: @escaping () async throws -> LottieAnimationSource?, @ViewBuilder placeholder: @escaping () -> Placeholder) { self.loadAnimation = loadAnimation @@ -110,9 +141,8 @@ public struct LottieView: UIViewConfiguringSwiftUIView { .onAppear { loadAnimationIfNecessary() } - .valueChanged(value: loadAnimationTrigger?.wrappedValue) { _ in - animationSource = nil - loadAnimationIfNecessary() + .valueChanged(value: reloadAnimationTrigger) { _ in + reloadAnimationTriggerDidChange() } } @@ -299,12 +329,16 @@ public struct LottieView: UIViewConfiguringSwiftUIView { /// whenever the `binding` value is updated. /// /// - Note: This function requires a valid `loadAnimation` closure provided during view initialization, - /// otherwise the `loadAnimationTrigger` will have no effect. - /// - Note: The existing animation will be removed before calling `loadAnimation`, - /// which will cause the `Placeholder` to be displayed until the new animation finishes loading. - public func loadAnimationTrigger(_ binding: Binding) -> Self { + /// otherwise `reloadAnimationTrigger` will have no effect. + /// - Parameters: + /// - binding: The binding that triggers the reloading when its value changes. + /// - showPlaceholder: When `true`, the current animation will be removed before invoking `loadAnimation`, + /// displaying the `Placeholder` until the new animation loads. + /// When `false`, the previous animation remains visible while the new one loads. + public func reloadAnimationTrigger(_ value: Value, showPlaceholder: Bool = true) -> Self { var copy = self - copy.loadAnimationTrigger = binding.map(transform: AnyHashable.init) + copy.reloadAnimationTrigger = AnyEquatable(value) + copy.showPlaceholderWhileReloading = showPlaceholder return copy } @@ -355,8 +389,9 @@ public struct LottieView: UIViewConfiguringSwiftUIView { // MARK: Private @State private var animationSource: LottieAnimationSource? - private var loadAnimationTrigger: Binding? + private var reloadAnimationTrigger: AnyEquatable? private var loadAnimation: (() async throws -> LottieAnimationSource?)? + private var showPlaceholderWhileReloading = false private var imageProvider: AnimationImageProvider? private var textProvider: AnimationTextProvider = DefaultTextProvider() private var fontProvider: AnimationFontProvider = DefaultFontProvider() @@ -366,10 +401,7 @@ public struct LottieView: UIViewConfiguringSwiftUIView { private let placeholder: (() -> Placeholder)? private func loadAnimationIfNecessary() { - guard - let loadAnimation = loadAnimation, - animationSource == nil - else { return } + guard let loadAnimation = loadAnimation else { return } Task { do { @@ -380,4 +412,13 @@ public struct LottieView: UIViewConfiguringSwiftUIView { } } + private func reloadAnimationTriggerDidChange() { + guard loadAnimation != nil else { return } + + if showPlaceholderWhileReloading { + animationSource = nil + } + + loadAnimationIfNecessary() + } } diff --git a/Sources/Public/DotLottie/DotLottieFileHelpers.swift b/Sources/Public/DotLottie/DotLottieFileHelpers.swift index b7336668a9..e540992476 100644 --- a/Sources/Public/DotLottie/DotLottieFileHelpers.swift +++ b/Sources/Public/DotLottie/DotLottieFileHelpers.swift @@ -20,6 +20,10 @@ extension DotLottieFile { 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 = dotLottieCache, @@ -55,6 +59,10 @@ extension DotLottieFile { 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 @@ -91,6 +99,10 @@ extension DotLottieFile { 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)