Skip to content

Commit

Permalink
Add SwiftUI support for loading async LottieAnimations and DotLottieF…
Browse files Browse the repository at this point in the history
…iles

Co-authored-by: Cal Stephens <[email protected]>
  • Loading branch information
miguel-jimenez-0529 and calda committed Jul 26, 2023
1 parent edf0dd8 commit 5f21ab0
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 37 deletions.
6 changes: 6 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
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 */
Expand Down Expand Up @@ -67,6 +69,7 @@
2E9E77BC27602BD400C84BA3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2EC6E5072763D981002E091C /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = "<group>"; };
2EC6E50F2763E79F002E091C /* AnimationPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationPreviewViewController.swift; sourceTree = "<group>"; };
AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnimationDemoView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -101,6 +104,7 @@
08E359932A55FFC400141956 /* LottieViewLayoutDemoView.swift */,
08E359972A55FFC600141956 /* Example.entitlements */,
607FACD11AFB9204008FA782 /* Products */,
AB82EA9A2A7090B400AEBB48 /* RemoteAnimationDemoView.swift */,
);
path = Example;
sourceTree = "<group>";
Expand Down Expand Up @@ -284,6 +288,7 @@
08E359922A55FFC400141956 /* ExampleApp.swift in Sources */,
085D97872A5E0DB600C78D18 /* AnimationPreviewView.swift in Sources */,
085D97852A5DF94C00C78D18 /* AnimationListView.swift in Sources */,
AB3278112A71A86E00A9C9F1 /* RemoteAnimationDemoView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -295,6 +300,7 @@
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 */,
Expand Down
25 changes: 23 additions & 2 deletions Example/Example/AnimationListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import SwiftUI

struct AnimationListView: View {

// MARK: Internal

let directory: String

var body: some View {
Expand All @@ -29,18 +31,31 @@ struct AnimationListView: View {
case .subdirectory(let subdirectoryURL):
Text(subdirectoryURL.lastPathComponent)
.frame(height: 50)
case .remoteDemo:
Text("Remote animations")
.frame(height: 50)
}
}
.navigationDestination(for: Item.self) { item in
switch item {
case .animation(_, let animationPath):
AnimationPreviewView(animationName: 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)
}
}
}
}.navigationTitle(directory)
}
.navigationTitle(directory)
}

// MARK: Private

private var isTopLevel: Bool {
directory == "Samples"
}

}
Expand All @@ -52,11 +67,13 @@ extension AnimationListView {
enum Item: Hashable {
case subdirectory(URL)
case animation(name: String, path: String)
case remoteDemo
}

var items: [Item] {
animations.map { .animation(name: $0.name, path: $0.path) }
+ subdirectoryURLs.map { .subdirectory($0) }
+ customDemos
}

// MARK: Private
Expand Down Expand Up @@ -92,4 +109,8 @@ extension AnimationListView {
(Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: directory) ?? []) +
(Bundle.main.urls(forResourcesWithExtension: "lottie", subdirectory: directory) ?? [])
}

private var customDemos: [Item] {
isTopLevel ? [.remoteDemo] : []
}
}
67 changes: 59 additions & 8 deletions Example/Example/AnimationPreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,34 @@ struct AnimationPreviewView: View {

// MARK: Internal

let animationName: String
enum AnimationSource {
case local(animationPath: String)
case remote(url: URL, name: String)

var name: String {
switch self {
case .local(let name):
return name
case .remote(_, let name):
return name
}
}
}

let animationSource: AnimationSource

var body: some View {
VStack {
LottieView(animation: .named(animationName))
.imageProvider(.exampleAppSampleImages)
.resizable()
.looping()
.currentProgress(animationPlaying ? nil : sliderValue)
.getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)
LottieView {
try await lottieSource()
} placeholder: {
LoadingIndicator()
}
.imageProvider(.exampleAppSampleImages)
.resizable()
.looping()
.currentProgress(animationPlaying ? nil : sliderValue)
.getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)

Spacer()

Expand All @@ -33,7 +51,7 @@ struct AnimationPreviewView: View {
.padding(.all, 16)
#endif
}
.navigationTitle(animationName.components(separatedBy: "/").last!)
.navigationTitle(animationSource.name.components(separatedBy: "/").last!)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.secondaryBackground)
}
Expand All @@ -43,6 +61,20 @@ struct AnimationPreviewView: View {
@State private var animationPlaying = true
@State private var sliderValue: AnimationProgressTime = 0

private func lottieSource() async throws -> LottieAnimationSource? {
switch animationSource {
case .local(let name):
if let animation = LottieAnimation.named(name) {
return .lottieAnimation(animation)
} else {
let lottie = try await DotLottieFile.named(name)
return .dotLottieFile(lottie)
}
case .remote(let url, _):
let animation = await LottieAnimation.loadedFrom(url: url)
return animation.map(LottieAnimationSource.lottieAnimation)
}
}
}

extension Color {
Expand All @@ -60,3 +92,22 @@ extension AnimationImageProvider where Self == FilepathImageProvider {
FilepathImageProvider(filepath: Bundle.main.resourceURL!.appending(path: "Samples/Images"))
}
}

// MARK: - LoadingIndicator

struct LoadingIndicator: View {
@State private var animating = false

var body: some View {
Image(systemName: "rays")
.rotationEffect(animating ? Angle.degrees(360) : .zero)
.animation(
Animation
.linear(duration: 2)
.repeatForever(autoreverses: false),
value: animating)
.onAppear {
animating = true
}
}
}
8 changes: 4 additions & 4 deletions Example/Example/Example.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
63 changes: 63 additions & 0 deletions Example/Example/RemoteAnimationDemoView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Created by miguel_jimenez on 7/25/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

import Lottie
import SwiftUI

// MARK: - AnimationListView

struct RemoteAnimationsDemoView: View {

struct Item: Hashable {
let name: String
let url: 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.url)
} 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(url: item.url, name: item.name))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Remote Animations")
}
}

var items: [Item] {
[
Item(
name: "Rooms Animation",
url: URL(string: "https://a0.muscache.com/pictures/96699af6-b73e-499f-b0f5-3c862ae7d126.json")!),
]
}

}
14 changes: 13 additions & 1 deletion Example/iOS/ViewControllers/SampleListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class SampleListViewController: CollectionViewController {

if isTopLevel {
demoLinks
remoteAnimationLinks
}
}

Expand Down Expand Up @@ -87,7 +88,7 @@ final class SampleListViewController: CollectionViewController {
switch Configuration.previewImplementation {
case .swiftUI:
previewViewController = UIHostingController(
rootView: AnimationPreviewView(animationName: animationPath))
rootView: AnimationPreviewView(animationSource: .local(animationPath: animationPath)))

case .uiKit:
previewViewController = AnimationPreviewViewController(animationPath)
Expand Down Expand Up @@ -124,6 +125,17 @@ 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",
Expand Down
8 changes: 8 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
0819D2A12A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
0819D2A22A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
0819D2A32A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */; };
0887346F28F0CBDE00458627 /* LottieAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887346E28F0CBDE00458627 /* LottieAnimation.swift */; };
0887347028F0CBDE00458627 /* LottieAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887346E28F0CBDE00458627 /* LottieAnimation.swift */; };
0887347128F0CBDE00458627 /* LottieAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0887346E28F0CBDE00458627 /* LottieAnimation.swift */; };
Expand Down Expand Up @@ -833,6 +836,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationSource.swift; sourceTree = "<group>"; };
0887346E28F0CBDE00458627 /* LottieAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimation.swift; sourceTree = "<group>"; };
0887347228F0CCDD00458627 /* LottieAnimationHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationHelpers.swift; sourceTree = "<group>"; };
0887347328F0CCDD00458627 /* LottieAnimationViewInitializers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimationViewInitializers.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1756,6 +1760,7 @@
2E9C95C72822F43100677516 /* Primitives */,
2E9C95CE2822F43100677516 /* Interpolatable */,
2E9C95D12822F43100677516 /* Helpers */,
0819D2A02A718CAE00D7DE49 /* LottieAnimationSource.swift */,
);
path = Utility;
sourceTree = "<group>";
Expand Down Expand Up @@ -2267,6 +2272,7 @@
2E9C96422822F43100677516 /* RootAnimationLayer.swift in Sources */,
2E9C97712822F43100677516 /* AnimationContext.swift in Sources */,
08C002052A46150D00AB54BA /* Archive+Progress.swift in Sources */,
0819D2A12A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */,
08C002F52A461D6A00AB54BA /* LottieView.swift in Sources */,
2E9C96B12822F43100677516 /* NodeProperty.swift in Sources */,
2E9C965D2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
Expand Down Expand Up @@ -2615,6 +2621,7 @@
2E9C95E92822F43100677516 /* Merge.swift in Sources */,
2E9C96042822F43100677516 /* ImageLayerModel.swift in Sources */,
19465F53282F998B00BB2C97 /* CachedImageProvider.swift in Sources */,
0819D2A22A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */,
08F8B20E2898A7B100CB5323 /* RepeaterLayer.swift in Sources */,
0887347928F0CCDD00458627 /* LottieAnimationViewInitializers.swift in Sources */,
2E9C96BB2822F43100677516 /* KeypathSearchable.swift in Sources */,
Expand Down Expand Up @@ -2832,6 +2839,7 @@
2E9C96442822F43100677516 /* RootAnimationLayer.swift in Sources */,
2E9C97732822F43200677516 /* AnimationContext.swift in Sources */,
08C002F62A461D6A00AB54BA /* LottieView.swift in Sources */,
0819D2A32A718CAE00D7DE49 /* LottieAnimationSource.swift in Sources */,
2E9C96B32822F43100677516 /* NodeProperty.swift in Sources */,
2E9C965F2822F43100677516 /* MainThreadAnimationLayer.swift in Sources */,
2E9C96502822F43100677516 /* SolidCompositionLayer.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions Sources/Private/Model/DotLottie/DotLottieImageProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,16 @@ class DotLottieImageProvider: AnimationImageProvider {
}

}

// MARK: Hashable

extension DotLottieImageProvider: Hashable {
static func ==(_ lhs: DotLottieImageProvider, _ rhs: DotLottieImageProvider) -> Bool {
lhs.filepath == rhs.filepath
}

func hash(into hasher: inout Hasher) {
hasher.combine(filepath)
}

}
17 changes: 17 additions & 0 deletions Sources/Private/Utility/LottieAnimationSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Created by Cal Stephens on 7/26/23.
// Copyright © 2023 Airbnb Inc. All rights reserved.

public enum LottieAnimationSource {
case lottieAnimation(LottieAnimation)
case dotLottieFile(DotLottieFile)

/// The animation displayed by this data source
var animation: LottieAnimation? {
switch self {
case .lottieAnimation(let animation):
return animation
case .dotLottieFile(let dotLottieFile):
return dotLottieFile.animation()?.animation
}
}
}
Loading

0 comments on commit 5f21ab0

Please sign in to comment.