Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SwiftUI APIs for controlling playback behavior using LottiePlaybackMode #2128

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 94 additions & 8 deletions Example/Example/AnimationPreviewView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,43 @@ struct AnimationPreviewView: View {
.imageProvider(.exampleAppSampleImages)
.resizable()
.reloadAnimationTrigger(currentURLIndex, showPlaceholder: false)
.looping()
.currentProgress(animationPlaying ? nil : sliderValue)
.playbackMode(playbackMode)
.getRealtimeAnimationProgress(animationPlaying ? $sliderValue : nil)

Spacer()

#if !os(tvOS)
Slider(value: $sliderValue, in: 0...1, onEditingChanged: { editing in
if animationPlaying, editing {
animationPlaying = false
HStack {
#if !os(tvOS)
Slider(value: $sliderValue, in: 0...1, onEditingChanged: { editing in
if animationPlaying, editing {
animationPlaying = false
}
})

Spacer(minLength: 16)
#endif

Button {
animationPlaying.toggle()
} label: {
if animationPlaying {
Image(systemName: "pause.fill")
} else {
Image(systemName: "play.fill")
}
}
})
}
.padding(.all, 16)
#endif
}
.navigationTitle(animationSource.name.components(separatedBy: "/").last!)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.secondaryBackground)
.onReceive(timer) { _ in
updateIndex()
}
.toolbar {
optionsMenu
}
}

// MARK: Private
Expand All @@ -85,6 +101,63 @@ struct AnimationPreviewView: View {
@State private var animationPlaying = true
@State private var sliderValue: AnimationProgressTime = 0
@State private var currentURLIndex: Int
@State private var renderingEngine: RenderingEngineOption = .automatic
@State private var loopMode: LottieLoopMode = .loop
@State private var playFromProgress: AnimationProgressTime = 0
@State private var playToProgress: AnimationProgressTime = 1

private var playbackMode: LottiePlaybackMode {
if animationPlaying {
return .fromProgress(playFromProgress, toProgress: playToProgress, loopMode: loopMode)
} else {
return .progress(sliderValue)
}
}

@ViewBuilder
private var optionsMenu: some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So cool

#if !os(tvOS)
Menu {
Menu {
option("Automatic", keyPath: \.renderingEngine, value: .automatic)
option("Core Animaiton", keyPath: \.renderingEngine, value: .coreAnimation)
option("Main Thread", keyPath: \.renderingEngine, value: .mainThread)
} label: {
Text("Rendering Engine")
}

Menu {
option("Play Once", keyPath: \.loopMode, value: .playOnce)
option("Loop", keyPath: \.loopMode, value: .loop)
option("Autoreverse", keyPath: \.loopMode, value: .autoReverse)
} label: {
Text("Loop Mode")
}

Menu {
option("0%", keyPath: \.playFromProgress, value: 0)
option("25%", keyPath: \.playFromProgress, value: 0.25)
option("50%", keyPath: \.playFromProgress, value: 0.5)
option("75%", keyPath: \.playFromProgress, value: 0.75)
option("100%", keyPath: \.playFromProgress, value: 1.0)
} label: {
Text("Play from...")
}

Menu {
option("0%", keyPath: \.playToProgress, value: 0)
option("25%", keyPath: \.playToProgress, value: 0.25)
option("50%", keyPath: \.playToProgress, value: 0.5)
option("75%", keyPath: \.playToProgress, value: 0.75)
option("100%", keyPath: \.playToProgress, value: 1.0)
} label: {
Text("Play to...")
}
} label: {
Image(systemName: "gear")
}
#endif
}

private func loadAnimation() async throws -> LottieAnimationSource? {
switch animationSource {
Expand All @@ -105,6 +178,19 @@ struct AnimationPreviewView: View {
let nextIndex = currentIndex == urls.index(before: urls.endIndex) ? urls.startIndex : currentIndex + 1
currentURLIndex = nextIndex
}

/// A `Button` that controls the value of the given keypath
private func option<T: Equatable>(_ label: String, keyPath: ReferenceWritableKeyPath<Self, T>, value: T) -> some View {
Button {
self[keyPath: keyPath] = value
} label: {
if self[keyPath: keyPath] == value {
Text("✔ \(label)")
} else {
Text(label)
}
}
}
}

extension Color {
Expand Down
8 changes: 8 additions & 0 deletions Lottie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
08C002F52A461D6A00AB54BA /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C002F42A461D6A00AB54BA /* LottieView.swift */; };
08C002F62A461D6A00AB54BA /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C002F42A461D6A00AB54BA /* LottieView.swift */; };
08CB2681291ED2B700B4F071 /* AnimationViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CB2680291ED2B700B4F071 /* AnimationViewTests.swift */; };
08CD109C2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD109B2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift */; };
08CD109D2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD109B2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift */; };
08CD109E2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CD109B2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift */; };
08E206DF2A56014E002DCE17 /* StyledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206AD2A56014E002DCE17 /* StyledView.swift */; };
08E206E02A56014E002DCE17 /* StyledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206AD2A56014E002DCE17 /* StyledView.swift */; };
08E206E12A56014E002DCE17 /* StyledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E206AD2A56014E002DCE17 /* StyledView.swift */; };
Expand Down Expand Up @@ -876,6 +879,7 @@
08C002F32A461A7300AB54BA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
08C002F42A461D6A00AB54BA /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = "<group>"; };
08CB2680291ED2B700B4F071 /* AnimationViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationViewTests.swift; sourceTree = "<group>"; };
08CD109B2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottiePlaybackMode.swift; sourceTree = "<group>"; };
08E206AD2A56014E002DCE17 /* StyledView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledView.swift; sourceTree = "<group>"; };
08E206AE2A56014E002DCE17 /* ViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewType.swift; sourceTree = "<group>"; };
08E206AF2A56014E002DCE17 /* ContentConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentConfigurableView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1887,6 +1891,7 @@
0887347428F0CCDD00458627 /* LottieAnimationView.swift */,
0887347328F0CCDD00458627 /* LottieAnimationViewInitializers.swift */,
08C002F42A461D6A00AB54BA /* LottieView.swift */,
08CD109B2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift */,
);
path = Animation;
sourceTree = "<group>";
Expand Down Expand Up @@ -2332,6 +2337,7 @@
2E9C95D32822F43100677516 /* Fill.swift in Sources */,
6DB3BDB8282454A6002A276D /* DictionaryInitializable.swift in Sources */,
08E207002A56014E002DCE17 /* UIViewConfiguringSwiftUIView.swift in Sources */,
08CD109C2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */,
2E9C96B72822F43100677516 /* NodePropertyMap.swift in Sources */,
2E9C97682822F43100677516 /* VectorsExtensions.swift in Sources */,
2E9C97232822F43100677516 /* RectangleAnimation.swift in Sources */,
Expand Down Expand Up @@ -2715,6 +2721,7 @@
2EAF5AF927A0798700E00531 /* FloatValueProvider.swift in Sources */,
2E9C968E2822F43100677516 /* PassThroughOutputNode.swift in Sources */,
08E207402A56014E002DCE17 /* SetBehaviorsProviding.swift in Sources */,
08CD109D2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */,
2EAF5AB727A0798700E00531 /* CompatibleAnimationKeypath.swift in Sources */,
2E9C96882822F43100677516 /* GroupOutputNode.swift in Sources */,
2E9C966A2822F43100677516 /* InvertedMatteLayer.swift in Sources */,
Expand Down Expand Up @@ -2905,6 +2912,7 @@
2E9C96B92822F43100677516 /* NodePropertyMap.swift in Sources */,
2E9C976A2822F43100677516 /* VectorsExtensions.swift in Sources */,
08E207022A56014E002DCE17 /* UIViewConfiguringSwiftUIView.swift in Sources */,
08CD109E2A7C2D9F0043A1A9 /* LottiePlaybackMode.swift in Sources */,
2E9C97252822F43100677516 /* RectangleAnimation.swift in Sources */,
2E450DAE283415D500E56D19 /* OpacityAnimation.swift in Sources */,
2E9C96FE2822F43100677516 /* CALayer+setupLayerHierarchy.swift in Sources */,
Expand Down
76 changes: 71 additions & 5 deletions Sources/Public/Animation/LottieAnimationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public class LottieAnimationLayer: CALayer {
open func play(completion: LottieCompletionBlock? = nil) {
guard let animation = animation else { return }

defer {
currentPlaybackMode = .toProgress(1, loopMode: loopMode)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
return
Expand Down Expand Up @@ -134,6 +138,10 @@ public class LottieAnimationLayer: CALayer {
{
guard let animation = animation else { return }

defer {
currentPlaybackMode = .fromProgress(fromProgress, toProgress: toProgress, loopMode: loopMode ?? self.loopMode)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
return
Expand Down Expand Up @@ -163,6 +171,10 @@ public class LottieAnimationLayer: CALayer {
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
defer {
currentPlaybackMode = .fromFrame(fromFrame, toFrame: toFrame, loopMode: loopMode ?? self.loopMode)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
return
Expand Down Expand Up @@ -203,6 +215,14 @@ public class LottieAnimationLayer: CALayer {
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
defer {
currentPlaybackMode = .fromMarker(
fromMarker,
toMarker: toMarker,
playEndMarkerFrame: playEndMarkerFrame,
loopMode: loopMode ?? self.loopMode)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
return
Expand Down Expand Up @@ -248,12 +268,16 @@ public class LottieAnimationLayer: CALayer {
loopMode: LottieLoopMode? = nil,
completion: LottieCompletionBlock? = nil)
{
if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
guard let from = animation?.markerMap?[marker] else {
return
}

guard let from = animation?.markerMap?[marker] else {
defer {
currentPlaybackMode = .marker(marker, loopMode: loopMode ?? self.loopMode)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: completion)
return
}

Expand All @@ -280,13 +304,17 @@ public class LottieAnimationLayer: CALayer {
///
/// - Parameter markers: The list of markers to play sequentially.
open func play(markers: [String]) {
guard !markers.isEmpty else { return }

defer {
currentPlaybackMode = .markers(markers)
}

if shouldOverrideWithReducedMotionAnimation {
playReducedMotionAnimation(completion: nil)
return
}

guard !markers.isEmpty else { return }

let markerToPlay = markers[0]
let followingMarkers = Array(markers.dropFirst())

Expand Down Expand Up @@ -320,8 +348,43 @@ public class LottieAnimationLayer: CALayer {
removeCurrentAnimation()
}

/// Applies the given `LottiePlaybackMode` to this layer.
open func play(_ playbackMode: LottiePlaybackMode) {
switch playbackMode {
case .progress(let progress):
currentProgress = progress

case .frame(let frame):
currentFrame = frame

case .time(let time):
currentTime = time

case .pause:
pause()

case .fromProgress(let fromProgress, let toProgress, let loopMode):
play(fromProgress: fromProgress, toProgress: toProgress, loopMode: loopMode, completion: nil)

case .fromFrame(let fromFrame, let toFrame, let loopMode):
play(fromFrame: fromFrame, toFrame: toFrame, loopMode: loopMode)

case .fromMarker(let fromMarker, let toMarker, let playEndMarkerFrame, let loopMode):
play(fromMarker: fromMarker, toMarker: toMarker, playEndMarkerFrame: playEndMarkerFrame, loopMode: loopMode)

case .marker(let marker, let loopMode):
play(marker: marker, loopMode: loopMode)

case .markers(let markers):
play(markers: markers)
}
}

// MARK: Public

/// The current `LottiePlaybackMode` that is being used
public private(set) var currentPlaybackMode: LottiePlaybackMode?

/// Value Providers that have been registered using `setValueProvider(_:keypath:)`
public private(set) var valueProviders = [AnimationKeypath: AnyValueProvider]()

Expand Down Expand Up @@ -503,6 +566,7 @@ public class LottieAnimationLayer: CALayer {
set {
if let animation = animation {
currentFrame = animation.frameTime(forProgress: newValue)
currentPlaybackMode = .progress(newValue)
} else {
currentFrame = 0
}
Expand All @@ -524,6 +588,7 @@ public class LottieAnimationLayer: CALayer {
set {
if let animation = animation {
currentFrame = animation.frameTime(forTime: newValue)
currentPlaybackMode = .time(newValue)
} else {
currentFrame = 0
}
Expand All @@ -544,6 +609,7 @@ public class LottieAnimationLayer: CALayer {
set {
removeCurrentAnimationIfNecessary()
updateAnimationFrame(newValue)
currentPlaybackMode = .frame(currentFrame)
}
get {
rootAnimationLayer?.currentFrame ?? 0
Expand Down
12 changes: 11 additions & 1 deletion Sources/Public/Animation/LottieAnimationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public enum LottieBackgroundBehavior {
// MARK: - LottieLoopMode

/// Defines animation loop behavior
public enum LottieLoopMode {
public enum LottieLoopMode: Hashable {
/// Animation is played once then stops.
case playOnce
/// Animation will loop from beginning to end until stopped.
Expand Down Expand Up @@ -337,6 +337,11 @@ open class LottieAnimationView: LottieAnimationViewBase {
lottieAnimationLayer.pause()
}

/// Applies the given `LottiePlaybackMode` to this layer.
open func play(_ playbackMode: LottiePlaybackMode) {
lottieAnimationLayer.play(playbackMode)
}

// MARK: Public

/// The configuration that this `LottieAnimationView` uses when playing its animation
Expand Down Expand Up @@ -552,6 +557,11 @@ open class LottieAnimationView: LottieAnimationViewBase {
lottieAnimationLayer.currentRenderingEngine
}

/// The current `LottiePlaybackMode` that is being used
public var currentPlaybackMode: LottiePlaybackMode? {
lottieAnimationLayer.currentPlaybackMode
}

/// Sets the lottie file backing the animation view. Setting this will clear the
/// view's contents, completion blocks and current state. The new animation will
/// be loaded up and set to the beginning of its timeline.
Expand Down
Loading