diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 15805de0bd..bbd7b178b6 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 08E3599F2A56004100141956 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 08E3599E2A56004100141956 /* Lottie */; }; 08E6CF822A86C35B00A6D92F /* LottieSwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF812A86C35B00A6D92F /* LottieSwitchRow.swift */; }; 08E6CF842A86C49300A6D92F /* ControlsDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6CF832A86C49300A6D92F /* ControlsDemoView.swift */; }; + ABD0BC842B5B451C003D7587 /* SwiftUIInteroperabilityDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABD0BC832B5B451C003D7587 /* SwiftUIInteroperabilityDemoView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +33,7 @@ 08E6CF832A86C49300A6D92F /* ControlsDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsDemoView.swift; sourceTree = ""; }; 2E0F2FB627602C1500B65DE3 /* .. */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ..; sourceTree = ""; }; 2E3EEB372763C68C00287EEA /* Samples */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Samples; path = ../Tests/Samples; sourceTree = ""; }; + ABD0BC832B5B451C003D7587 /* SwiftUIInteroperabilityDemoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUIInteroperabilityDemoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +51,7 @@ 08E359902A55FFC400141956 /* Example */ = { isa = PBXGroup; children = ( + ABD0BC832B5B451C003D7587 /* SwiftUIInteroperabilityDemoView.swift */, 08E359912A55FFC400141956 /* ExampleApp.swift */, 085D97832A5DF76C00C78D18 /* AnimationListView.swift */, 085D97862A5E0DB600C78D18 /* AnimationPreviewView.swift */, @@ -165,6 +168,7 @@ buildActionMask = 2147483647; files = ( 0820D59B2A8ACE64007D705C /* LottieButtonRow.swift in Sources */, + ABD0BC842B5B451C003D7587 /* SwiftUIInteroperabilityDemoView.swift in Sources */, 08E359942A55FFC400141956 /* LottieViewLayoutDemoView.swift in Sources */, 08E359922A55FFC400141956 /* ExampleApp.swift in Sources */, 085D97872A5E0DB600C78D18 /* AnimationPreviewView.swift in Sources */, diff --git a/Example/Example/AnimationListView.swift b/Example/Example/AnimationListView.swift index 9ab0c3cf46..0873e0271c 100644 --- a/Example/Example/AnimationListView.swift +++ b/Example/Example/AnimationListView.swift @@ -35,7 +35,7 @@ struct AnimationListView: View { Text(item.name) } - case .animationList, .controlsDemo: + case .animationList, .controlsDemo, .swiftUIInteroperability: Text(item.name) .frame(height: 50) } @@ -50,6 +50,8 @@ struct AnimationListView: View { AnimationListView(content: listContent) case .controlsDemo: ControlsDemoView() + case .swiftUIInteroperability: + SwiftUIInteroperabilityDemoView() } } } @@ -70,7 +72,7 @@ struct AnimationListView: View { guard let url = urls.first else { return nil } return await LottieAnimation.loadedFrom(url: url)?.animationSource - case .animationList, .controlsDemo: + case .animationList, .controlsDemo, .swiftUIInteroperability: return nil } } @@ -101,6 +103,7 @@ extension AnimationListView { case animation(name: String, path: String) case remoteAnimations(name: String, urls: [URL]) case controlsDemo + case swiftUIInteroperability } var items: [Item] { @@ -155,6 +158,7 @@ extension AnimationListView { return [ .animationList(.remoteAnimationsDemo), .controlsDemo, + .swiftUIInteroperability, ] } } @@ -168,6 +172,8 @@ extension AnimationListView.Item { return content.name case .controlsDemo: return "Controls Demo" + case .swiftUIInteroperability: + return "SwiftUI Interoperability Demo" } } } diff --git a/Example/Example/SwiftUIInteroperabilityDemoView.swift b/Example/Example/SwiftUIInteroperabilityDemoView.swift new file mode 100644 index 0000000000..0385b697a7 --- /dev/null +++ b/Example/Example/SwiftUIInteroperabilityDemoView.swift @@ -0,0 +1,131 @@ +// Created by miguel_jimenez on 1/19/24. +// Copyright © 2024 Airbnb Inc. All rights reserved. + +import Lottie +import SwiftUI + +// MARK: - SwiftUIInteroperabilityDemoView + +struct SwiftUIInteroperabilityDemoView: View { + + var body: some View { + List { + Demo(name: "On appear offset animation") { + OnAppearOffsetAnimation() + } + + Demo(name: "Placeholder size inheritance") { + PlaceholderSizeInheritance() + } + } + .navigationTitle("SwiftUI Interoperability Demo") + } +} + +// MARK: - Demo + +struct Demo: View { + + // MARK: Lifecycle + + init(name: String, @ViewBuilder content: () -> Content) { + self.name = name + self.content = content() + } + + // MARK: Internal + + let name: String + let content: Content + + var body: some View { + VStack(alignment: .leading) { + Text(name) + .frame(alignment: .top) + Spacer() + HStack(alignment: .center) { + Button(show ? "Hide" : "Show") { + show.toggle() + } + if show { + content + } + } + Spacer() + } + .frame(height: 150) + } + + // MARK: Private + + @State private var show = false + +} + +// MARK: - OnAppearOffsetAnimation + +/// Demonstrates how `LottieView` is animated by the `.offset` modifier. +struct OnAppearOffsetAnimation: View { + + @State private var demo1Appeared = false + + var body: some View { + LottieView { + try await DotLottieFile.named("Samples/DotLottie/multiple_animations.lottie") + } placeholder: { + LoadingIndicator() + } + .looping() + .resizable() + .frame(width: 100, height: 100) + .offset(x: demo1Appeared ? 0 : 300) + .onAppear { + withAnimation { + demo1Appeared = true + } + } + } +} + +// MARK: - PlaceholderSizeInheritance + +/// Demonstrates how the placeholder's `Rectangle` get's its size from it's parent. +struct PlaceholderSizeInheritance: View { + + var body: some View { + HStack(alignment: .top) { + LottieView { + await LottieAnimation + .loadedFrom(url: URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!) + } placeholder: { + Rectangle() + .fill(.red) + .cornerRadius(20) + } + .resizable() + .frame(width: 100, height: 100) + + LottieView { + await LottieAnimation + .loadedFrom(url: URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!) + } placeholder: { + Rectangle() + .fill(.red) + .cornerRadius(10) + } + .resizable() + .frame(width: 50, height: 50) + + LottieView { + await LottieAnimation + .loadedFrom(url: URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!) + } placeholder: { + Rectangle() + .fill(.red) + .cornerRadius(3) + } + .resizable() + .frame(width: 10, height: 10) + } + } +} diff --git a/Sources/Public/Animation/LottieView.swift b/Sources/Public/Animation/LottieView.swift index ce54370bde..463c0ed5c6 100644 --- a/Sources/Public/Animation/LottieView.swift +++ b/Sources/Public/Animation/LottieView.swift @@ -115,26 +115,24 @@ public struct LottieView: UIViewConfiguringSwiftUIView { // MARK: Public public var body: some View { - ZStack { - if let animationSource { - LottieAnimationView.swiftUIView { - defer { animationDidLoad?(animationSource) } - return LottieAnimationView( - animationSource: animationSource, - imageProvider: imageProviderConfiguration?.imageProvider, - textProvider: textProvider, - fontProvider: fontProvider, - configuration: configuration, - logger: logger) - } - .sizing(sizing) - .configure { context in - applyCurrentAnimationConfiguration(to: context.view) - } - .configurations(configurations) - } else { - placeholder?() - } + LottieAnimationView.swiftUIView { + LottieAnimationView( + animationSource: animationSource, + imageProvider: imageProviderConfiguration?.imageProvider, + textProvider: textProvider, + fontProvider: fontProvider, + configuration: configuration, + logger: logger) + } + .sizing(sizing) + .configure { context in + applyCurrentAnimationConfiguration(to: context.view) + } + .configurations(configurations) + .opacity(animationSource == nil ? 0 : 1) + .overlay { + placeholder?() + .opacity(animationSource == nil ? 1 : 0) } .onAppear { loadAnimationIfNecessary() @@ -565,4 +563,17 @@ public struct LottieView: UIViewConfiguringSwiftUIView { } } } + +@available(iOS 13.0, tvOS 13.0, macOS 10.15, *) +extension View { + + /// The `.overlay` modifier that uses a `ViewBuilder` is available in iOS 15+, this helper function helps us to use the same API in older OSs + fileprivate func overlay( + @ViewBuilder content: () -> some View) + -> some View + { + overlay(content(), alignment: .center) + } +} + #endif