From 173b119ae867719cdfaec85bf0ae971848a38d26 Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Fri, 14 Jul 2023 10:24:35 -0700 Subject: [PATCH] Improve SwiftUI APIs for configuring value providers, image providers, etc (#2109) --- Example/Example.xcodeproj/project.pbxproj | 4 +- Example/Example/AnimationListView.swift | 1 + Example/Example/AnimationPreviewView.swift | 7 + .../Animation/LottieAnimationLayer.swift | 12 +- .../Animation/LottieAnimationView.swift | 3 +- Sources/Public/Animation/LottieView.swift | 126 +++++++++++++----- .../ValueProviders/ColorValueProvider.swift | 14 ++ .../ValueProviders/FloatValueProvider.swift | 13 ++ .../GradientValueProvider.swift | 13 ++ .../ValueProviders/PointValueProvider.swift | 12 ++ .../ValueProviders/SizeValueProvider.swift | 11 ++ .../FontProvider/AnimationFontProvider.swift | 8 ++ .../TextProvider/AnimationTextProvider.swift | 16 +++ Sources/Public/iOS/BundleImageProvider.swift | 7 + .../Public/iOS/FilepathImageProvider.swift | 6 + .../macOS/BundleImageProvider.macOS.swift | 7 + .../macOS/FilepathImageProvider.macOS.swift | 6 + 17 files changed, 228 insertions(+), 38 deletions(-) diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 6c3773e448..0d5997627e 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -430,7 +430,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = iOS/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -460,7 +460,7 @@ CODE_SIGN_STYLE = Automatic; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = iOS/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Example/Example/AnimationListView.swift b/Example/Example/AnimationListView.swift index 1a381a93b0..0ed26c6120 100644 --- a/Example/Example/AnimationListView.swift +++ b/Example/Example/AnimationListView.swift @@ -18,6 +18,7 @@ struct AnimationListView: View { case .animation(let animationName, _): HStack { LottieView(animation: .named(animationName, subdirectory: directory)) + .imageProvider(.exampleAppSampleImages) .frame(width: 50, height: 50) .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) diff --git a/Example/Example/AnimationPreviewView.swift b/Example/Example/AnimationPreviewView.swift index dd63830ad4..e6ce010798 100644 --- a/Example/Example/AnimationPreviewView.swift +++ b/Example/Example/AnimationPreviewView.swift @@ -14,6 +14,7 @@ struct AnimationPreviewView: View { var body: some View { VStack { LottieView(animation: .named(animationName)) + .imageProvider(.exampleAppSampleImages) .resizable() .looping() } @@ -33,3 +34,9 @@ extension Color { #endif } } + +extension AnimationImageProvider where Self == FilepathImageProvider { + static var exampleAppSampleImages: FilepathImageProvider { + FilepathImageProvider(filepath: Bundle.main.resourceURL!.appending(path: "Samples/Images")) + } +} diff --git a/Sources/Public/Animation/LottieAnimationLayer.swift b/Sources/Public/Animation/LottieAnimationLayer.swift index 720aaa081c..c13ca8193a 100644 --- a/Sources/Public/Animation/LottieAnimationLayer.swift +++ b/Sources/Public/Animation/LottieAnimationLayer.swift @@ -296,9 +296,6 @@ public class LottieAnimationLayer: CALayer { // MARK: Public - /// The configuration that this `LottieAnimationView` uses when playing its animation - public let configuration: LottieConfiguration - /// Value Providers that have been registered using `setValueProvider(_:keypath:)` public private(set) var valueProviders = [AnimationKeypath: AnyValueProvider]() @@ -306,6 +303,15 @@ public class LottieAnimationLayer: CALayer { /// Will inform the receiver the type of rendering engine that is used for the layer. public var animationLayerDidLoad:((_ animationLayer: LottieAnimationLayer, _ renderingEngine: RenderingEngineOption) -> Void)? + /// The configuration that this `LottieAnimationView` uses when playing its animation + public var configuration: LottieConfiguration { + didSet { + if configuration.renderingEngine != oldValue.renderingEngine { + makeAnimationLayer(usingEngine: configuration.renderingEngine) + } + } + } + /// The underlying CALayer created to display the content. /// Use this property to change CALayer props like the content's transform, anchor point, etc. public var animationLayer: CALayer? { rootAnimationLayer } diff --git a/Sources/Public/Animation/LottieAnimationView.swift b/Sources/Public/Animation/LottieAnimationView.swift index 31816ec6a2..59d7aaac79 100644 --- a/Sources/Public/Animation/LottieAnimationView.swift +++ b/Sources/Public/Animation/LottieAnimationView.swift @@ -304,7 +304,8 @@ open class LottieAnimationView: LottieAnimationViewBase { /// The configuration that this `LottieAnimationView` uses when playing its animation public var configuration: LottieConfiguration { - lottieAnimationLayer.configuration + get { lottieAnimationLayer.configuration } + set { lottieAnimationLayer.configuration = newValue } } /// Value Providers that have been registered using `setValueProvider(_:keypath:)` diff --git a/Sources/Public/Animation/LottieView.swift b/Sources/Public/Animation/LottieView.swift index e2b3a75273..e6deb9edf3 100644 --- a/Sources/Public/Animation/LottieView.swift +++ b/Sources/Public/Animation/LottieView.swift @@ -11,20 +11,9 @@ public struct LottieView: UIViewConfiguringSwiftUIView { // MARK: Lifecycle - public init( - animation: LottieAnimation?, - imageProvider: AnimationImageProvider? = nil, - textProvider: AnimationTextProvider? = nil, - fontProvider: AnimationFontProvider? = nil, - configuration: LottieConfiguration = .shared, - accessibilityLabel: String? = nil) - { + /// Creates a `LottieView` that displays the given animation + public init(animation: LottieAnimation?) { self.animation = animation - self.imageProvider = imageProvider - self.textProvider = textProvider - self.fontProvider = fontProvider - self.configuration = configuration - self.accessibilityLabel = accessibilityLabel } // MARK: Public @@ -34,30 +23,18 @@ public struct LottieView: UIViewConfiguringSwiftUIView { LottieAnimationView( animation: animation, imageProvider: imageProvider, - textProvider: textProvider ?? DefaultTextProvider(), - fontProvider: fontProvider ?? DefaultFontProvider(), + textProvider: textProvider, + fontProvider: fontProvider, configuration: configuration) } .sizing(sizing) .configure { context in - #if os(macOS) - context.view.setAccessibilityElement(accessibilityLabel != nil) - context.view.setAccessibilityLabel(accessibilityLabel) - #else - context.view.isAccessibilityElement = accessibilityLabel != nil - context.view.accessibilityLabel = accessibilityLabel - #endif - // We check referential equality of the animation before updating as updating the // animation has a side-effect of rebuilding the animation layer, and it would be // prohibitive to do so on every state update. if animation !== context.view.animation { context.view.animation = animation } - - // Technically the image provider, text provider, font provider, and Lottie configuration - // could also need to be updated here, but there's no performant way to check their equality, - // so we assume they are not. } .configurations(configurations) } @@ -99,17 +76,102 @@ public struct LottieView: UIViewConfiguringSwiftUIView { } } + /// Returns a copy of this view with its accessibility label updated to the given value. + public func accessibilityLabel(_ accessibilityLabel: String?) -> Self { + configure { view in + #if os(macOS) + view.setAccessibilityElement(accessibilityLabel != nil) + view.setAccessibilityLabel(accessibilityLabel) + #else + view.isAccessibilityElement = accessibilityLabel != nil + view.accessibilityLabel = accessibilityLabel + #endif + } + } + + /// Returns a copt of this view with its `LottieConfiguration` updated to the given value. + public func configuration(_ configuration: LottieConfiguration) -> Self { + var copy = self + copy.configuration = configuration + + copy = copy.configure { view in + if view.configuration != configuration { + view.configuration = configuration + } + } + + return copy + } + + /// Returns a copy of this view with its image provider updated to the given value. + /// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders. + public func imageProvider(_ imageProvider: ImageProvider) -> Self { + var copy = self + copy.imageProvider = imageProvider + + copy = copy.configure { view in + if (view.imageProvider as? ImageProvider) != imageProvider { + view.imageProvider = imageProvider + } + } + + return copy + } + + /// Returns a copy of this view with its text provider updated to the given value. + /// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders. + public func textProvider(_ textProvider: TextProvider) -> Self { + var copy = self + copy.textProvider = textProvider + + copy = copy.configure { view in + if (view.textProvider as? TextProvider) != textProvider { + view.textProvider = textProvider + } + } + + return copy + } + + /// Returns a copy of this view with its image provider updated to the given value. + /// The image provider must be `Equatable` to avoid unnecessary state updates / re-renders. + public func fontProvider(_ fontProvider: FontProvider) -> Self { + var copy = self + copy.fontProvider = fontProvider + + copy = configure { view in + if (view.fontProvider as? FontProvider) != fontProvider { + view.fontProvider = fontProvider + } + } + + return copy + } + + /// Returns a copy of this view using the given value provider for the given keypath. + /// The value provider must be `Equatable` to avoid unnecessary state updates / re-renders. + public func valueProvider( + _ valueProvider: ValueProvider, + for keypath: AnimationKeypath) + -> Self + { + configure { view in + if (view.valueProviders[keypath] as? ValueProvider) != valueProvider { + view.setValueProvider(valueProvider, keypath: keypath) + } + } + } + // MARK: Internal var configurations = [SwiftUIView.Configuration]() // MARK: Private - private let accessibilityLabel: String? private let animation: LottieAnimation? - private let imageProvider: Lottie.AnimationImageProvider? - private let textProvider: Lottie.AnimationTextProvider? - private let fontProvider: Lottie.AnimationFontProvider? - private let configuration: LottieConfiguration + private var imageProvider: AnimationImageProvider? + private var textProvider: AnimationTextProvider = DefaultTextProvider() + private var fontProvider: AnimationFontProvider = DefaultFontProvider() + private var configuration: LottieConfiguration = .shared private var sizing = SwiftUIMeasurementContainerStrategy.automatic } diff --git a/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift b/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift index a9271c10f3..3e7e182852 100644 --- a/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift +++ b/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift @@ -8,6 +8,8 @@ import CoreGraphics import Foundation +// MARK: - ColorValueProvider + /// A `ValueProvider` that returns a CGColor Value public final class ColorValueProvider: ValueProvider { @@ -18,6 +20,7 @@ public final class ColorValueProvider: ValueProvider { self.block = block color = LottieColor(r: 0, g: 0, b: 0, a: 1) keyframes = nil + identity = UUID() } /// Initializes with a single color. @@ -26,6 +29,7 @@ public final class ColorValueProvider: ValueProvider { block = nil keyframes = nil hasUpdate = true + identity = color } /// Initializes with multiple colors, with timing information @@ -34,6 +38,7 @@ public final class ColorValueProvider: ValueProvider { color = LottieColor(r: 0, g: 0, b: 0, a: 1) block = nil hasUpdate = true + identity = keyframes } // MARK: Public @@ -81,4 +86,13 @@ public final class ColorValueProvider: ValueProvider { private var block: ColorValueBlock? private var keyframes: [Keyframe]? + private var identity: AnyHashable +} + +// MARK: Equatable + +extension ColorValueProvider: Equatable { + public static func ==(_ lhs: ColorValueProvider, _ rhs: ColorValueProvider) -> Bool { + lhs.identity == rhs.identity + } } diff --git a/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift b/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift index b8f7c44b2e..d00cb83842 100644 --- a/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift +++ b/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift @@ -8,6 +8,8 @@ import CoreGraphics import Foundation +// MARK: - FloatValueProvider + /// A `ValueProvider` that returns a CGFloat Value public final class FloatValueProvider: ValueProvider { @@ -17,6 +19,7 @@ public final class FloatValueProvider: ValueProvider { public init(block: @escaping CGFloatValueBlock) { self.block = block float = 0 + identity = UUID() } /// Initializes with a single float. @@ -24,6 +27,7 @@ public final class FloatValueProvider: ValueProvider { self.float = float block = nil hasUpdate = true + identity = float } // MARK: Public @@ -67,4 +71,13 @@ public final class FloatValueProvider: ValueProvider { private var hasUpdate = true private var block: CGFloatValueBlock? + private var identity: AnyHashable +} + +// MARK: Equatable + +extension FloatValueProvider: Equatable { + public static func ==(_ lhs: FloatValueProvider, _ rhs: FloatValueProvider) -> Bool { + lhs.identity == rhs.identity + } } diff --git a/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift b/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift index fc4bf6d44a..b200129cc4 100644 --- a/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +++ b/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift @@ -8,6 +8,8 @@ import CoreGraphics import Foundation +// MARK: - GradientValueProvider + /// A `ValueProvider` that returns a Gradient Color Value. public final class GradientValueProvider: ValueProvider { @@ -22,6 +24,7 @@ public final class GradientValueProvider: ValueProvider { locationsBlock = locations colors = [] self.locations = [] + identity = UUID() } /// Initializes with an array of colors. @@ -31,6 +34,7 @@ public final class GradientValueProvider: ValueProvider { { self.colors = colors self.locations = locations + identity = [AnyHashable(colors), AnyHashable(locations)] updateValueArray() hasUpdate = true } @@ -93,6 +97,8 @@ public final class GradientValueProvider: ValueProvider { private var locationsBlock: ColorLocationsBlock? private var value: [Double] = [] + private let identity: AnyHashable + private func value(from colors: [LottieColor], locations: [Double]) -> [Double] { var colorValues = [Double]() var alphaValues = [Double]() @@ -120,4 +126,11 @@ public final class GradientValueProvider: ValueProvider { private func updateValueArray() { value = value(from: colors, locations: locations) } + +} + +extension GradientValueProvider { + public static func ==(_ lhs: GradientValueProvider, _ rhs: GradientValueProvider) -> Bool { + lhs.identity == rhs.identity + } } diff --git a/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift b/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift index 8dd177a378..cfafa6c03f 100644 --- a/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift +++ b/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift @@ -7,6 +7,9 @@ import CoreGraphics import Foundation + +// MARK: - PointValueProvider + /// A `ValueProvider` that returns a CGPoint Value public final class PointValueProvider: ValueProvider { @@ -16,6 +19,7 @@ public final class PointValueProvider: ValueProvider { public init(block: @escaping PointValueBlock) { self.block = block point = .zero + identity = UUID() } /// Initializes with a single point. @@ -23,6 +27,7 @@ public final class PointValueProvider: ValueProvider { self.point = point block = nil hasUpdate = true + identity = [point.x, point.y] } // MARK: Public @@ -66,4 +71,11 @@ public final class PointValueProvider: ValueProvider { private var hasUpdate = true private var block: PointValueBlock? + private let identity: AnyHashable +} + +extension PointValueProvider { + public static func ==(_ lhs: PointValueProvider, _ rhs: PointValueProvider) -> Bool { + lhs.identity == rhs.identity + } } diff --git a/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift b/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift index f62e7ed204..242238e1a0 100644 --- a/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift +++ b/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift @@ -8,6 +8,8 @@ import CoreGraphics import Foundation +// MARK: - SizeValueProvider + /// A `ValueProvider` that returns a CGSize Value public final class SizeValueProvider: ValueProvider { @@ -17,6 +19,7 @@ public final class SizeValueProvider: ValueProvider { public init(block: @escaping SizeValueBlock) { self.block = block size = .zero + identity = UUID() } /// Initializes with a single size. @@ -24,6 +27,7 @@ public final class SizeValueProvider: ValueProvider { self.size = size block = nil hasUpdate = true + identity = [size.width, size.height] } // MARK: Public @@ -67,4 +71,11 @@ public final class SizeValueProvider: ValueProvider { private var hasUpdate = true private var block: SizeValueBlock? + private let identity: AnyHashable +} + +extension SizeValueProvider { + public static func ==(_ lhs: SizeValueProvider, _ rhs: SizeValueProvider) -> Bool { + lhs.identity == rhs.identity + } } diff --git a/Sources/Public/FontProvider/AnimationFontProvider.swift b/Sources/Public/FontProvider/AnimationFontProvider.swift index 3731a3f0c6..a0b07c2a1e 100644 --- a/Sources/Public/FontProvider/AnimationFontProvider.swift +++ b/Sources/Public/FontProvider/AnimationFontProvider.swift @@ -33,3 +33,11 @@ public final class DefaultFontProvider: AnimationFontProvider { CTFontCreateWithName(family as CFString, size, nil) } } + +// MARK: Equatable + +extension DefaultFontProvider: Equatable { + public static func ==(_: DefaultFontProvider, _: DefaultFontProvider) -> Bool { + true + } +} diff --git a/Sources/Public/TextProvider/AnimationTextProvider.swift b/Sources/Public/TextProvider/AnimationTextProvider.swift index 4bbbe3d927..8ca99bc69f 100644 --- a/Sources/Public/TextProvider/AnimationTextProvider.swift +++ b/Sources/Public/TextProvider/AnimationTextProvider.swift @@ -36,6 +36,14 @@ public final class DictionaryTextProvider: AnimationTextProvider { let values: [String: String] } +// MARK: Equatable + +extension DictionaryTextProvider: Equatable { + public static func ==(_ lhs: DictionaryTextProvider, _ rhs: DictionaryTextProvider) -> Bool { + lhs.values == rhs.values + } +} + // MARK: - DefaultTextProvider /// Default text provider. Uses text in the animation file @@ -51,3 +59,11 @@ public final class DefaultTextProvider: AnimationTextProvider { sourceText } } + +// MARK: Equatable + +extension DefaultTextProvider: Equatable { + public static func ==(_: DefaultTextProvider, _: DefaultTextProvider) -> Bool { + true + } +} diff --git a/Sources/Public/iOS/BundleImageProvider.swift b/Sources/Public/iOS/BundleImageProvider.swift index b452ca91dd..6c35d72b8f 100644 --- a/Sources/Public/iOS/BundleImageProvider.swift +++ b/Sources/Public/iOS/BundleImageProvider.swift @@ -86,4 +86,11 @@ public class BundleImageProvider: AnimationImageProvider { let bundle: Bundle let searchPath: String? } + +extension BundleImageProvider: Equatable { + public static func ==(_ lhs: BundleImageProvider, _ rhs: BundleImageProvider) -> Bool { + lhs.bundle == rhs.bundle + && lhs.searchPath == rhs.searchPath + } +} #endif diff --git a/Sources/Public/iOS/FilepathImageProvider.swift b/Sources/Public/iOS/FilepathImageProvider.swift index f4d2779286..e0206683ce 100644 --- a/Sources/Public/iOS/FilepathImageProvider.swift +++ b/Sources/Public/iOS/FilepathImageProvider.swift @@ -56,4 +56,10 @@ public class FilepathImageProvider: AnimationImageProvider { let filepath: URL } + +extension FilepathImageProvider: Equatable { + public static func ==(_ lhs: FilepathImageProvider, _ rhs: FilepathImageProvider) -> Bool { + lhs.filepath == rhs.filepath + } +} #endif diff --git a/Sources/Public/macOS/BundleImageProvider.macOS.swift b/Sources/Public/macOS/BundleImageProvider.macOS.swift index 33416ebadf..cdd7a3dd56 100644 --- a/Sources/Public/macOS/BundleImageProvider.macOS.swift +++ b/Sources/Public/macOS/BundleImageProvider.macOS.swift @@ -76,4 +76,11 @@ public class BundleImageProvider: AnimationImageProvider { let searchPath: String? } +extension BundleImageProvider: Equatable { + public static func ==(_ lhs: BundleImageProvider, _ rhs: BundleImageProvider) -> Bool { + lhs.bundle == rhs.bundle + && lhs.searchPath == rhs.searchPath + } +} + #endif diff --git a/Sources/Public/macOS/FilepathImageProvider.macOS.swift b/Sources/Public/macOS/FilepathImageProvider.macOS.swift index 45acf31a2b..eed2a13c26 100644 --- a/Sources/Public/macOS/FilepathImageProvider.macOS.swift +++ b/Sources/Public/macOS/FilepathImageProvider.macOS.swift @@ -56,6 +56,12 @@ public class FilepathImageProvider: AnimationImageProvider { let filepath: URL } +extension FilepathImageProvider: Equatable { + public static func ==(_ lhs: FilepathImageProvider, _ rhs: FilepathImageProvider) -> Bool { + lhs.filepath == rhs.filepath + } +} + extension NSImage { @nonobjc var lottie_CGImage: CGImage? {