lottie-ios 4.2.0 → 4.3.0

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 (192) 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 +1542 -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 +346 -102
  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/DropShadowAnimation.swift +160 -0
  20. package/Sources/Private/CoreAnimation/Animations/LayerProperty.swift +83 -10
  21. package/Sources/Private/CoreAnimation/Animations/TransformAnimations.swift +85 -79
  22. package/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +7 -7
  23. package/Sources/Private/CoreAnimation/Extensions/Keyframes+combined.swift +67 -6
  24. package/Sources/Private/CoreAnimation/Layers/AnimationLayer.swift +2 -2
  25. package/Sources/Private/CoreAnimation/Layers/BaseCompositionLayer.swift +9 -0
  26. package/Sources/Private/CoreAnimation/Layers/ImageLayer.swift +1 -0
  27. package/Sources/Private/CoreAnimation/Layers/LayerModel+makeAnimationLayer.swift +1 -1
  28. package/Sources/Private/CoreAnimation/Layers/RepeaterLayer.swift +2 -0
  29. package/Sources/Private/CoreAnimation/Layers/ShapeLayer.swift +11 -13
  30. package/Sources/Private/CoreAnimation/Layers/SolidLayer.swift +20 -5
  31. package/Sources/Private/CoreAnimation/Layers/TextLayer.swift +13 -3
  32. package/Sources/Private/CoreAnimation/ValueProviderStore.swift +6 -4
  33. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Collection+Diff.swift +263 -0
  34. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/Diffable.swift +18 -0
  35. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/DiffableSection.swift +16 -0
  36. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/IndexChangeset.swift +187 -0
  37. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Diffing/SectionedChangeset.swift +32 -0
  38. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Logging/EpoxyLogger.swift +99 -0
  39. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/CallbackContextEpoxyModeled.swift +8 -0
  40. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelArrayBuilder.swift +48 -0
  41. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelProperty.swift +158 -0
  42. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModelStorage.swift +88 -0
  43. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/EpoxyModeled.swift +54 -0
  44. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/AnyEpoxyModelProperty.swift +29 -0
  45. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Internal/ClassReference.swift +39 -0
  46. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/AnimatedProviding.swift +10 -0
  47. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DataIDProviding.swift +57 -0
  48. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidDisplayProviding.swift +41 -0
  49. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidEndDisplayingProviding.swift +41 -0
  50. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/DidSelectProviding.swift +36 -0
  51. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ErasedContentProviding.swift +49 -0
  52. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/MakeViewProviding.swift +60 -0
  53. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetBehaviorsProviding.swift +38 -0
  54. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/SetContentProviding.swift +38 -0
  55. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/StyleIDProviding.swift +37 -0
  56. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/TraitCollectionProviding.swift +14 -0
  57. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewDifferentiatorProviding.swift +34 -0
  58. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/ViewProviding.swift +13 -0
  59. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/Providers/WillDisplayProviding.swift +41 -0
  60. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Model/ViewEpoxyModeled.swift +10 -0
  61. package/Sources/Private/EmbeddedLibraries/EpoxyCore/README.md +31 -0
  62. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingController.swift +46 -0
  63. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIHostingView.swift +391 -0
  64. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUIIntrinsicContentSizeInvalidator.swift +44 -0
  65. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxySwiftUILayoutMargins.swift +51 -0
  66. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift +172 -0
  67. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +128 -0
  68. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +452 -0
  69. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/SwiftUIView.swift +148 -0
  70. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift +40 -0
  71. package/Sources/Private/EmbeddedLibraries/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift +43 -0
  72. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/BehaviorsConfigurableView.swift +45 -0
  73. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ContentConfigurableView.swift +36 -0
  74. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/EpoxyableView.swift +5 -0
  75. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/StyledView.swift +42 -0
  76. package/Sources/Private/EmbeddedLibraries/EpoxyCore/Views/ViewType.swift +51 -0
  77. package/Sources/Private/EmbeddedLibraries/README.md +27 -0
  78. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+BackingConfiguration.swift +4 -4
  79. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+MemoryFile.swift +2 -2
  80. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Writing.swift +4 -4
  81. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive.swift +8 -7
  82. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Compression.swift +5 -5
  83. package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/FileManager+ZIP.swift +4 -4
  84. package/Sources/Private/MainThread/LayerContainers/CompLayers/ImageCompositionLayer.swift +6 -0
  85. package/Sources/Private/MainThread/LayerContainers/CompLayers/MaskContainerLayer.swift +3 -1
  86. package/Sources/Private/MainThread/LayerContainers/CompLayers/PreCompositionLayer.swift +18 -5
  87. package/Sources/Private/MainThread/LayerContainers/CompLayers/TextCompositionLayer.swift +31 -3
  88. package/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +33 -8
  89. package/Sources/Private/MainThread/LayerContainers/Utility/CachedImageProvider.swift +8 -1
  90. package/Sources/Private/MainThread/LayerContainers/Utility/CompositionLayersInitializer.swift +13 -4
  91. package/Sources/Private/MainThread/LayerContainers/Utility/LayerFontProvider.swift +2 -2
  92. package/Sources/Private/MainThread/LayerContainers/Utility/LayerImageProvider.swift +1 -0
  93. package/Sources/Private/MainThread/LayerContainers/Utility/LayerTextProvider.swift +4 -4
  94. package/Sources/Private/MainThread/NodeRenderSystem/NodeProperties/Protocols/NodePropertyMap.swift +1 -1
  95. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/ModifierNodes/TrimPathNode.swift +3 -1
  96. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/GradientFillRenderer.swift +2 -1
  97. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/OutputNodes/Renderables/LegacyGradientFillRenderer.swift +2 -1
  98. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientFillNode.swift +1 -1
  99. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/GradientStrokeNode.swift +2 -2
  100. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/RenderNodes/StrokeNode.swift +1 -1
  101. package/Sources/Private/MainThread/NodeRenderSystem/Nodes/Text/TextAnimatorNode.swift +1 -1
  102. package/Sources/Private/MainThread/NodeRenderSystem/RenderLayers/ShapeContainerLayer.swift +6 -2
  103. package/Sources/Private/Model/Assets/Asset.swift +8 -0
  104. package/Sources/Private/Model/Assets/AssetLibrary.swift +2 -2
  105. package/Sources/Private/Model/Assets/ImageAsset.swift +3 -3
  106. package/Sources/Private/Model/DotLottie/DotLottieAnimation.swift +16 -2
  107. package/Sources/Private/Model/DotLottie/DotLottieImageProvider.swift +20 -7
  108. package/Sources/Private/Model/Extensions/KeyedDecodingContainerExtensions.swift +26 -0
  109. package/Sources/Private/Model/Keyframes/KeyframeGroup.swift +8 -5
  110. package/Sources/Private/Model/LayerEffects/DropShadowEffect.swift +45 -0
  111. package/Sources/Private/Model/LayerEffects/EffectValues/ColorEffectValue.swift +38 -0
  112. package/Sources/Private/Model/LayerEffects/EffectValues/EffectValue.swift +98 -0
  113. package/Sources/Private/Model/LayerEffects/EffectValues/Vector1DEffectValue.swift +38 -0
  114. package/Sources/Private/Model/LayerEffects/LayerEffect.swift +103 -0
  115. package/Sources/Private/Model/LayerStyles/DropShadowStyle.swift +72 -0
  116. package/Sources/Private/Model/LayerStyles/LayerStyle.swift +85 -0
  117. package/Sources/Private/Model/Layers/LayerModel.swift +27 -0
  118. package/Sources/Private/Model/Objects/Marker.swift +1 -1
  119. package/Sources/Private/Model/Objects/Transform.swift +1 -2
  120. package/Sources/Private/Model/ShapeItems/GradientFill.swift +1 -1
  121. package/Sources/Private/Model/ShapeItems/GradientStroke.swift +2 -2
  122. package/Sources/Private/Model/ShapeItems/Merge.swift +1 -1
  123. package/Sources/Private/Model/ShapeItems/ShapeItem.swift +31 -26
  124. package/Sources/Private/Model/ShapeItems/ShapeTransform.swift +0 -9
  125. package/Sources/Private/Model/ShapeItems/Star.swift +1 -1
  126. package/Sources/Private/Model/Text/Font.swift +2 -2
  127. package/Sources/Private/Model/Text/Glyph.swift +1 -1
  128. package/Sources/Private/RootAnimationLayer.swift +2 -2
  129. package/Sources/Private/Utility/Debugging/LayerDebugging.swift +3 -1
  130. package/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +32 -2
  131. package/Sources/Private/Utility/Helpers/AnimationContext.swift +4 -2
  132. package/Sources/Private/Utility/Helpers/AnyEquatable.swift +24 -0
  133. package/Sources/Private/Utility/Helpers/Binding+Map.swift +18 -0
  134. package/Sources/Private/Utility/Helpers/View+ValueChanged.swift +20 -0
  135. package/Sources/Private/Utility/LottieAnimationSource.swift +41 -0
  136. package/Sources/Private/Utility/Primitives/BezierPath.swift +2 -2
  137. package/Sources/Private/Utility/Primitives/BezierPathRoundExtension.swift +2 -2
  138. package/Sources/Private/Utility/Primitives/VectorsExtensions.swift +1 -1
  139. package/Sources/Public/Animation/LottieAnimation.swift +21 -2
  140. package/Sources/Public/Animation/LottieAnimationLayer.swift +1464 -0
  141. package/Sources/Public/Animation/LottieAnimationView.swift +258 -735
  142. package/Sources/Public/Animation/LottiePlaybackMode.swift +117 -0
  143. package/Sources/Public/Animation/LottieView.swift +462 -0
  144. package/Sources/Public/AnimationCache/AnimationCacheProvider.swift +2 -1
  145. package/Sources/Public/AnimationCache/DefaultAnimationCache.swift +5 -6
  146. package/Sources/Public/Configuration/DecodingStrategy.swift +15 -0
  147. package/Sources/Public/Configuration/LottieConfiguration.swift +47 -0
  148. package/Sources/Public/Configuration/ReducedMotionOption.swift +114 -0
  149. package/Sources/Public/{LottieConfiguration.swift → Configuration/RenderingEngineOption.swift} +2 -57
  150. package/Sources/Public/{iOS → Controls}/AnimatedButton.swift +56 -13
  151. package/Sources/Public/{iOS → Controls}/AnimatedControl.swift +80 -8
  152. package/Sources/Public/{iOS → Controls}/AnimatedSwitch.swift +71 -31
  153. package/Sources/Public/Controls/LottieButton.swift +122 -0
  154. package/Sources/Public/Controls/LottieSwitch.swift +144 -0
  155. package/Sources/Public/Controls/LottieViewType.swift +79 -0
  156. package/Sources/Public/DotLottie/DotLottieConfiguration.swift +24 -0
  157. package/Sources/Public/DotLottie/DotLottieFile.swift +21 -6
  158. package/Sources/Public/DotLottie/DotLottieFileHelpers.swift +79 -0
  159. package/Sources/Public/DynamicProperties/AnimationKeypath.swift +11 -1
  160. package/Sources/Public/DynamicProperties/ValueProviders/ColorValueProvider.swift +14 -0
  161. package/Sources/Public/DynamicProperties/ValueProviders/FloatValueProvider.swift +13 -0
  162. package/Sources/Public/DynamicProperties/ValueProviders/GradientValueProvider.swift +22 -5
  163. package/Sources/Public/DynamicProperties/ValueProviders/PointValueProvider.swift +14 -0
  164. package/Sources/Public/DynamicProperties/ValueProviders/SizeValueProvider.swift +13 -0
  165. package/Sources/Public/FontProvider/AnimationFontProvider.swift +8 -0
  166. package/Sources/Public/ImageProvider/AnimationImageProvider.swift +24 -0
  167. package/Sources/Public/Keyframes/Keyframe.swift +6 -0
  168. package/Sources/Public/Primitives/Vectors.swift +2 -2
  169. package/Sources/Public/TextProvider/AnimationTextProvider.swift +79 -6
  170. package/Sources/Public/iOS/AnimationSubview.swift +1 -1
  171. package/Sources/Public/iOS/BundleImageProvider.swift +16 -2
  172. package/Sources/Public/iOS/Compatibility/CompatibleAnimationView.swift +2 -2
  173. package/Sources/Public/iOS/FilepathImageProvider.swift +22 -3
  174. package/Sources/Public/iOS/LottieAnimationViewBase.swift +7 -1
  175. package/Sources/Public/macOS/BundleImageProvider.macOS.swift +15 -1
  176. package/Sources/Public/macOS/FilepathImageProvider.macOS.swift +21 -2
  177. package/lottie-ios.podspec +2 -1
  178. package/package.json +1 -1
  179. package/Sources/Private/Model/DotLottie/DotLottieConfiguration.swift +0 -15
  180. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Helpers.swift +0 -0
  181. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Progress.swift +0 -0
  182. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+Reading.swift +0 -0
  183. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ReadingDeprecated.swift +0 -0
  184. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+WritingDeprecated.swift +0 -0
  185. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Archive+ZIP64.swift +0 -0
  186. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+CompressionDeprecated.swift +0 -0
  187. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Data+Serialization.swift +0 -0
  188. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+Serialization.swift +0 -0
  189. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry+ZIP64.swift +0 -0
  190. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/Entry.swift +0 -0
  191. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/README.md +0 -0
  192. /package/Sources/Private/{Model/DotLottie → EmbeddedLibraries}/ZipFoundation/URL+ZIP.swift +0 -0
@@ -0,0 +1,391 @@
1
+ // Created by eric_horacek on 9/16/21.
2
+ // Copyright © 2021 Airbnb Inc. All rights reserved.
3
+
4
+ import Combine
5
+ import SwiftUI
6
+
7
+ #if !os(macOS)
8
+
9
+ // MARK: - SwiftUIHostingViewReuseBehavior
10
+
11
+ /// The reuse behavior of an `EpoxySwiftUIHostingView`.
12
+ enum SwiftUIHostingViewReuseBehavior: Hashable {
13
+ /// Instances of a `EpoxySwiftUIHostingView` with `RootView`s of same type can be reused within
14
+ /// the Epoxy container.
15
+ ///
16
+ /// This is the default reuse behavior.
17
+ case reusable
18
+ /// Instances of a `EpoxySwiftUIHostingView` with `RootView`s of same type can only reused within
19
+ /// the Epoxy container when they have identical `reuseID`s.
20
+ case unique(reuseID: AnyHashable)
21
+ }
22
+
23
+ // MARK: - CallbackContextEpoxyModeled
24
+
25
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
26
+ extension CallbackContextEpoxyModeled
27
+ where
28
+ Self: WillDisplayProviding & DidEndDisplayingProviding,
29
+ CallbackContext: ViewProviding & AnimatedProviding
30
+ {
31
+ /// Updates the appearance state of a `EpoxySwiftUIHostingView` in coordination with the
32
+ /// `willDisplay` and `didEndDisplaying` callbacks of this `EpoxyableModel`.
33
+ ///
34
+ /// - Note: You should only need to call then from the implementation of a concrete
35
+ /// `EpoxyableModel` convenience vendor method, e.g. `SwiftUI.View.itemModel(…)`.
36
+ func linkDisplayLifecycle<RootView: View>() -> Self
37
+ where
38
+ CallbackContext.View == EpoxySwiftUIHostingView<RootView>
39
+ {
40
+ willDisplay { context in
41
+ context.view.handleWillDisplay(animated: context.animated)
42
+ }
43
+ .didEndDisplaying { context in
44
+ context.view.handleDidEndDisplaying(animated: context.animated)
45
+ }
46
+ }
47
+ }
48
+
49
+ // MARK: - EpoxySwiftUIHostingView
50
+
51
+ /// A `UIView` that hosts a SwiftUI view within an Epoxy container, e.g. an Epoxy `CollectionView`.
52
+ ///
53
+ /// Wraps an `EpoxySwiftUIHostingController` and adds it as a child view controller to the next
54
+ /// ancestor view controller in the hierarchy.
55
+ ///
56
+ /// There's a private API that accomplishes this same behavior without needing a `UIViewController`:
57
+ /// `_UIHostingView`, but we can't safely use it as 1) the behavior may change out from under us, 2)
58
+ /// the API is private and 3) the `_UIHostingView` doesn't not accept setting a new `View` instance.
59
+ ///
60
+ /// - SeeAlso: `EpoxySwiftUIHostingController`
61
+ @available(iOS 13.0, tvOS 13.0, *)
62
+ final class EpoxySwiftUIHostingView<RootView: View>: UIView, EpoxyableView {
63
+
64
+ // MARK: Lifecycle
65
+
66
+ init(style: Style) {
67
+ // Ignore the safe area to ensure the view isn't laid out incorrectly when being sized while
68
+ // overlapping the safe area.
69
+ epoxyContent = EpoxyHostingContent(rootView: style.initialContent.rootView)
70
+ viewController = EpoxySwiftUIHostingController(
71
+ rootView: .init(content: epoxyContent, environment: epoxyEnvironment),
72
+ ignoreSafeArea: true)
73
+
74
+ dataID = style.initialContent.dataID ?? DefaultDataID.noneProvided as AnyHashable
75
+
76
+ super.init(frame: .zero)
77
+
78
+ epoxyEnvironment.intrinsicContentSizeInvalidator = .init(invalidate: { [weak self] in
79
+ self?.viewController.view.invalidateIntrinsicContentSize()
80
+
81
+ // Inform the enclosing collection view that the size has changed, if we're contained in one,
82
+ // allowing the cell to resize.
83
+ //
84
+ // On iOS 16+, we could call `invalidateIntrinsicContentSize()` on the enclosing collection
85
+ // view cell instead, but that currently causes visual artifacts with `MagazineLayout`. The
86
+ // better long term fix is likely to switch to `UIHostingConfiguration` on iOS 16+ anyways.
87
+ if let enclosingCollectionView = self?.superview?.superview?.superview as? UICollectionView {
88
+ enclosingCollectionView.collectionViewLayout.invalidateLayout()
89
+ }
90
+ })
91
+ layoutMargins = .zero
92
+ }
93
+
94
+ @available(*, unavailable)
95
+ required init?(coder _: NSCoder) {
96
+ fatalError("init(coder:) has not been implemented")
97
+ }
98
+
99
+ // MARK: Internal
100
+
101
+ struct Style: Hashable {
102
+ init(reuseBehavior: SwiftUIHostingViewReuseBehavior, initialContent: Content) {
103
+ self.reuseBehavior = reuseBehavior
104
+ self.initialContent = initialContent
105
+ }
106
+
107
+ var reuseBehavior: SwiftUIHostingViewReuseBehavior
108
+ var initialContent: Content
109
+
110
+ static func == (lhs: Style, rhs: Style) -> Bool {
111
+ lhs.reuseBehavior == rhs.reuseBehavior
112
+ }
113
+
114
+ func hash(into hasher: inout Hasher) {
115
+ hasher.combine(reuseBehavior)
116
+ }
117
+ }
118
+
119
+ struct Content: Equatable {
120
+ init(rootView: RootView, dataID: AnyHashable?) {
121
+ self.rootView = rootView
122
+ self.dataID = dataID
123
+ }
124
+
125
+ var rootView: RootView
126
+ var dataID: AnyHashable?
127
+
128
+ static func == (_: Content, _: Content) -> Bool {
129
+ // The content should never be equal since we need the `rootView` to be updated on every
130
+ // content change.
131
+ false
132
+ }
133
+ }
134
+
135
+ override func didMoveToWindow() {
136
+ super.didMoveToWindow()
137
+
138
+ // We'll only be able to discover a valid parent `viewController` once we're added to a window,
139
+ // so we do so here in addition to the `handleWillDisplay(…)` method.
140
+ if window != nil {
141
+ addViewControllerIfNeeded()
142
+ }
143
+ }
144
+
145
+ func setContent(_ content: Content, animated _: Bool) {
146
+ // This triggers a change in the observed `EpoxyHostingContent` object and allows the
147
+ // propagation of the SwiftUI transaction, instead of just replacing the `rootView`.
148
+ epoxyContent.rootView = content.rootView
149
+ dataID = content.dataID ?? DefaultDataID.noneProvided as AnyHashable
150
+
151
+ // The view controller must be added to the view controller hierarchy to measure its content.
152
+ if window != nil {
153
+ addViewControllerIfNeeded()
154
+ }
155
+
156
+ // As of iOS 15.2, `UIHostingController` now renders updated content asynchronously, and as such
157
+ // this view will get sized incorrectly with the previous content when reused unless we invoke
158
+ // this semi-private API. We couldn't find any other method to get the view to resize
159
+ // synchronously after updating `rootView`, but hopefully this will become a internal API soon so
160
+ // we can remove this call.
161
+ viewController._render(seconds: 0)
162
+
163
+ // This is required to ensure that views with new content are properly resized.
164
+ viewController.view.invalidateIntrinsicContentSize()
165
+ }
166
+
167
+ override func layoutMarginsDidChange() {
168
+ super.layoutMarginsDidChange()
169
+
170
+ let margins = layoutMargins
171
+ switch effectiveUserInterfaceLayoutDirection {
172
+ case .rightToLeft:
173
+ epoxyEnvironment.layoutMargins = .init(
174
+ top: margins.top,
175
+ leading: margins.right,
176
+ bottom: margins.bottom,
177
+ trailing: margins.left)
178
+ case .leftToRight:
179
+ fallthrough
180
+ @unknown default:
181
+ epoxyEnvironment.layoutMargins = .init(
182
+ top: margins.top,
183
+ leading: margins.left,
184
+ bottom: margins.bottom,
185
+ trailing: margins.right)
186
+ }
187
+
188
+ // Allow the layout margins update to fully propagate through to the SwiftUI View before
189
+ // invalidating the layout.
190
+ DispatchQueue.main.async {
191
+ self.viewController.view.invalidateIntrinsicContentSize()
192
+ }
193
+ }
194
+
195
+ func handleWillDisplay(animated: Bool) {
196
+ guard state != .appeared, window != nil else { return }
197
+ transition(to: .appearing(animated: animated))
198
+ transition(to: .appeared)
199
+ }
200
+
201
+ func handleDidEndDisplaying(animated: Bool) {
202
+ guard state != .disappeared else { return }
203
+ transition(to: .disappearing(animated: animated))
204
+ transition(to: .disappeared)
205
+ }
206
+
207
+ // MARK: Private
208
+
209
+ private let viewController: EpoxySwiftUIHostingController<EpoxyHostingWrapper<RootView>>
210
+ private let epoxyContent: EpoxyHostingContent<RootView>
211
+ private let epoxyEnvironment = EpoxyHostingEnvironment()
212
+ private var dataID: AnyHashable
213
+ private var state: AppearanceState = .disappeared
214
+
215
+ /// Updates the appearance state of the `viewController`.
216
+ private func transition(to state: AppearanceState) {
217
+ guard state != self.state else { return }
218
+
219
+ // See "Handling View-Related Notifications" section for the state machine diagram.
220
+ // https://developer.apple.com/documentation/uikit/uiviewcontroller
221
+ switch (to: state, from: self.state) {
222
+ case (to: .appearing(let animated), from: .disappeared):
223
+ viewController.beginAppearanceTransition(true, animated: animated)
224
+ addViewControllerIfNeeded()
225
+ case (to: .disappearing(let animated), from: .appeared):
226
+ viewController.beginAppearanceTransition(false, animated: animated)
227
+ case (to: .disappeared, from: .disappearing):
228
+ removeViewControllerIfNeeded()
229
+ case (to: .appeared, from: .appearing):
230
+ viewController.endAppearanceTransition()
231
+ case (to: .disappeared, from: .appeared):
232
+ viewController.beginAppearanceTransition(false, animated: true)
233
+ removeViewControllerIfNeeded()
234
+ case (to: .appeared, from: .disappearing(let animated)):
235
+ viewController.beginAppearanceTransition(true, animated: animated)
236
+ viewController.endAppearanceTransition()
237
+ case (to: .disappeared, from: .appearing(let animated)):
238
+ viewController.beginAppearanceTransition(false, animated: animated)
239
+ removeViewControllerIfNeeded()
240
+ case (to: .appeared, from: .disappeared):
241
+ viewController.beginAppearanceTransition(true, animated: false)
242
+ addViewControllerIfNeeded()
243
+ viewController.endAppearanceTransition()
244
+ case (to: .appearing(let animated), from: .appeared):
245
+ viewController.beginAppearanceTransition(false, animated: animated)
246
+ viewController.beginAppearanceTransition(true, animated: animated)
247
+ case (to: .appearing(let animated), from: .disappearing):
248
+ viewController.beginAppearanceTransition(true, animated: animated)
249
+ case (to: .disappearing(let animated), from: .disappeared):
250
+ viewController.beginAppearanceTransition(true, animated: animated)
251
+ addViewControllerIfNeeded()
252
+ viewController.beginAppearanceTransition(false, animated: animated)
253
+ case (to: .disappearing(let animated), from: .appearing):
254
+ viewController.beginAppearanceTransition(false, animated: animated)
255
+ case (to: .appearing, from: .appearing),
256
+ (to: .appeared, from: .appeared),
257
+ (to: .disappearing, from: .disappearing),
258
+ (to: .disappeared, from: .disappeared):
259
+ // This should never happen since we guard on identical states.
260
+ EpoxyLogger.shared.assertionFailure("Impossible state change from \(self.state) to \(state)")
261
+ }
262
+
263
+ self.state = state
264
+ }
265
+
266
+ private func addViewControllerIfNeeded() {
267
+ // This isn't great, and means that we're going to add this view controller as a child view
268
+ // controller of a view controller somewhere else in the hierarchy, which the author of that
269
+ // view controller may not be expecting. However there's not really a better pathway forward
270
+ // here without requiring a view controller instance to be passed all the way through, which is
271
+ // both burdensome and error-prone.
272
+ guard let nextViewController = superview?.next(UIViewController.self) else {
273
+ EpoxyLogger.shared.assertionFailure(
274
+ """
275
+ Unable to add a UIHostingController view, could not locate a UIViewController in the \
276
+ responder chain for view with ID \(dataID) of type \(RootView.self).
277
+ """)
278
+ return
279
+ }
280
+
281
+ guard viewController.parent !== nextViewController else { return }
282
+
283
+ // If in a different parent, we need to first remove from it before we add.
284
+ if viewController.parent != nil {
285
+ removeViewControllerIfNeeded()
286
+ }
287
+
288
+ addViewController(to: nextViewController)
289
+
290
+ state = .appeared
291
+ }
292
+
293
+ private func addViewController(to parent: UIViewController) {
294
+ viewController.willMove(toParent: parent)
295
+
296
+ parent.addChild(viewController)
297
+
298
+ addSubview(viewController.view)
299
+
300
+ // Get the view controller's view to be sized correctly so that we don't have to wait for
301
+ // autolayout to perform a pass to do so.
302
+ viewController.view.frame = bounds
303
+
304
+ viewController.view.translatesAutoresizingMaskIntoConstraints = false
305
+ NSLayoutConstraint.activate([
306
+ viewController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
307
+ viewController.view.topAnchor.constraint(equalTo: topAnchor),
308
+ viewController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
309
+ viewController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
310
+ ])
311
+
312
+ viewController.didMove(toParent: parent)
313
+ }
314
+
315
+ private func removeViewControllerIfNeeded() {
316
+ guard viewController.parent != nil else { return }
317
+
318
+ viewController.willMove(toParent: nil)
319
+ viewController.view.removeFromSuperview()
320
+ viewController.removeFromParent()
321
+ viewController.didMove(toParent: nil)
322
+ }
323
+ }
324
+
325
+ // MARK: - AppearanceState
326
+
327
+ /// The appearance state of a `EpoxySwiftUIHostingController` contained within a
328
+ /// `EpoxySwiftUIHostingView`.
329
+ private enum AppearanceState: Equatable {
330
+ case appearing(animated: Bool)
331
+ case appeared
332
+ case disappearing(animated: Bool)
333
+ case disappeared
334
+ }
335
+
336
+ // MARK: - UIResponder
337
+
338
+ extension UIResponder {
339
+ /// Recursively traverses the responder chain upwards from this responder to its next responder
340
+ /// until the a responder of the given type is located, else returns `nil`.
341
+ @nonobjc
342
+ fileprivate func next<ResponderType>(_ type: ResponderType.Type) -> ResponderType? {
343
+ self as? ResponderType ?? next?.next(type)
344
+ }
345
+ }
346
+
347
+ // MARK: - EpoxyHostingContent
348
+
349
+ /// The object that is used to communicate changes in the root view to the
350
+ /// `EpoxySwiftUIHostingController`.
351
+ @available(iOS 13.0, tvOS 13.0, *)
352
+ final class EpoxyHostingContent<RootView: View>: ObservableObject {
353
+
354
+ // MARK: Lifecycle
355
+
356
+ init(rootView: RootView) {
357
+ _rootView = .init(wrappedValue: rootView)
358
+ }
359
+
360
+ // MARK: Internal
361
+
362
+ @Published var rootView: RootView
363
+ }
364
+
365
+ // MARK: - EpoxyHostingEnvironment
366
+
367
+ /// The object that is used to communicate values to SwiftUI views within an
368
+ /// `EpoxySwiftUIHostingController`, e.g. layout margins.
369
+ @available(iOS 13.0, tvOS 13.0, *)
370
+ final class EpoxyHostingEnvironment: ObservableObject {
371
+ @Published var layoutMargins = EdgeInsets()
372
+ @Published var intrinsicContentSizeInvalidator = EpoxyIntrinsicContentSizeInvalidator(invalidate: { })
373
+ }
374
+
375
+ // MARK: - EpoxyHostingWrapper
376
+
377
+ /// The wrapper view that is used to communicate values to SwiftUI views within an
378
+ /// `EpoxySwiftUIHostingController`, e.g. layout margins.
379
+ @available(iOS 13.0, tvOS 13.0, *)
380
+ struct EpoxyHostingWrapper<Content: View>: View {
381
+ @ObservedObject var content: EpoxyHostingContent<Content>
382
+ @ObservedObject var environment: EpoxyHostingEnvironment
383
+
384
+ var body: some View {
385
+ content.rootView
386
+ .environment(\.epoxyLayoutMargins, environment.layoutMargins)
387
+ .environment(\.epoxyIntrinsicContentSizeInvalidator, environment.intrinsicContentSizeInvalidator)
388
+ }
389
+ }
390
+
391
+ #endif
@@ -0,0 +1,44 @@
1
+ // Created by matthew_cheok on 11/19/21.
2
+ // Copyright © 2021 Airbnb Inc. All rights reserved.
3
+
4
+ import SwiftUI
5
+
6
+ // MARK: - EpoxyIntrinsicContentSizeInvalidator
7
+
8
+ /// Allows the SwiftUI view contained in an Epoxy model to request the invalidation of
9
+ /// the container's intrinsic content size.
10
+ ///
11
+ /// ```
12
+ /// @Environment(\.epoxyIntrinsicContentSizeInvalidator) var invalidateIntrinsicContentSize
13
+ ///
14
+ /// var body: some View {
15
+ /// ...
16
+ /// .onChange(of: size) {
17
+ /// invalidateIntrinsicContentSize()
18
+ /// }
19
+ /// }
20
+ /// ```
21
+ struct EpoxyIntrinsicContentSizeInvalidator {
22
+ let invalidate: () -> Void
23
+
24
+ func callAsFunction() {
25
+ invalidate()
26
+ }
27
+ }
28
+
29
+ // MARK: - EnvironmentValues
30
+
31
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
32
+ extension EnvironmentValues {
33
+ /// A means of invalidating the intrinsic content size of the parent `EpoxySwiftUIHostingView`.
34
+ var epoxyIntrinsicContentSizeInvalidator: EpoxyIntrinsicContentSizeInvalidator {
35
+ get { self[EpoxyIntrinsicContentSizeInvalidatorKey.self] }
36
+ set { self[EpoxyIntrinsicContentSizeInvalidatorKey.self] = newValue }
37
+ }
38
+ }
39
+
40
+ // MARK: - EpoxyIntrinsicContentSizeInvalidatorKey
41
+
42
+ private struct EpoxyIntrinsicContentSizeInvalidatorKey: EnvironmentKey {
43
+ static let defaultValue = EpoxyIntrinsicContentSizeInvalidator(invalidate: { })
44
+ }
@@ -0,0 +1,51 @@
1
+ // Created by eric_horacek on 10/8/21.
2
+ // Copyright © 2021 Airbnb Inc. All rights reserved.
3
+
4
+ import SwiftUI
5
+
6
+ // MARK: - View
7
+
8
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
9
+ extension View {
10
+ /// Applies the layout margins from the parent `EpoxySwiftUIHostingView` to this `View`, if there
11
+ /// are any.
12
+ ///
13
+ /// Can be used to have a background in SwiftUI underlap the safe area within a bar installer, for
14
+ /// example.
15
+ ///
16
+ /// These margins are propagated via the `EnvironmentValues.epoxyLayoutMargins`.
17
+ func epoxyLayoutMargins() -> some View {
18
+ modifier(EpoxyLayoutMarginsPadding())
19
+ }
20
+ }
21
+
22
+ // MARK: - EnvironmentValues
23
+
24
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
25
+ extension EnvironmentValues {
26
+ /// The layout margins of the parent `EpoxySwiftUIHostingView`, else zero if there is none.
27
+ var epoxyLayoutMargins: EdgeInsets {
28
+ get { self[EpoxyLayoutMarginsKey.self] }
29
+ set { self[EpoxyLayoutMarginsKey.self] = newValue }
30
+ }
31
+ }
32
+
33
+ // MARK: - EpoxyLayoutMarginsKey
34
+
35
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
36
+ private struct EpoxyLayoutMarginsKey: EnvironmentKey {
37
+ static let defaultValue = EdgeInsets()
38
+ }
39
+
40
+ // MARK: - EpoxyLayoutMarginsPadding
41
+
42
+ /// A view modifier that applies the layout margins from an enclosing `EpoxySwiftUIHostingView` to
43
+ /// the modified `View`.
44
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
45
+ private struct EpoxyLayoutMarginsPadding: ViewModifier {
46
+ @Environment(\.epoxyLayoutMargins) var epoxyLayoutMargins
47
+
48
+ func body(content: Content) -> some View {
49
+ content.padding(epoxyLayoutMargins)
50
+ }
51
+ }
@@ -0,0 +1,172 @@
1
+ // Created by eric_horacek on 9/13/21.
2
+ // Copyright © 2021 Airbnb Inc. All rights reserved.
3
+
4
+ import SwiftUI
5
+
6
+ // MARK: - StyledView
7
+
8
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
9
+ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurableView {
10
+ /// Returns a SwiftUI `View` representing this `EpoxyableView`.
11
+ ///
12
+ /// To perform additional configuration of the `EpoxyableView` instance, call `configure` on the
13
+ /// returned SwiftUI `View`:
14
+ /// ```
15
+ /// MyView.swiftUIView(…)
16
+ /// .configure { context in
17
+ /// context.view.doSomething()
18
+ /// }
19
+ /// ```
20
+ ///
21
+ /// To configure the sizing behavior of the `EpoxyableView` instance, call `sizing` on the
22
+ /// returned SwiftUI `View`:
23
+ /// ```
24
+ /// MyView.swiftUIView(…).sizing(.intrinsicSize)
25
+ /// ```
26
+ static func swiftUIView(
27
+ content: Content,
28
+ style: Style,
29
+ behaviors: Behaviors? = nil)
30
+ -> SwiftUIView<Self, (content: Content, style: Style)>
31
+ {
32
+ SwiftUIView(storage: (content: content, style: style)) {
33
+ let view = Self(style: style)
34
+ view.setContent(content, animated: false)
35
+ return view
36
+ }
37
+ .configure { context in
38
+ // We need to create a new view instance when the style changes.
39
+ if context.oldStorage.style != style {
40
+ context.view = Self(style: style)
41
+ context.view.setContent(content, animated: context.animated)
42
+ }
43
+ // Otherwise, if the just the content changes, we need to update it.
44
+ else if context.oldStorage.content != content {
45
+ context.view.setContent(content, animated: context.animated)
46
+ context.container.invalidateIntrinsicContentSize()
47
+ }
48
+
49
+ context.view.setBehaviors(behaviors)
50
+ }
51
+ }
52
+ }
53
+
54
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
55
+ extension StyledView
56
+ where
57
+ Self: ContentConfigurableView & BehaviorsConfigurableView,
58
+ Style == Never
59
+ {
60
+ /// Returns a SwiftUI `View` representing this `EpoxyableView`.
61
+ ///
62
+ /// To perform additional configuration of the `EpoxyableView` instance, call `configure` on the
63
+ /// returned SwiftUI `View`:
64
+ /// ```
65
+ /// MyView.swiftUIView(…)
66
+ /// .configure { context in
67
+ /// context.view.doSomething()
68
+ /// }
69
+ /// ```
70
+ ///
71
+ /// To configure the sizing behavior of the `EpoxyableView` instance, call `sizing` on the
72
+ /// returned SwiftUI `View`:
73
+ /// ```
74
+ /// MyView.swiftUIView(…).sizing(.intrinsicSize)
75
+ /// ```
76
+ static func swiftUIView(
77
+ content: Content,
78
+ behaviors: Behaviors? = nil)
79
+ -> SwiftUIView<Self, Content>
80
+ {
81
+ SwiftUIView(storage: content) {
82
+ let view = Self()
83
+ view.setContent(content, animated: false)
84
+ return view
85
+ }
86
+ .configure { context in
87
+ // We need to update the content of the existing view when the content is updated.
88
+ if context.oldStorage != content {
89
+ context.view.setContent(content, animated: context.animated)
90
+ context.container.invalidateIntrinsicContentSize()
91
+ }
92
+
93
+ context.view.setBehaviors(behaviors)
94
+ }
95
+ }
96
+ }
97
+
98
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
99
+ extension StyledView
100
+ where
101
+ Self: ContentConfigurableView & BehaviorsConfigurableView,
102
+ Content == Never
103
+ {
104
+ /// Returns a SwiftUI `View` representing this `EpoxyableView`.
105
+ ///
106
+ /// To perform additional configuration of the `EpoxyableView` instance, call `configure` on the
107
+ /// returned SwiftUI `View`:
108
+ /// ```
109
+ /// MyView.swiftUIView(…)
110
+ /// .configure { context in
111
+ /// context.view.doSomething()
112
+ /// }
113
+ /// ```
114
+ ///
115
+ /// To configure the sizing behavior of the `EpoxyableView` instance, call `sizing` on the
116
+ /// returned SwiftUI `View`:
117
+ /// ```
118
+ /// MyView.swiftUIView(…).sizing(.intrinsicSize)
119
+ /// ```
120
+ /// The sizing defaults to `.automatic`.
121
+ static func swiftUIView(
122
+ style: Style,
123
+ behaviors: Behaviors? = nil)
124
+ -> SwiftUIView<Self, Style>
125
+ {
126
+ SwiftUIView(storage: style) {
127
+ Self(style: style)
128
+ }
129
+ .configure { context in
130
+ // We need to create a new view instance when the style changes.
131
+ if context.oldStorage != style {
132
+ context.view = Self(style: style)
133
+ }
134
+
135
+ context.view.setBehaviors(behaviors)
136
+ }
137
+ }
138
+ }
139
+
140
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
141
+ extension StyledView
142
+ where
143
+ Self: ContentConfigurableView & BehaviorsConfigurableView,
144
+ Content == Never,
145
+ Style == Never
146
+ {
147
+ /// Returns a SwiftUI `View` representing this `EpoxyableView`.
148
+ ///
149
+ /// To perform additional configuration of the `EpoxyableView` instance, call `configure` on the
150
+ /// returned SwiftUI `View`:
151
+ /// ```
152
+ /// MyView.swiftUIView(…)
153
+ /// .configure { context in
154
+ /// context.view.doSomething()
155
+ /// }
156
+ /// ```
157
+ ///
158
+ /// To configure the sizing behavior of the `EpoxyableView` instance, call `sizing` on the
159
+ /// returned SwiftUI `View`:
160
+ /// ```
161
+ /// MyView.swiftUIView(…).sizing(.intrinsicSize)
162
+ /// ```
163
+ /// The sizing defaults to `.automatic`.
164
+ static func swiftUIView(behaviors: Behaviors? = nil) -> SwiftUIView<Self, Void> {
165
+ SwiftUIView {
166
+ Self()
167
+ }
168
+ .configure { context in
169
+ context.view.setBehaviors(behaviors)
170
+ }
171
+ }
172
+ }