Skip to content

Commit

Permalink
Add loadAnimationTrigger to support calling loadAnimation again a…
Browse files Browse the repository at this point in the history
…fter `onAppear` (airbnb#2118)
  • Loading branch information
miguel-jimenez-0529 authored and Igor Moroz committed May 22, 2024
1 parent 7f75df5 commit 797b818
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 8 deletions.
37 changes: 34 additions & 3 deletions Example/Example/AnimationPreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,26 @@ import SwiftUI
/// TODO: Implement functionality from UIKit `AnimationPreviewViewController`
struct AnimationPreviewView: View {

// MARK: Lifecycle

init(animationSource: AnimationSource) {
self.animationSource = animationSource

switch animationSource {
case .remote(let urls, _):
_currentURLIndex = State(initialValue: urls.startIndex)
self.urls = urls
default:
_currentURLIndex = State(initialValue: 0)
urls = []
}
}

// MARK: Internal

enum AnimationSource {
case local(animationPath: String)
case remote(url: URL, name: String)
case remote(urls: [URL], name: String)

var name: String {
switch self {
Expand All @@ -33,9 +48,11 @@ struct AnimationPreviewView: View {
try await lottieSource()
} placeholder: {
LoadingIndicator()
.frame(width: 50, height: 50)
}
.imageProvider(.exampleAppSampleImages)
.resizable()
.loadAnimationTrigger($currentURLIndex)
.looping()
.currentProgress(animationPlaying ? nil : sliderValue)
.getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)
Expand All @@ -54,12 +71,20 @@ struct AnimationPreviewView: View {
.navigationTitle(animationSource.name.components(separatedBy: "/").last!)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.secondaryBackground)
.onReceive(timer) { _ in
updateIndex()
}
}

// MARK: Private

/// Used for remote animations only, when more than one URL is provided we loop over the urls every 2 seconds.
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
private let urls: [URL]

@State private var animationPlaying = true
@State private var sliderValue: AnimationProgressTime = 0
@State private var currentURLIndex: Int

private func lottieSource() async throws -> LottieAnimationSource? {
switch animationSource {
Expand All @@ -70,11 +95,17 @@ struct AnimationPreviewView: View {
let lottie = try await DotLottieFile.named(name)
return .dotLottieFile(lottie)
}
case .remote(let url, _):
let animation = await LottieAnimation.loadedFrom(url: url)
case .remote:
let animation = await LottieAnimation.loadedFrom(url: urls[currentURLIndex])
return animation.map(LottieAnimationSource.lottieAnimation)
}
}

private func updateIndex() {
let currentIndex = currentURLIndex
let nextIndex = currentIndex == urls.index(before: urls.endIndex) ? urls.startIndex : currentIndex + 1
currentURLIndex = nextIndex
}
}

extension Color {
Expand Down
14 changes: 10 additions & 4 deletions Example/Example/RemoteAnimationDemoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct RemoteAnimationsDemoView: View {

struct Item: Hashable {
let name: String
let url: URL
let urls: [URL]
}

let wrapInNavStack: Bool
Expand All @@ -31,7 +31,7 @@ struct RemoteAnimationsDemoView: View {
NavigationLink(value: item) {
HStack {
LottieView {
await LottieAnimation.loadedFrom(url: item.url)
await LottieAnimation.loadedFrom(url: item.urls.first!)
} placeholder: {
LoadingIndicator()
}
Expand All @@ -44,7 +44,7 @@ struct RemoteAnimationsDemoView: View {
}
}
.navigationDestination(for: Item.self) { item in
AnimationPreviewView(animationSource: .remote(url: item.url, name: item.name))
AnimationPreviewView(animationSource: .remote(urls: item.urls, name: item.name))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand All @@ -56,7 +56,13 @@ struct RemoteAnimationsDemoView: View {
[
Item(
name: "Rooms Animation",
url: URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!),
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")!,
]),
]
}

Expand Down
16 changes: 16 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,12 @@
A40460592832C52B00ACFEDC /* BlendMode+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40460582832C52B00ACFEDC /* BlendMode+Filter.swift */; };
A404605A2832C52B00ACFEDC /* BlendMode+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40460582832C52B00ACFEDC /* BlendMode+Filter.swift */; };
A404605B2832C52B00ACFEDC /* BlendMode+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40460582832C52B00ACFEDC /* BlendMode+Filter.swift */; };
AB3278132A71BA0400A9C9F1 /* View+ValueChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */; };
AB3278142A71BA3500A9C9F1 /* View+ValueChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */; };
AB87F02C2A72F5A80091D7B8 /* View+ValueChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */; };
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 */; };
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 */; };
Expand Down Expand Up @@ -1124,6 +1130,8 @@
82A552742A2FD44B00E47AC8 /* LottieAnimationLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationLayer.swift; sourceTree = "<group>"; };
A1D5BAAB27C731A500777D06 /* DataURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataURLTests.swift; sourceTree = "<group>"; };
A40460582832C52B00ACFEDC /* BlendMode+Filter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BlendMode+Filter.swift"; sourceTree = "<group>"; };
AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ValueChanged.swift"; sourceTree = "<group>"; };
AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = "<group>"; };
D453D8AA28FE6EE300D3F49C /* LottieAnimationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationCache.swift; sourceTree = "<group>"; };
D453D8AE28FF9BC600D3F49C /* AnimationCacheProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationCacheProviderTests.swift; sourceTree = "<group>"; };
D453D8B028FF9E3A00D3F49C /* DefaultAnimationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAnimationCache.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1818,7 +1826,9 @@
2E9C95D12822F43100677516 /* Helpers */ = {
isa = PBXGroup;
children = (
AB3278122A71BA0400A9C9F1 /* View+ValueChanged.swift */,
2E9C95D22822F43100677516 /* AnimationContext.swift */,
AB87F02D2A72FA3A0091D7B8 /* Binding+Map.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -2336,6 +2346,7 @@
2E9C96BA2822F43100677516 /* KeypathSearchable.swift in Sources */,
2E9C963C2822F43100677516 /* AssetLibrary.swift in Sources */,
2E9C97022822F43100677516 /* PreCompLayer.swift in Sources */,
AB3278132A71BA0400A9C9F1 /* View+ValueChanged.swift in Sources */,
2E9C96EA2822F43100677516 /* SolidLayer.swift in Sources */,
08E207302A56014E002DCE17 /* WillDisplayProviding.swift in Sources */,
08E207482A56014E002DCE17 /* AnimatedProviding.swift in Sources */,
Expand Down Expand Up @@ -2365,6 +2376,7 @@
5721091B2910874A00169699 /* RoundedCornersNode.swift in Sources */,
2E9C95FA2822F43100677516 /* Star.swift in Sources */,
2E9C961E2822F43100677516 /* KeyedDecodingContainerExtensions.swift in Sources */,
AB87F02E2A72FA3A0091D7B8 /* Binding+Map.swift in Sources */,
08C001F32A46150D00AB54BA /* Archive+Helpers.swift in Sources */,
08C002022A46150D00AB54BA /* Archive+ReadingDeprecated.swift in Sources */,
2E9C96512822F43100677516 /* PreCompositionLayer.swift in Sources */,
Expand Down Expand Up @@ -2573,7 +2585,9 @@
2E9C96B22822F43100677516 /* NodeProperty.swift in Sources */,
08E2071C2A56014E002DCE17 /* EpoxySwiftUIHostingView.swift in Sources */,
2E9C965E2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
AB87F02C2A72F5A80091D7B8 /* View+ValueChanged.swift in Sources */,
2E9C964F2822F43100677516 /* SolidCompositionLayer.swift in Sources */,
AB87F02F2A72FA3A0091D7B8 /* Binding+Map.swift in Sources */,
2E9C96402822F43100677516 /* Asset.swift in Sources */,
2E9C96FA2822F43100677516 /* BaseCompositionLayer.swift in Sources */,
2EAF5A9C27A0798700E00531 /* BundleImageProvider.macOS.swift in Sources */,
Expand Down Expand Up @@ -2903,6 +2917,7 @@
2E9C97042822F43100677516 /* PreCompLayer.swift in Sources */,
2E9C96EC2822F43100677516 /* SolidLayer.swift in Sources */,
08C002E82A46196300AB54BA /* Data+CompressionDeprecated.swift in Sources */,
AB3278142A71BA3500A9C9F1 /* View+ValueChanged.swift in Sources */,
2EAF5AA327A0798700E00531 /* AnimationSubview.macOS.swift in Sources */,
08E207322A56014E002DCE17 /* WillDisplayProviding.swift in Sources */,
08E2074A2A56014E002DCE17 /* AnimatedProviding.swift in Sources */,
Expand Down Expand Up @@ -2932,6 +2947,7 @@
5721091D2910874A00169699 /* RoundedCornersNode.swift in Sources */,
2E9C95FC2822F43100677516 /* Star.swift in Sources */,
2E9C96202822F43100677516 /* KeyedDecodingContainerExtensions.swift in Sources */,
AB87F0302A72FA3A0091D7B8 /* Binding+Map.swift in Sources */,
2E9C96532822F43100677516 /* PreCompositionLayer.swift in Sources */,
2EAF5AF427A0798700E00531 /* AnyValueProvider.swift in Sources */,
2E9C96652822F43100677516 /* CoreTextRenderLayer.swift in Sources */,
Expand Down
18 changes: 18 additions & 0 deletions Sources/Private/Utility/Helpers/Binding+Map.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Created by miguel_jimenez on 7/27/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

import SwiftUI

@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension Binding {

/// Helper to transform a `Binding` from one `Value` type to another.
func map<Transformed>(transform: @escaping (Value) -> Transformed) -> Binding<Transformed> {
.init {
transform(wrappedValue)
} set: { newValue in
guard let newValue = newValue as? Value else { return }
self.wrappedValue = newValue
}
}
}
20 changes: 20 additions & 0 deletions Sources/Private/Utility/Helpers/View+ValueChanged.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Created by miguel_jimenez on 7/26/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

import Combine
import SwiftUI

@available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
extension View {
/// A backwards compatible wrapper for iOS 14 `onChange`
@ViewBuilder
func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *, macOS 11.0, tvOS 14.0) {
self.onChange(of: value, perform: onChange)
} else {
onReceive(Just(value)) { value in
onChange(value)
}
}
}
}
24 changes: 23 additions & 1 deletion Sources/Public/Animation/LottieView.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Created by Bryn Bodayle on 1/20/22.
// Copyright © 2022 Airbnb Inc. All rights reserved.

import Combine
import SwiftUI

// MARK: - LottieView
Expand All @@ -26,12 +25,14 @@ public struct LottieView<Placeholder: View>: 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(...)`.
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(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(
_ loadAnimation: @escaping () async throws -> LottieAnimation?,
Expand All @@ -46,12 +47,14 @@ public struct LottieView<Placeholder: View>: 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(...)`.
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(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(
_ loadDotLottieFile: @escaping () async throws -> DotLottieFile?,
Expand All @@ -66,6 +69,7 @@ public struct LottieView<Placeholder: View>: 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(...)`.
/// While the animation is loading, the `placeholder` view is shown in place of the `LottieAnimationView`.
public init(
loadAnimation: @escaping () async throws -> LottieAnimationSource?,
Expand Down Expand Up @@ -106,6 +110,10 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
.onAppear {
loadAnimationIfNecessary()
}
.valueChanged(value: loadAnimationTrigger?.wrappedValue) { _ in
animationSource = nil
loadAnimationIfNecessary()
}
}

/// Returns a copy of this `LottieView` updated to have the given closure applied to its
Expand Down Expand Up @@ -287,6 +295,19 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
}
}

/// Returns a new instance of this view, which will invoke the provided `loadAnimation` closure
/// 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<Value: Hashable>(_ binding: Binding<Value>) -> Self {
var copy = self
copy.loadAnimationTrigger = binding.map(transform: AnyHashable.init)
return copy
}

/// Returns a view that updates the given binding each frame with the animation's `realtimeAnimationProgress`.
/// The `LottieView` is wrapped in a `TimelineView` with the `.animation` schedule.
/// - This is a one-way binding. Its value is updated but never read.
Expand Down Expand Up @@ -334,6 +355,7 @@ public struct LottieView<Placeholder: View>: UIViewConfiguringSwiftUIView {
// MARK: Private

@State private var animationSource: LottieAnimationSource?
private var loadAnimationTrigger: Binding<AnyHashable>?
private var loadAnimation: (() async throws -> LottieAnimationSource?)?
private var imageProvider: AnimationImageProvider?
private var textProvider: AnimationTextProvider = DefaultTextProvider()
Expand Down

0 comments on commit 797b818

Please sign in to comment.