lottie-ios 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/.github/workflows/main.yml +117 -11
  2. package/Gemfile +1 -0
  3. package/Gemfile.lock +5 -0
  4. package/Lottie.xcodeproj/project.pbxproj +1546 -194
  5. package/Lottie.xcodeproj/project.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  6. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (iOS).xcscheme +1 -1
  7. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (macOS).xcscheme +1 -1
  8. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (tvOS).xcscheme +4 -5
  9. package/Lottie.xcodeproj/xcshareddata/xcschemes/Lottie (visionOS).xcscheme +66 -0
  10. package/Lottie.xcodeproj/xcuserdata/calstephens.xcuserdatad/xcschemes/xcschememanagement.plist +18 -0
  11. package/Lottie.xcworkspace/xcshareddata/swiftpm/Package.resolved +2 -11
  12. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  13. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +374 -116
  14. package/Lottie.xcworkspace/xcuserdata/calstephens.xcuserdatad/xcdebugger/Expressions.xcexplist +16 -5
  15. package/Package.resolved +2 -2
  16. package/Package.swift +16 -9
  17. package/README.md +2 -2
  18. package/Rakefile +122 -25
  19. package/Sources/Private/CoreAnimation/Animations/CALayer+addAnimation.swift +3 -4
  20. package/Sources/Private/CoreAnimation/Animations/DropShadowAnimation.swift +160 -0
  21. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +83 -10
  22. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +85 -79
  23. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +11 -7
  24. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +67 -6
  25. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +21 -2
  26. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +9 -0
  27. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +1 -0
  28. package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +1 -1
  29. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +2 -0
  30. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +11 -13
  31. package/Sources/Private/CoreAnimation/Layers/SolidLayer.swift +20 -5
  32. package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +13 -3
  33. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +6 -4
  34. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Collection+Diff.swift +263 -0
  35. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Diffable.swift +18 -0
  36. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/DiffableSection.swift +16 -0
  37. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/IndexChangeset.swift +187 -0
  38. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/SectionedChangeset.swift +32 -0
  39. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Logging/EpoxyLogger.swift +99 -0
  40. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/CallbackContextEpoxyModeled.swift +8 -0
  41. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelArrayBuilder.swift +48 -0
  42. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelProperty.swift +158 -0
  43. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelStorage.swift +88 -0
  44. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModeled.swift +54 -0
  45. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/AnyEpoxyModelProperty.swift +29 -0
  46. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/ClassReference.swift +39 -0
  47. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/AnimatedProviding.swift +10 -0
  48. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DataIDProviding.swift +57 -0
  49. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidDisplayProviding.swift +41 -0
  50. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidEndDisplayingProviding.swift +41 -0
  51. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidSelectProviding.swift +36 -0
  52. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ErasedContentProviding.swift +49 -0
  53. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/MakeViewProviding.swift +60 -0
  54. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetBehaviorsProviding.swift +38 -0
  55. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetContentProviding.swift +38 -0
  56. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/StyleIDProviding.swift +37 -0
  57. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/TraitCollectionProviding.swift +14 -0
  58. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewDifferentiatorProviding.swift +34 -0
  59. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewProviding.swift +13 -0
  60. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/WillDisplayProviding.swift +41 -0
  61. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/ViewEpoxyModeled.swift +10 -0
  62. package/Sources/Private/EmbeddedLibraries/EpoxyCore/README.md +31 -0
  63. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingController.swift +46 -0
  64. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift +391 -0
  65. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIIntrinsicContentSizeInvalidator.swift +44 -0
  66. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUILayoutMargins.swift +51 -0
  67. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift +172 -0
  68. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +128 -0
  69. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +452 -0
  70. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/SwiftUIView.swift +148 -0
  71. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift +40 -0
  72. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift +43 -0
  73. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/BehaviorsConfigurableView.swift +45 -0
  74. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ContentConfigurableView.swift +36 -0
  75. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/EpoxyableView.swift +5 -0
  76. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/StyledView.swift +42 -0
  77. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ViewType.swift +51 -0
  78. package/Sources/Private/EmbeddedLibraries/README.md +27 -0
  79. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+BackingConfiguration.swift +4 -4
  80. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+MemoryFile.swift +2 -2
  81. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Writing.swift +4 -4
  82. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive.swift +8 -7
  83. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Compression.swift +5 -5
  84. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/FileManager+ZIP.swift +4 -4
  85. package/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift +6 -0
  86. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +3 -1
  87. package/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift +18 -5
  88. package/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift +31 -3
  89. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +33 -8
  90. package/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift +8 -1
  91. package/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift +13 -4
  92. package/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift +2 -2
  93. package/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift +1 -0
  94. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift +4 -4
  95. package/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift +1 -1
  96. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift +3 -1
  97. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +2 -1
  98. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift +2 -1
  99. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  100. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +2 -2
  101. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  102. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +1 -1
  103. package/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift +6 -2
  104. package/Sources/Private/Model/Assets/Asset.swift +8 -0
  105. package/Sources/Private/Model/Assets/AssetLibrary.swift +2 -2
  106. package/Sources/Private/Model/Assets/ImageAsset.swift +3 -3
  107. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +16 -2
  108. package/Sources/Private/Model/DotLottie/DotLottieImageProvider.swift +20 -7
  109. package/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift +26 -0
  110. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +8 -5
  111. package/Sources/Private/Model/LayerEffects/DropShadowEffect.swift +45 -0
  112. package/Sources/Private/Model/LayerEffects/EffectValues/ColorEffectValue.swift +38 -0
  113. package/Sources/Private/Model/LayerEffects/EffectValues/EffectValue.swift +98 -0
  114. package/Sources/Private/Model/LayerEffects/EffectValues/Vector1DEffectValue.swift +38 -0
  115. package/Sources/Private/Model/LayerEffects/LayerEffect.swift +103 -0
  116. package/Sources/Private/Model/LayerStyles/DropShadowStyle.swift +72 -0
  117. package/Sources/Private/Model/LayerStyles/LayerStyle.swift +85 -0
  118. package/Sources/Private/Model/Layers/LayerModel.swift +27 -0
  119. package/Sources/Private/Model/Objects/Marker.swift +1 -1
  120. package/Sources/Private/Model/Objects/Transform.swift +1 -2
  121. package/Sources/Private/Model/ShapeItems/GradientFill.swift +1 -1
  122. package/Sources/Private/Model/ShapeItems/GradientStroke.swift +2 -2
  123. package/Sources/Private/Model/ShapeItems/Merge.swift +1 -1
  124. package/Sources/Private/Model/ShapeItems/ShapeItem.swift +31 -26
  125. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -9
  126. package/Sources/Private/Model/ShapeItems/Star.swift +1 -1
  127. package/Sources/Private/Model/Text/Font.swift +2 -2
  128. package/Sources/Private/Model/Text/Glyph.swift +1 -1
  129. package/Sources/Private/RootAnimationLayer.swift +2 -2
  130. package/Sources/Private/Utility/Debugging/LayerDebugging.swift +3 -1
  131. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +32 -2
  132. package/Sources/Private/Utility/Helpers/AnimationContext.swift +4 -2
  133. package/Sources/Private/Utility/Helpers/AnyEquatable.swift +24 -0
  134. package/Sources/Private/Utility/Helpers/Binding+Map.swift +18 -0
  135. package/Sources/Private/Utility/Helpers/View+ValueChanged.swift +20 -0
  136. package/Sources/Private/Utility/LottieAnimationSource.swift +41 -0
  137. package/Sources/Private/Utility/Primitives/BezierPath.swift +2 -2
  138. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +2 -2
  139. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +1 -1
  140. package/Sources/Public/Animation/LottieAnimation.swift +21 -2
  141. package/Sources/Public/Animation/LottieAnimationLayer.swift +1464 -0
  142. package/Sources/Public/Animation/LottieAnimationView.swift +259 -736
  143. package/Sources/Public/Animation/LottiePlaybackMode.swift +117 -0
  144. package/Sources/Public/Animation/LottieView.swift +462 -0
  145. package/Sources/Public/AnimationCache/AnimationCacheProvider.swift +2 -1
  146. package/Sources/Public/AnimationCache/DefaultAnimationCache.swift +5 -6
  147. package/Sources/Public/Configuration/DecodingStrategy.swift +15 -0
  148. package/Sources/Public/Configuration/LottieConfiguration.swift +47 -0
  149. package/Sources/Public/Configuration/ReducedMotionOption.swift +114 -0
  150. package/Sources/Public/{LottieConfiguration.swift → Configuration/RenderingEngineOption.swift} +2 -57
  151. package/Sources/Public/{iOS → Controls}/AnimatedButton.swift +56 -13
  152. package/Sources/Public/{iOS → Controls}/AnimatedControl.swift +80 -8
  153. package/Sources/Public/{iOS → Controls}/AnimatedSwitch.swift +71 -31
  154. package/Sources/Public/Controls/LottieButton.swift +122 -0
  155. package/Sources/Public/Controls/LottieSwitch.swift +144 -0
  156. package/Sources/Public/Controls/LottieViewType.swift +79 -0
  157. package/Sources/Public/DotLottie/DotLottieConfiguration.swift +24 -0
  158. package/Sources/Public/DotLottie/DotLottieFile.swift +21 -6
  159. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +79 -0
  160. package/Sources/Public/DynamicProperties/AnimationKeypath.swift +11 -1
  161. package/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift +14 -0
  162. package/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift +13 -0
  163. package/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +22 -5
  164. package/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift +14 -0
  165. package/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift +13 -0
  166. package/Sources/Public/FontProvider/AnimationFontProvider.swift +8 -0
  167. package/Sources/Public/ImageProvider/AnimationImageProvider.swift +24 -0
  168. package/Sources/Public/Keyframes/Keyframe.swift +6 -0
  169. package/Sources/Public/Primitives/Vectors.swift +2 -2
  170. package/Sources/Public/TextProvider/AnimationTextProvider.swift +79 -6
  171. package/Sources/Public/iOS/AnimationSubview.swift +1 -1
  172. package/Sources/Public/iOS/BundleImageProvider.swift +16 -2
  173. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +6 -4
  174. package/Sources/Public/iOS/FilepathImageProvider.swift +22 -3
  175. package/Sources/Public/iOS/LottieAnimationViewBase.swift +7 -1
  176. package/Sources/Public/macOS/BundleImageProvider.macOS.swift +15 -1
  177. package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +21 -2
  178. package/lottie-ios.podspec +2 -1
  179. package/package.json +1 -1
  180. package/Sources/Private/Model/DotLottie/DotLottieConfiguration.swift +0 -15
  181. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Helpers.swift +0 -0
  182. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Progress.swift +0 -0
  183. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Reading.swift +0 -0
  184. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ReadingDeprecated.swift +0 -0
  185. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+WritingDeprecated.swift +0 -0
  186. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ZIP64.swift +0 -0
  187. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+CompressionDeprecated.swift +0 -0
  188. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Serialization.swift +0 -0
  189. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+Serialization.swift +0 -0
  190. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+ZIP64.swift +0 -0
  191. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry.swift +0 -0
  192. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/README.md +0 -0
  193. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/URL+ZIP.swift +0 -0
@@ -26,6 +26,16 @@ struct LayerProperty<ValueRepresentation> {
26
26
  }
27
27
 
28
28
  extension LayerProperty where ValueRepresentation: Equatable {
29
+ /// Initializes a `LayerProperty` that corresponds to a property on `CALayer`
30
+ /// or some other `CALayer` subclass like `CAShapeLayer`.
31
+ /// - Parameters:
32
+ /// - caLayerKeypath: The Objective-C `#keyPath` to the `CALayer` property,
33
+ /// e.g. `#keyPath(CALayer.opacity)` or `#keyPath(CAShapeLayer.path)`.
34
+ /// - defaultValue: The default value of the property (e.g. the value of the
35
+ /// property immediately after calling `CALayer.init()`). Knowing this value
36
+ /// lets us perform some optimizations in `CALayer+addAnimation`.
37
+ /// - customizableProperty: A description of how this property can be customized
38
+ /// dynamically at runtime using `AnimationView.setValueProvider(_:keypath:)`.
29
39
  init(
30
40
  caLayerKeypath: String,
31
41
  defaultValue: ValueRepresentation?,
@@ -52,7 +62,10 @@ struct CustomizableProperty<ValueRepresentation> {
52
62
 
53
63
  /// A closure that coverts the type-erased value of an `AnyValueProvider`
54
64
  /// to the strongly-typed representation used by this property, if possible.
55
- let conversion: (Any) -> ValueRepresentation?
65
+ /// - `value` is the value for the current frame that should be converted,
66
+ /// as returned by `AnyValueProvider.typeErasedStorage`.
67
+ /// - `valueProvider` is the `AnyValueProvider` that returned the type-erased value.
68
+ let conversion: (_ value: Any, _ valueProvider: AnyValueProvider) -> ValueRepresentation?
56
69
  }
57
70
 
58
71
  // MARK: - PropertyName
@@ -66,6 +79,8 @@ enum PropertyName: String, CaseIterable {
66
79
  case scale = "Scale"
67
80
  case position = "Position"
68
81
  case rotation = "Rotation"
82
+ case strokeWidth = "Stroke Width"
83
+ case gradientColors = "Colors"
69
84
  }
70
85
 
71
86
  // MARK: CALayer properties
@@ -159,6 +174,34 @@ extension LayerProperty {
159
174
  },
160
175
  customizableProperty: nil /* currently unsupported */ )
161
176
  }
177
+
178
+ static var shadowOpacity: LayerProperty<CGFloat> {
179
+ .init(
180
+ caLayerKeypath: #keyPath(CALayer.shadowOpacity),
181
+ defaultValue: 0,
182
+ customizableProperty: nil /* currently unsupported */ )
183
+ }
184
+
185
+ static var shadowColor: LayerProperty<CGColor> {
186
+ .init(
187
+ caLayerKeypath: #keyPath(CALayer.shadowColor),
188
+ defaultValue: .rgb(0, 0, 0),
189
+ customizableProperty: nil /* currently unsupported */ )
190
+ }
191
+
192
+ static var shadowRadius: LayerProperty<CGFloat> {
193
+ .init(
194
+ caLayerKeypath: #keyPath(CALayer.shadowRadius),
195
+ defaultValue: 3.0,
196
+ customizableProperty: nil /* currently unsupported */ )
197
+ }
198
+
199
+ static var shadowOffset: LayerProperty<CGSize> {
200
+ .init(
201
+ caLayerKeypath: #keyPath(CALayer.shadowOffset),
202
+ defaultValue: CGSize(width: 0, height: -3.0),
203
+ customizableProperty: nil /* currently unsupported */ )
204
+ }
162
205
  }
163
206
 
164
207
  // MARK: CAShapeLayer properties
@@ -182,7 +225,7 @@ extension LayerProperty {
182
225
  .init(
183
226
  caLayerKeypath: #keyPath(CAShapeLayer.lineWidth),
184
227
  defaultValue: 1,
185
- customizableProperty: nil /* currently unsupported */ )
228
+ customizableProperty: .floatValue(.strokeWidth))
186
229
  }
187
230
 
188
231
  static var lineDashPhase: LayerProperty<CGFloat> {
@@ -221,14 +264,14 @@ extension LayerProperty {
221
264
  .init(
222
265
  caLayerKeypath: #keyPath(CAGradientLayer.colors),
223
266
  defaultValue: nil,
224
- customizableProperty: nil /* currently unsupported */ )
267
+ customizableProperty: .gradientColors)
225
268
  }
226
269
 
227
270
  static var locations: LayerProperty<[CGFloat]> {
228
271
  .init(
229
272
  caLayerKeypath: #keyPath(CAGradientLayer.locations),
230
273
  defaultValue: nil,
231
- customizableProperty: nil /* currently unsupported */ )
274
+ customizableProperty: .gradientLocations)
232
275
  }
233
276
 
234
277
  static var startPoint: LayerProperty<CGPoint> {
@@ -252,7 +295,7 @@ extension CustomizableProperty {
252
295
  static var color: CustomizableProperty<CGColor> {
253
296
  .init(
254
297
  name: [.color],
255
- conversion: { typeErasedValue in
298
+ conversion: { typeErasedValue, _ in
256
299
  guard let color = typeErasedValue as? LottieColor else {
257
300
  return nil
258
301
  }
@@ -264,7 +307,7 @@ extension CustomizableProperty {
264
307
  static var opacity: CustomizableProperty<CGFloat> {
265
308
  .init(
266
309
  name: [.opacity],
267
- conversion: { typeErasedValue in
310
+ conversion: { typeErasedValue, _ in
268
311
  guard let vector = typeErasedValue as? LottieVector1D else { return nil }
269
312
 
270
313
  // Lottie animation files express opacity as a numerical percentage value
@@ -277,7 +320,7 @@ extension CustomizableProperty {
277
320
  static var scaleX: CustomizableProperty<CGFloat> {
278
321
  .init(
279
322
  name: [.scale],
280
- conversion: { typeErasedValue in
323
+ conversion: { typeErasedValue, _ in
281
324
  guard let vector = typeErasedValue as? LottieVector3D else { return nil }
282
325
 
283
326
  // Lottie animation files express scale as a numerical percentage value
@@ -290,7 +333,7 @@ extension CustomizableProperty {
290
333
  static var scaleY: CustomizableProperty<CGFloat> {
291
334
  .init(
292
335
  name: [.scale],
293
- conversion: { typeErasedValue in
336
+ conversion: { typeErasedValue, _ in
294
337
  guard let vector = typeErasedValue as? LottieVector3D else { return nil }
295
338
 
296
339
  // Lottie animation files express scale as a numerical percentage value
@@ -303,7 +346,7 @@ extension CustomizableProperty {
303
346
  static var rotation: CustomizableProperty<CGFloat> {
304
347
  .init(
305
348
  name: [.rotation],
306
- conversion: { typeErasedValue in
349
+ conversion: { typeErasedValue, _ in
307
350
  guard let vector = typeErasedValue as? LottieVector1D else { return nil }
308
351
 
309
352
  // Lottie animation files express rotation in degrees
@@ -316,6 +359,36 @@ extension CustomizableProperty {
316
359
  static var position: CustomizableProperty<CGPoint> {
317
360
  .init(
318
361
  name: [.position],
319
- conversion: { ($0 as? LottieVector3D)?.pointValue })
362
+ conversion: { typeErasedValue, _ in
363
+ guard let vector = typeErasedValue as? LottieVector3D else { return nil }
364
+ return vector.pointValue
365
+ })
366
+ }
367
+
368
+ static var gradientColors: CustomizableProperty<[CGColor]> {
369
+ .init(
370
+ name: [.gradientColors],
371
+ conversion: { _, typeErasedValueProvider in
372
+ guard let gradientValueProvider = typeErasedValueProvider as? GradientValueProvider else { return nil }
373
+ return gradientValueProvider.colors.map { $0.cgColorValue }
374
+ })
375
+ }
376
+
377
+ static var gradientLocations: CustomizableProperty<[CGFloat]> {
378
+ .init(
379
+ name: [.gradientColors],
380
+ conversion: { _, typeErasedValueProvider in
381
+ guard let gradientValueProvider = typeErasedValueProvider as? GradientValueProvider else { return nil }
382
+ return gradientValueProvider.locations.map { CGFloat($0) }
383
+ })
384
+ }
385
+
386
+ static func floatValue(_ name: PropertyName...) -> CustomizableProperty<CGFloat> {
387
+ .init(
388
+ name: name,
389
+ conversion: { typeErasedValue, _ in
390
+ guard let vector = typeErasedValue as? LottieVector1D else { return nil }
391
+ return vector.cgFloatValue
392
+ })
320
393
  }
321
394
  }
@@ -6,7 +6,7 @@ import QuartzCore
6
6
  // MARK: - TransformModel
7
7
 
8
8
  /// This protocol mirrors the interface of `Transform`,
9
- /// but it also implemented by `ShapeTransform` to allow
9
+ /// but is also implemented by `ShapeTransform` to allow
10
10
  /// both transform types to share the same animation implementation.
11
11
  protocol TransformModel {
12
12
  /// The anchor point of the transform.
@@ -32,6 +32,12 @@ protocol TransformModel {
32
32
 
33
33
  /// The rotation of the transform on Z axis.
34
34
  var rotationZ: KeyframeGroup<LottieVector1D> { get }
35
+
36
+ /// The skew of the transform (only present on `ShapeTransform`s)
37
+ var _skew: KeyframeGroup<LottieVector1D>? { get }
38
+
39
+ /// The skew axis of the transform (only present on `ShapeTransform`s)
40
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { get }
35
41
  }
36
42
 
37
43
  // MARK: - Transform + TransformModel
@@ -40,6 +46,8 @@ extension Transform: TransformModel {
40
46
  var _position: KeyframeGroup<LottieVector3D>? { position }
41
47
  var _positionX: KeyframeGroup<LottieVector1D>? { positionX }
42
48
  var _positionY: KeyframeGroup<LottieVector1D>? { positionY }
49
+ var _skew: KeyframeGroup<LottieVector1D>? { nil }
50
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { nil }
43
51
  }
44
52
 
45
53
  // MARK: - ShapeTransform + TransformModel
@@ -49,6 +57,8 @@ extension ShapeTransform: TransformModel {
49
57
  var _position: KeyframeGroup<LottieVector3D>? { position }
50
58
  var _positionX: KeyframeGroup<LottieVector1D>? { nil }
51
59
  var _positionY: KeyframeGroup<LottieVector1D>? { nil }
60
+ var _skew: KeyframeGroup<LottieVector1D>? { skew }
61
+ var _skewAxis: KeyframeGroup<LottieVector1D>? { skewAxis }
52
62
  }
53
63
 
54
64
  // MARK: - CALayer + TransformModel
@@ -66,15 +76,18 @@ extension CALayer {
66
76
  context: LayerAnimationContext)
67
77
  throws
68
78
  {
69
- // CALayers don't support animating skew with its own set of keyframes.
70
- // If the transform includes a skew, we have to combine all of the transform
71
- // components into a single set of keyframes.
72
- // Only `ShapeTransform` supports skews.
73
79
  if
74
- let shapeTransform = transformModel as? ShapeTransform,
75
- shapeTransform.hasSkew
80
+ // CALayers don't support animating skew with its own set of keyframes.
81
+ // If the transform includes a skew, we have to combine all of the transform
82
+ // components into a single set of keyframes.
83
+ transformModel.hasSkew
84
+ // Negative `scale.x` values aren't applied correctly by Core Animation when animating
85
+ // `transform.scale.x` and `transform.scale.y` using separate `CAKeyframeAnimation`s
86
+ // (https://openradar.appspot.com/FB9862872). If the transform includes negative `scale.x`
87
+ // values, we have to combine all of the transform components into a single set of keyframes.
88
+ || transformModel.hasNegativeXScaleValues
76
89
  {
77
- try addCombinedTransformAnimation(for: shapeTransform, context: context)
90
+ try addCombinedTransformAnimation(for: transformModel, context: context)
78
91
  }
79
92
 
80
93
  else {
@@ -159,65 +172,10 @@ extension CALayer {
159
172
  // Lottie animation files express scale as a numerical percentage value
160
173
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
161
174
  // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
162
- // - Negative `scale.x` values aren't applied correctly by Core Animation.
163
- // This appears to be because we animate `transform.scale.x` and `transform.scale.y`
164
- // as separate `CAKeyframeAnimation`s instead of using a single animation of `transform` itself.
165
- // https://openradar.appspot.com/FB9862872
166
- // - To work around this, we set up a `rotationY` animation below
167
- // to flip the view horizontally, which gives us the desired effect.
168
- abs(CGFloat(scale.x) / 100)
175
+ CGFloat(scale.x) / 100
169
176
  },
170
177
  context: context)
171
178
 
172
- /// iOS 14 and earlier doesn't properly support rendering transforms with
173
- /// negative `scale.x` values: https://github.com/airbnb/lottie-ios/issues/1882
174
- let osSupportsNegativeScaleValues: Bool = {
175
- #if os(iOS) || os(tvOS)
176
- if #available(iOS 15.0, tvOS 15.0, *) {
177
- return true
178
- } else {
179
- return false
180
- }
181
- #else
182
- // We'll assume this works correctly on macOS until told otherwise
183
- return true
184
- #endif
185
- }()
186
-
187
- lazy var hasNegativeXScaleValues = transformModel.scale.keyframes.contains(where: { $0.value.x < 0 })
188
-
189
- // When `scale.x` is negative, we have to rotate the view
190
- // half way around the y axis to flip it horizontally.
191
- // - We don't do this in snapshot tests because it breaks the tests
192
- // in surprising ways that don't happen at runtime. Definitely not ideal.
193
- // - This isn't supported on iOS 14 and earlier either, so we have to
194
- // log a compatibility error on devices running older OSs.
195
- if TestHelpers.snapshotTestsAreRunning {
196
- if hasNegativeXScaleValues {
197
- context.logger.warn("""
198
- Negative `scale.x` values are not displayed correctly in snapshot tests
199
- """)
200
- }
201
- } else {
202
- if !osSupportsNegativeScaleValues, hasNegativeXScaleValues {
203
- try context.logCompatibilityIssue("""
204
- iOS 14 and earlier does not support rendering negative `scale.x` values
205
- """)
206
- }
207
-
208
- try addAnimation(
209
- for: .rotationY,
210
- keyframes: transformModel.scale,
211
- value: { scale in
212
- if scale.x < 0 {
213
- return .pi
214
- } else {
215
- return 0
216
- }
217
- },
218
- context: context)
219
- }
220
-
221
179
  try addAnimation(
222
180
  for: .scaleY,
223
181
  keyframes: transformModel.scale,
@@ -225,9 +183,6 @@ extension CALayer {
225
183
  // Lottie animation files express scale as a numerical percentage value
226
184
  // (e.g. 50%, 100%, 200%) so we divide by 100 to get the decimal values
227
185
  // expected by Core Animation (e.g. 0.5, 1.0, 2.0).
228
- // - Negative `scaleY` values are correctly applied (they flip the view
229
- // vertically), so we don't have to apply an additional rotation animation
230
- // like we do for `scaleX`.
231
186
  CGFloat(scale.y) / 100
232
187
  },
233
188
  context: context)
@@ -258,7 +213,7 @@ extension CALayer {
258
213
  }
259
214
 
260
215
  // Lottie animation files express rotation in degrees
261
- // (e.g. 90º, 180º, 360º) so we covert to radians to get the
216
+ // (e.g. 90º, 180º, 360º) so we convert to radians to get the
262
217
  // values expected by Core Animation (e.g. π/2, π, 2π)
263
218
 
264
219
  try addAnimation(
@@ -282,7 +237,7 @@ extension CALayer {
282
237
  keyframes: transformModel.rotationZ,
283
238
  value: { rotationDegrees in
284
239
  // Lottie animation files express rotation in degrees
285
- // (e.g. 90º, 180º, 360º) so we covert to radians to get the
240
+ // (e.g. 90º, 180º, 360º) so we convert to radians to get the
286
241
  // values expected by Core Animation (e.g. π/2, π, 2π)
287
242
  rotationDegrees.cgFloatValue * .pi / 180
288
243
  },
@@ -291,26 +246,44 @@ extension CALayer {
291
246
 
292
247
  /// Adds an animation for the entire `transform` key by combining all of the
293
248
  /// position / size / rotation / skew animations into a single set of keyframes.
294
- /// This is necessary when there's a skew animation, since skew can only
295
- /// be applied via a transform.
249
+ /// This is more expensive that animating each component separately, since
250
+ /// it may require manually interpolating the keyframes at each frame.
296
251
  private func addCombinedTransformAnimation(
297
- for transformModel: ShapeTransform,
252
+ for transformModel: TransformModel,
298
253
  context: LayerAnimationContext)
299
254
  throws
300
255
  {
256
+ // Core Animation doesn't animate skew changes properly. If the skew value
257
+ // changes over the course of the animation then we have to manually
258
+ // compute the `CATransform3D` for each frame individually.
259
+ let requiresManualInterpolation = transformModel.hasSkewAnimation
260
+
301
261
  let combinedTransformKeyframes = Keyframes.combined(
302
- transformModel.anchor,
303
- transformModel.position,
262
+ transformModel.anchorPoint,
263
+ transformModel._position ?? KeyframeGroup(LottieVector3D(x: 0.0, y: 0.0, z: 0.0)),
264
+ transformModel._positionX ?? KeyframeGroup(LottieVector1D(0)),
265
+ transformModel._positionY ?? KeyframeGroup(LottieVector1D(0)),
304
266
  transformModel.scale,
305
267
  transformModel.rotationX,
306
268
  transformModel.rotationY,
307
269
  transformModel.rotationZ,
308
- transformModel.skew,
309
- transformModel.skewAxis,
310
- makeCombinedResult: { anchor, position, scale, rotationX, rotationY, rotationZ, skew, skewAxis in
311
- CATransform3D.makeTransform(
270
+ transformModel._skew ?? KeyframeGroup(LottieVector1D(0)),
271
+ transformModel._skewAxis ?? KeyframeGroup(LottieVector1D(0)),
272
+ requiresManualInterpolation: requiresManualInterpolation,
273
+ makeCombinedResult: {
274
+ anchor, position, positionX, positionY, scale, rotationX, rotationY, rotationZ, skew, skewAxis
275
+ -> CATransform3D in
276
+
277
+ let transformPosition: CGPoint
278
+ if transformModel._positionX != nil, transformModel._positionY != nil {
279
+ transformPosition = CGPoint(x: positionX.cgFloatValue, y: positionY.cgFloatValue)
280
+ } else {
281
+ transformPosition = position.pointValue
282
+ }
283
+
284
+ return CATransform3D.makeTransform(
312
285
  anchor: anchor.pointValue,
313
- position: position.pointValue,
286
+ position: transformPosition,
314
287
  scale: scale.sizeValue,
315
288
  rotationX: rotationX.cgFloatValue,
316
289
  rotationY: rotationY.cgFloatValue,
@@ -327,3 +300,36 @@ extension CALayer {
327
300
  }
328
301
 
329
302
  }
303
+
304
+ extension TransformModel {
305
+ /// Whether or not this transform has a non-zero skew value
306
+ var hasSkew: Bool {
307
+ guard
308
+ let _skew = _skew,
309
+ let _skewAxis = _skewAxis,
310
+ !_skew.keyframes.isEmpty,
311
+ !_skewAxis.keyframes.isEmpty
312
+ else {
313
+ return false
314
+ }
315
+
316
+ return _skew.keyframes.contains(where: { $0.value.cgFloatValue != 0 })
317
+ }
318
+
319
+ /// Whether or not this transform has a non-zero skew value which animates
320
+ var hasSkewAnimation: Bool {
321
+ guard
322
+ hasSkew,
323
+ let _skew = _skew,
324
+ let _skewAxis = _skewAxis
325
+ else { return false }
326
+
327
+ return _skew.keyframes.count > 1
328
+ || _skewAxis.keyframes.count > 1
329
+ }
330
+
331
+ /// Whether or not this `TransformModel` has any negative X scale values
332
+ var hasNegativeXScaleValues: Bool {
333
+ scale.keyframes.contains(where: { $0.value.x < 0 })
334
+ }
335
+ }
@@ -17,7 +17,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
17
17
  init(
18
18
  animation: LottieAnimation,
19
19
  imageProvider: AnimationImageProvider,
20
- textProvider: AnimationTextProvider,
20
+ textProvider: AnimationKeypathTextProvider,
21
21
  fontProvider: AnimationFontProvider,
22
22
  maskAnimationToBounds: Bool,
23
23
  compatibilityTrackerMode: CompatibilityTracker.Mode,
@@ -94,8 +94,8 @@ final class CoreAnimationLayer: BaseAnimationLayer {
94
94
  }
95
95
  }
96
96
 
97
- /// The parent `LottieAnimationView` that manages this layer
98
- weak var animationView: LottieAnimationView?
97
+ /// The parent `LottieAnimationLayer` that manages this layer
98
+ weak var lottieAnimationLayer: LottieAnimationLayer?
99
99
 
100
100
  /// A closure that is called after this layer sets up its animation.
101
101
  /// If the animation setup was unsuccessful and encountered compatibility issues,
@@ -108,9 +108,9 @@ final class CoreAnimationLayer: BaseAnimationLayer {
108
108
  didSet { reloadImages() }
109
109
  }
110
110
 
111
- /// The `AnimationTextProvider` that `TextLayer`'s use to retrieve texts,
111
+ /// The `AnimationKeypathTextProvider` that `TextLayer`'s use to retrieve texts,
112
112
  /// that they should use to render their text context
113
- var textProvider: AnimationTextProvider {
113
+ var textProvider: AnimationKeypathTextProvider {
114
114
  didSet {
115
115
  // We need to rebuild the current animation after updating the text provider,
116
116
  // since this is used in `TextLayer.setupAnimations(context:)`
@@ -209,6 +209,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
209
209
  private let valueProviderStore: ValueProviderStore
210
210
  private let compatibilityTracker: CompatibilityTracker
211
211
  private let logger: LottieLogger
212
+ private let loggingState = LoggingState()
212
213
 
213
214
  /// The current playback state of the animation that is displayed in this layer
214
215
  private var currentPlaybackState: PlaybackState? {
@@ -265,6 +266,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
265
266
  valueProviderStore: valueProviderStore,
266
267
  compatibilityTracker: compatibilityTracker,
267
268
  logger: logger,
269
+ loggingState: loggingState,
268
270
  currentKeypath: AnimationKeypath(keys: []),
269
271
  textProvider: textProvider,
270
272
  recordHierarchyKeypath: configuration.recordHierarchyKeypath)
@@ -316,7 +318,7 @@ final class CoreAnimationLayer: BaseAnimationLayer {
316
318
  else { return }
317
319
 
318
320
  if isAnimationPlaying == true {
319
- animationView?.updateInFlightAnimation()
321
+ lottieAnimationLayer?.updateInFlightAnimation()
320
322
  } else {
321
323
  let currentFrame = currentFrame
322
324
  removeAnimations()
@@ -449,7 +451,9 @@ extension CoreAnimationLayer: RootAnimationLayer {
449
451
  }
450
452
 
451
453
  func forceDisplayUpdate() {
452
- // Unimplemented / unused
454
+ // Unimplemented
455
+ // - We can't call `display()` here, because it would cause unexpected frame animations:
456
+ // https://github.com/airbnb/lottie-ios/issues/2193
453
457
  }
454
458
 
455
459
  func logHierarchyKeypaths() {
@@ -10,12 +10,18 @@ enum Keyframes {
10
10
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
11
11
  /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
12
12
  /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
13
- static func combined<T>(_ allGroups: [KeyframeGroup<T>]) -> KeyframeGroup<[T]>
13
+ static func combined<T>(
14
+ _ allGroups: [KeyframeGroup<T>],
15
+ requiresManualInterpolation: Bool = false)
16
+ -> KeyframeGroup<[T]>
14
17
  where T: AnyInterpolatable
15
18
  {
16
- Keyframes.combined(allGroups, makeCombinedResult: { untypedValues in
17
- untypedValues.compactMap { $0 as? T }
18
- })
19
+ Keyframes.combined(
20
+ allGroups,
21
+ requiresManualInterpolation: requiresManualInterpolation,
22
+ makeCombinedResult: { untypedValues in
23
+ untypedValues.compactMap { $0 as? T }
24
+ })
19
25
  }
20
26
 
21
27
  /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
@@ -24,6 +30,7 @@ enum Keyframes {
24
30
  static func combined<T1, T2, CombinedResult>(
25
31
  _ k1: KeyframeGroup<T1>,
26
32
  _ k2: KeyframeGroup<T2>,
33
+ requiresManualInterpolation: Bool = false,
27
34
  makeCombinedResult: (T1, T2) throws -> CombinedResult)
28
35
  rethrows
29
36
  -> KeyframeGroup<CombinedResult>
@@ -31,6 +38,7 @@ enum Keyframes {
31
38
  {
32
39
  try Keyframes.combined(
33
40
  [k1, k2],
41
+ requiresManualInterpolation: requiresManualInterpolation,
34
42
  makeCombinedResult: { untypedValues in
35
43
  guard
36
44
  let t1 = untypedValues[0] as? T1,
@@ -48,12 +56,14 @@ enum Keyframes {
48
56
  _ k1: KeyframeGroup<T1>,
49
57
  _ k2: KeyframeGroup<T2>,
50
58
  _ k3: KeyframeGroup<T3>,
59
+ requiresManualInterpolation: Bool = false,
51
60
  makeCombinedResult: (T1, T2, T3) -> CombinedResult)
52
61
  -> KeyframeGroup<CombinedResult>
53
62
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable
54
63
  {
55
64
  Keyframes.combined(
56
65
  [k1, k2, k3],
66
+ requiresManualInterpolation: requiresManualInterpolation,
57
67
  makeCombinedResult: { untypedValues in
58
68
  guard
59
69
  let t1 = untypedValues[0] as? T1,
@@ -76,6 +86,7 @@ enum Keyframes {
76
86
  _ k5: KeyframeGroup<T5>,
77
87
  _ k6: KeyframeGroup<T6>,
78
88
  _ k7: KeyframeGroup<T7>,
89
+ requiresManualInterpolation: Bool = false,
79
90
  makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7) -> CombinedResult)
80
91
  -> KeyframeGroup<CombinedResult>
81
92
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
@@ -83,6 +94,7 @@ enum Keyframes {
83
94
  {
84
95
  Keyframes.combined(
85
96
  [k1, k2, k3, k4, k5, k6, k7],
97
+ requiresManualInterpolation: requiresManualInterpolation,
86
98
  makeCombinedResult: { untypedValues in
87
99
  guard
88
100
  let t1 = untypedValues[0] as? T1,
@@ -110,6 +122,7 @@ enum Keyframes {
110
122
  _ k6: KeyframeGroup<T6>,
111
123
  _ k7: KeyframeGroup<T7>,
112
124
  _ k8: KeyframeGroup<T8>,
125
+ requiresManualInterpolation: Bool = false,
113
126
  makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7, T8) -> CombinedResult)
114
127
  -> KeyframeGroup<CombinedResult>
115
128
  where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
@@ -117,6 +130,7 @@ enum Keyframes {
117
130
  {
118
131
  Keyframes.combined(
119
132
  [k1, k2, k3, k4, k5, k6, k7, k8],
133
+ requiresManualInterpolation: requiresManualInterpolation,
120
134
  makeCombinedResult: { untypedValues in
121
135
  guard
122
136
  let t1 = untypedValues[0] as? T1,
@@ -133,6 +147,48 @@ enum Keyframes {
133
147
  })
134
148
  }
135
149
 
150
+ /// Combines the given keyframe groups of `Keyframe<T>`s into a single keyframe group of of `Keyframe<[T]>`s
151
+ /// - If all of the `KeyframeGroup`s have the exact same animation timing, the keyframes are merged
152
+ /// - Otherwise, the keyframes are manually interpolated at each frame in the animation
153
+ static func combined<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, CombinedResult>(
154
+ _ k1: KeyframeGroup<T1>,
155
+ _ k2: KeyframeGroup<T2>,
156
+ _ k3: KeyframeGroup<T3>,
157
+ _ k4: KeyframeGroup<T4>,
158
+ _ k5: KeyframeGroup<T5>,
159
+ _ k6: KeyframeGroup<T6>,
160
+ _ k7: KeyframeGroup<T7>,
161
+ _ k8: KeyframeGroup<T8>,
162
+ _ k9: KeyframeGroup<T9>,
163
+ _ k10: KeyframeGroup<T10>,
164
+ requiresManualInterpolation: Bool = false,
165
+ makeCombinedResult: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> CombinedResult)
166
+ -> KeyframeGroup<CombinedResult>
167
+ where T1: AnyInterpolatable, T2: AnyInterpolatable, T3: AnyInterpolatable, T4: AnyInterpolatable,
168
+ T5: AnyInterpolatable, T6: AnyInterpolatable, T7: AnyInterpolatable, T8: AnyInterpolatable,
169
+ T9: AnyInterpolatable, T10: AnyInterpolatable
170
+ {
171
+ Keyframes.combined(
172
+ [k1, k2, k3, k4, k5, k6, k7, k8, k9, k10],
173
+ requiresManualInterpolation: requiresManualInterpolation,
174
+ makeCombinedResult: { untypedValues in
175
+ guard
176
+ let t1 = untypedValues[0] as? T1,
177
+ let t2 = untypedValues[1] as? T2,
178
+ let t3 = untypedValues[2] as? T3,
179
+ let t4 = untypedValues[3] as? T4,
180
+ let t5 = untypedValues[4] as? T5,
181
+ let t6 = untypedValues[5] as? T6,
182
+ let t7 = untypedValues[6] as? T7,
183
+ let t8 = untypedValues[7] as? T8,
184
+ let t9 = untypedValues[8] as? T9,
185
+ let t10 = untypedValues[9] as? T10
186
+ else { return nil }
187
+
188
+ return makeCombinedResult(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)
189
+ })
190
+ }
191
+
136
192
  // MARK: Private
137
193
 
138
194
  /// Combines the given `[KeyframeGroup]` of `Keyframe<T>`s into a single `KeyframeGroup` of `Keyframe<CombinedResult>`s
@@ -141,8 +197,12 @@ enum Keyframes {
141
197
  ///
142
198
  /// `makeCombinedResult` is a closure that takes an array of keyframe values (with the exact same length as `AnyKeyframeGroup`),
143
199
  /// casts them to the expected type, and combined them into the final resulting keyframe.
200
+ ///
201
+ /// `requiresManualInterpolation` determines whether the keyframes must be computed using `Keyframes.manuallyInterpolated`,
202
+ /// which interpolates the value at each frame, or if the keyframes can simply be combined.
144
203
  private static func combined<CombinedResult>(
145
204
  _ allGroups: [AnyKeyframeGroup],
205
+ requiresManualInterpolation: Bool,
146
206
  makeCombinedResult: ([Any]) throws -> CombinedResult?)
147
207
  rethrows
148
208
  -> KeyframeGroup<CombinedResult>
@@ -155,6 +215,7 @@ enum Keyframes {
155
215
  let animatingKeyframes = untypedGroups.filter { $0.keyframes.count > 1 }
156
216
 
157
217
  guard
218
+ !requiresManualInterpolation,
158
219
  !allGroups.isEmpty,
159
220
  animatingKeyframes.allSatisfy({ $0.hasSameTimingParameters(as: animatingKeyframes[0]) })
160
221
  else {
@@ -222,7 +283,7 @@ enum Keyframes {
222
283
  extension KeyframeGroup {
223
284
  /// Whether or not all of the keyframes in this `KeyframeGroup` have the same
224
285
  /// timing parameters as the corresponding keyframe in the other given `KeyframeGroup`
225
- func hasSameTimingParameters<T>(as other: KeyframeGroup<T>) -> Bool {
286
+ func hasSameTimingParameters<U>(as other: KeyframeGroup<U>) -> Bool {
226
287
  guard keyframes.count == other.keyframes.count else {
227
288
  return false
228
289
  }
@@ -236,7 +297,7 @@ extension KeyframeGroup {
236
297
  extension Keyframe {
237
298
  /// Whether or not this keyframe has the same timing parameters as the given keyframe,
238
299
  /// excluding `spatialInTangent` and `spatialOutTangent`.
239
- fileprivate func hasSameTimingParameters<T>(as other: Keyframe<T>) -> Bool {
300
+ fileprivate func hasSameTimingParameters<U>(as other: Keyframe<U>) -> Bool {
240
301
  time == other.time
241
302
  && isHold == other.isHold
242
303
  && inTangent == other.inTangent