Runtime modification of animation properties through keypaths and value providers, enabling dynamic color changes, transforms, and other property animations without recreating the animation.
Keypath system for targeting specific elements and properties within Lottie animations using hierarchical dot notation.
/**
* Keypath for targeting animation properties
* Represents hierarchical path to animation elements
*/
public struct AnimationKeypath: Hashable, ExpressibleByStringLiteral {
/** Array of keypath components */
public let keys: [String]
/** Dot-separated string representation */
public let string: String
/**
* Initialize from dot-separated keypath string
* @param keypath - Dot-separated path like "layer.transform.position"
*/
public init(keypath: String)
/**
* Initialize from array of keypath components
* @param keys - Array of keypath components
*/
public init(keys: [String])
/**
* Initialize from string literal (supports keypath literals)
* @param value - String literal value
*/
public init(stringLiteral value: String)
/**
* Initialize from string interpolation
*/
public init(stringInterpolation: DefaultStringInterpolation)
/**
* Initialize from Unicode scalar literal
*/
public init(unicodeScalarLiteral value: String)
/**
* Initialize from extended grapheme cluster literal
*/
public init(extendedGraphemeClusterLiteral value: String)
}Usage Examples:
import Lottie
class KeypathExamples {
let animationView = LottieAnimationView()
func demonstrateKeypaths() {
// Different keypath initialization methods
let keypath1 = AnimationKeypath(keypath: "layer1.fill.color")
let keypath2 = AnimationKeypath(keys: ["layer1", "fill", "color"])
let keypath3: AnimationKeypath = "layer1.fill.color" // String literal
// Common keypath patterns
let backgroundKeypath = AnimationKeypath(keypath: "background.fill.color")
let iconKeypath = AnimationKeypath(keypath: "icon.transform.position")
let textKeypath = AnimationKeypath(keypath: "text.text")
// Wildcards for targeting multiple similar elements
let allColors = AnimationKeypath(keypath: "*.fill.color")
let allShapes = AnimationKeypath(keypath: "shape_*")
// Discover available keypaths
animationView.logHierarchyKeypaths()
let allKeypaths = animationView.allHierarchyKeypaths()
print("Available keypaths: \(allKeypaths)")
}
}Protocol-based system for providing dynamic values to animation properties with support for different value types and update patterns.
/**
* Protocol for providing dynamic values to animations
* Base protocol for all value provider types
*/
public protocol AnyValueProvider {
/** Type of values this provider supplies */
var valueType: Any.Type { get }
/** Type-erased storage for the provider */
var typeErasedStorage: AnyValueProviderStorage { get }
/**
* Check if provider has updates at specific frame
* @param frame - Frame number to check
* @returns Whether provider has new value at this frame
*/
func hasUpdate(frame: AnimationFrameTime) -> Bool
}
/**
* Storage mechanism for value providers
* Supports different value delivery patterns
*/
public protocol AnyValueProviderStorage {
/** Type of values stored */
var valueType: Any.Type { get }
/**
* Get value at specific frame
* @param frame - Frame number to get value for
* @returns Value at specified frame
*/
func value(frame: AnimationFrameTime) -> Any
/**
* Check if storage has updates at frame
* @param frame - Frame number to check
* @returns Whether storage has new value
*/
func hasUpdate(frame: AnimationFrameTime) -> Bool
}Storage patterns for different types of dynamic value delivery including single values, keyframes, and closures.
/**
* Storage patterns for value providers
* Supports different value delivery mechanisms
*/
public enum ValueProviderStorage<T>: AnyValueProviderStorage {
/** Single constant value for all frames */
case singleValue(T)
/** Keyframe-based values with interpolation */
case keyframes([Keyframe<T>])
/** Closure-based values computed per frame */
case closure((AnimationFrameTime) -> T)
// MARK: - AnyValueProviderStorage Implementation
public var valueType: Any.Type { T.self }
/**
* Get value for specific frame based on storage type
* @param frame - Frame number to evaluate
* @returns Value for the specified frame
*/
public func value(frame: AnimationFrameTime) -> Any {
switch self {
case .singleValue(let value):
return value
case .keyframes(let keyframes):
return interpolatedValue(from: keyframes, at: frame)
case .closure(let closure):
return closure(frame)
}
}
/**
* Check if value changes at frame
* @param frame - Frame to check
* @returns Whether value is different from previous frame
*/
public func hasUpdate(frame: AnimationFrameTime) -> Bool {
switch self {
case .singleValue:
return frame == 0 // Only update on first frame
case .keyframes(let keyframes):
return keyframes.contains { $0.time == frame }
case .closure:
return true // Always update for closures
}
}
}Specialized value provider for dynamic color modification supporting various input formats and interpolation.
/**
* Value provider for LottieColor values
* Supports dynamic color modification at runtime
*/
public final class ColorValueProvider: AnyValueProvider {
public let valueType: Any.Type = LottieColor.self
public let typeErasedStorage: AnyValueProviderStorage
/**
* Initialize with closure-based color computation
* @param block - Closure computing color for each frame
*/
public init(block: @escaping (AnimationFrameTime) -> LottieColor)
/**
* Initialize with single constant color
* @param color - Color value for all frames
*/
public init(_ color: LottieColor)
/**
* Initialize with keyframe animation
* @param keyframes - Array of color keyframes with timing
*/
public init(_ keyframes: [Keyframe<LottieColor>])
/**
* Check if provider has updates at frame
* @param frame - Frame to check for updates
* @returns Whether color changes at this frame
*/
public func hasUpdate(frame: AnimationFrameTime) -> Bool
}Usage Examples:
import Lottie
class ColorValueProviderExamples {
let animationView = LottieAnimationView()
func setupDynamicColors() {
// Single constant color
let redProvider = ColorValueProvider(LottieColor(r: 1.0, g: 0.0, b: 0.0, a: 1.0))
animationView.setValueProvider(redProvider, keypath: "icon.fill.color")
// Frame-based color computation
let gradientProvider = ColorValueProvider { frame in
let progress = frame / 120.0 // Assuming 120 frame animation
return LottieColor(
r: Double(progress),
g: 0.5,
b: 1.0 - Double(progress),
a: 1.0
)
}
animationView.setValueProvider(gradientProvider, keypath: "background.fill.color")
// Keyframe-based color animation
let keyframeProvider = ColorValueProvider([
Keyframe(value: LottieColor(r: 1.0, g: 0.0, b: 0.0, a: 1.0), time: 0),
Keyframe(value: LottieColor(r: 0.0, g: 1.0, b: 0.0, a: 1.0), time: 60),
Keyframe(value: LottieColor(r: 0.0, g: 0.0, b: 1.0, a: 1.0), time: 120)
])
animationView.setValueProvider(keyframeProvider, keypath: "accent.fill.color")
// Interactive color based on user input
setupInteractiveColor()
}
func setupInteractiveColor() {
let interactiveProvider = ColorValueProvider { [weak self] frame in
// Get color based on current user interaction state
return self?.getCurrentUserColor() ?? LottieColor(r: 0.5, g: 0.5, b: 0.5, a: 1.0)
}
animationView.setValueProvider(interactiveProvider, keypath: "interactive.fill.color")
}
func getCurrentUserColor() -> LottieColor {
// Implementation would determine color based on UI state
return LottieColor(r: 0.2, g: 0.8, b: 0.6, a: 1.0)
}
}Provider for single floating-point values such as opacity, scale factors, and rotation angles.
/**
* Value provider for Float/CGFloat values
* Used for opacity, scale, rotation, and other numeric properties
*/
public final class FloatValueProvider: AnyValueProvider {
public let valueType: Any.Type = CGFloat.self
public let typeErasedStorage: AnyValueProviderStorage
/**
* Initialize with closure-based float computation
* @param block - Closure computing float value for each frame
*/
public init(block: @escaping (AnimationFrameTime) -> CGFloat)
/**
* Initialize with single constant float
* @param float - Float value for all frames
*/
public init(_ float: CGFloat)
/**
* Initialize with keyframe animation
* @param keyframes - Array of float keyframes with timing
*/
public init(_ keyframes: [Keyframe<CGFloat>])
public func hasUpdate(frame: AnimationFrameTime) -> Bool
}Provider for 2D point values such as position and anchor point modifications.
/**
* Value provider for CGPoint values
* Used for position, anchor point, and other 2D coordinate properties
*/
public final class PointValueProvider: AnyValueProvider {
public let valueType: Any.Type = CGPoint.self
public let typeErasedStorage: AnyValueProviderStorage
/**
* Initialize with closure-based point computation
* @param block - Closure computing point value for each frame
*/
public init(block: @escaping (AnimationFrameTime) -> CGPoint)
/**
* Initialize with single constant point
* @param point - Point value for all frames
*/
public init(_ point: CGPoint)
/**
* Initialize with keyframe animation
* @param keyframes - Array of point keyframes with timing
*/
public init(_ keyframes: [Keyframe<CGPoint>])
public func hasUpdate(frame: AnimationFrameTime) -> Bool
}Provider for size values used in scale transforms and dimensional modifications.
/**
* Value provider for CGSize values
* Used for scale transforms and dimensional modifications
*/
public final class SizeValueProvider: AnyValueProvider {
public let valueType: Any.Type = CGSize.self
public let typeErasedStorage: AnyValueProviderStorage
/**
* Initialize with closure-based size computation
* @param block - Closure computing size value for each frame
*/
public init(block: @escaping (AnimationFrameTime) -> CGSize)
/**
* Initialize with single constant size
* @param size - Size value for all frames
*/
public init(_ size: CGSize)
/**
* Initialize with keyframe animation
* @param keyframes - Array of size keyframes with timing
*/
public init(_ keyframes: [Keyframe<CGSize>])
public func hasUpdate(frame: AnimationFrameTime) -> Bool
}Provider for gradient values supporting complex gradient animations and modifications.
/**
* Value provider for gradient values
* Supports dynamic gradient modifications including colors and stops
*/
public final class GradientValueProvider: AnyValueProvider {
public let valueType: Any.Type = [Double].self
public let typeErasedStorage: AnyValueProviderStorage
/**
* Initialize with closure-based gradient computation
* @param block - Closure computing gradient array for each frame
*/
public init(block: @escaping (AnimationFrameTime) -> [Double])
/**
* Initialize with single constant gradient
* @param gradient - Gradient array for all frames
*/
public init(_ gradient: [Double])
/**
* Initialize with keyframe animation
* @param keyframes - Array of gradient keyframes with timing
*/
public init(_ keyframes: [Keyframe<[Double]>])
public func hasUpdate(frame: AnimationFrameTime) -> Bool
}Methods for applying, removing, and querying value providers on animation views.
/**
* Dynamic property management methods
* Available on all animation view types
*/
extension LottieAnimationView {
/**
* Set value provider for specific keypath
* @param valueProvider - Provider supplying dynamic values
* @param keypath - Target keypath for the provider
*/
public func setValueProvider<ValueProvider: AnyValueProvider>(
_ valueProvider: ValueProvider,
keypath: AnimationKeypath
)
/**
* Remove value provider for keypath
* @param keypath - Target keypath to remove provider from
*/
public func removeValueProvider(for keypath: AnimationKeypath)
/**
* Get current value for keypath at specific frame
* @param keypath - Target keypath to query
* @param atFrame - Frame number to get value for
* @returns Current value including any applied providers
*/
public func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime) -> Any?
/**
* Get original animation value for keypath at specific frame
* @param keypath - Target keypath to query
* @param atFrame - Frame number to get value for
* @returns Original animation value ignoring any providers
*/
public func getOriginalValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime) -> Any?
}Usage Examples:
import Lottie
class DynamicPropertyManager {
let animationView = LottieAnimationView()
func setupDynamicProperties() {
// Set up various property providers
setupPositionAnimation()
setupScaleAnimation()
setupOpacityControl()
setupGradientAnimation()
// Query current values
queryPropertyValues()
}
func setupPositionAnimation() {
// Circular motion for an icon
let positionProvider = PointValueProvider { frame in
let angle = (frame / 60.0) * .pi * 2 // Full rotation every 60 frames
let radius: CGFloat = 50
let centerX: CGFloat = 100
let centerY: CGFloat = 100
return CGPoint(
x: centerX + radius * cos(angle),
y: centerY + radius * sin(angle)
)
}
animationView.setValueProvider(positionProvider, keypath: "icon.transform.position")
}
func setupScaleAnimation() {
// Pulsing scale effect
let scaleProvider = SizeValueProvider { frame in
let pulse = sin((frame / 30.0) * .pi) * 0.2 + 1.0 // Pulse between 0.8 and 1.2
return CGSize(width: pulse, height: pulse)
}
animationView.setValueProvider(scaleProvider, keypath: "heart.transform.scale")
}
func setupOpacityControl() {
// Interactive opacity based on user interaction
let opacityProvider = FloatValueProvider { [weak self] frame in
return self?.getCurrentOpacity() ?? 1.0
}
animationView.setValueProvider(opacityProvider, keypath: "overlay.transform.opacity")
}
func setupGradientAnimation() {
// Animated gradient colors
let gradientProvider = GradientValueProvider { frame in
let progress = (frame / 120.0).truncatingRemainder(dividingBy: 1.0)
// RGBA values for gradient stops
return [
Double(progress), 0.5, 1.0 - Double(progress), 1.0, // First color
1.0 - Double(progress), Double(progress), 0.5, 1.0 // Second color
]
}
animationView.setValueProvider(gradientProvider, keypath: "background.gradient.colors")
}
func queryPropertyValues() {
let currentFrame = animationView.currentFrame
// Get current modified values
if let currentPosition = animationView.getValue(
for: AnimationKeypath("icon.transform.position"),
atFrame: currentFrame
) as? CGPoint {
print("Current icon position: \(currentPosition)")
}
// Get original animation values
if let originalPosition = animationView.getOriginalValue(
for: AnimationKeypath("icon.transform.position"),
atFrame: currentFrame
) as? CGPoint {
print("Original icon position: \(originalPosition)")
}
}
func removeProviders() {
// Remove specific providers
animationView.removeValueProvider(for: AnimationKeypath("icon.transform.position"))
animationView.removeValueProvider(for: AnimationKeypath("heart.transform.scale"))
// Animation will revert to original values
}
func getCurrentOpacity() -> CGFloat {
// Implementation would return current UI state-based opacity
return 0.8
}
}Complex scenarios for coordinating multiple dynamic properties and creating sophisticated effects.
/**
* Advanced dynamic property coordination
*/
class AdvancedDynamicProperties {
let animationView = LottieAnimationView()
func setupCoordinatedAnimation() {
// Coordinate multiple properties for complex effect
let baseKeypath = "particle_system"
// Create coordinated providers that work together
setupParticleSystem(baseKeypath: baseKeypath)
setupPhysicsBasedMovement()
setupDataDrivenVisualization()
}
func setupParticleSystem(baseKeypath: String) {
// Create multiple particles with coordinated movement
for i in 0..<10 {
let particleKeypath = "\(baseKeypath).particle_\(i)"
let positionProvider = PointValueProvider { frame in
let time = frame / 60.0
let phase = Double(i) * 0.628 // Different phase for each particle
return CGPoint(
x: 150 + 100 * cos(time + phase),
y: 150 + 100 * sin(time + phase)
)
}
let scaleProvider = FloatValueProvider { frame in
let time = frame / 60.0
let phase = Double(i) * 0.628
return 0.5 + 0.5 * sin(time * 2 + phase)
}
animationView.setValueProvider(positionProvider, keypath: AnimationKeypath("\(particleKeypath).position"))
animationView.setValueProvider(scaleProvider, keypath: AnimationKeypath("\(particleKeypath).scale"))
}
}
func setupPhysicsBasedMovement() {
// Simulate physics-based movement with dynamic properties
var velocity: CGPoint = CGPoint(x: 2, y: 1)
var position: CGPoint = CGPoint(x: 150, y: 150)
let bounds = CGRect(x: 50, y: 50, width: 200, height: 200)
let physicsProvider = PointValueProvider { frame in
// Simple physics simulation
position.x += velocity.x
position.y += velocity.y
// Bounce off boundaries
if position.x <= bounds.minX || position.x >= bounds.maxX {
velocity.x *= -1
}
if position.y <= bounds.minY || position.y >= bounds.maxY {
velocity.y *= -1
}
return position
}
animationView.setValueProvider(physicsProvider, keypath: "bouncing_ball.position")
}
func setupDataDrivenVisualization() {
// Use external data to drive animation properties
let dataProvider = ColorValueProvider { [weak self] frame in
guard let self = self else { return LottieColor(r: 0.5, g: 0.5, b: 0.5, a: 1.0) }
// Get current data value (could be from network, sensors, etc.)
let dataValue = self.getCurrentDataValue()
// Map data to color
return self.mapDataToColor(dataValue)
}
animationView.setValueProvider(dataProvider, keypath: "data_visualization.color")
}
func getCurrentDataValue() -> Double {
// Implementation would fetch real-time data
return Double.random(in: 0...1)
}
func mapDataToColor(_ value: Double) -> LottieColor {
// Map data value to color spectrum
if value < 0.33 {
return LottieColor(r: 1.0, g: 0.0, b: 0.0, a: 1.0) // Red for low values
} else if value < 0.66 {
return LottieColor(r: 1.0, g: 1.0, b: 0.0, a: 1.0) // Yellow for medium values
} else {
return LottieColor(r: 0.0, g: 1.0, b: 0.0, a: 1.0) // Green for high values
}
}
}