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,128 @@
1
+ // Created by eric_horacek on 6/22/22.
2
+ // Copyright © 2022 Airbnb Inc. All rights reserved.
3
+
4
+ import SwiftUI
5
+
6
+ // MARK: - MeasuringViewRepresentable
7
+
8
+ /// A `UIViewRepresentable` that uses a `SwiftUIMeasurementContainer` wrapping its represented
9
+ /// `UIView` to report its size that fits a proposed size to SwiftUI.
10
+ ///
11
+ /// Supports iOS 13-15 using the private `_overrideSizeThatFits(…)` method and iOS 16+ using the
12
+ /// `sizeThatFits(…)` method.
13
+ ///
14
+ /// - SeeAlso: ``SwiftUIMeasurementContainer``
15
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
16
+ protocol MeasuringViewRepresentable: ViewRepresentableType
17
+ where
18
+ RepresentableViewType == SwiftUIMeasurementContainer<Content>
19
+ {
20
+ /// The `UIView` content that's being measured by the enclosing `SwiftUIMeasurementContainer`.
21
+ associatedtype Content: ViewType
22
+
23
+ /// The sizing strategy of the represented view.
24
+ ///
25
+ /// To configure the sizing behavior of the `View` instance, call `sizing` on this `View`, e.g.:
26
+ /// ```
27
+ /// myView.sizing(.intrinsicSize)
28
+ /// ```
29
+ var sizing: SwiftUIMeasurementContainerStrategy { get set }
30
+ }
31
+
32
+ // MARK: Extensions
33
+
34
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
35
+ extension MeasuringViewRepresentable {
36
+ /// Returns a copy of this view with its sizing strategy updated to the given `sizing` value.
37
+ func sizing(_ strategy: SwiftUIMeasurementContainerStrategy) -> Self {
38
+ var copy = self
39
+ copy.sizing = strategy
40
+ return copy
41
+ }
42
+ }
43
+
44
+ // MARK: Defaults
45
+
46
+ #if os(iOS) || os(tvOS)
47
+ @available(iOS 13.0, tvOS 13.0, *)
48
+ extension MeasuringViewRepresentable {
49
+ func _overrideSizeThatFits(
50
+ _ size: inout CGSize,
51
+ in proposedSize: _ProposedSize,
52
+ uiView: UIViewType)
53
+ {
54
+ uiView.strategy = sizing
55
+
56
+ // Note: this method is not double-called on iOS 16, so we don't need to do anything to prevent
57
+ // extra work here.
58
+ let children = Mirror(reflecting: proposedSize).children
59
+
60
+ // Creates a `CGSize` by replacing `nil`s with `UIView.noIntrinsicMetric`
61
+ uiView.proposedSize = .init(
62
+ width: children.first { $0.label == "width" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric,
63
+ height: children.first { $0.label == "height" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric)
64
+
65
+ size = uiView.measuredFittingSize
66
+ }
67
+
68
+ #if swift(>=5.7) // Proxy check for being built with the iOS 15 SDK
69
+ @available(iOS 16.0, tvOS 16.0, macOS 13.0, *)
70
+ func sizeThatFits(
71
+ _ proposal: ProposedViewSize,
72
+ uiView: UIViewType,
73
+ context _: Context)
74
+ -> CGSize?
75
+ {
76
+ uiView.strategy = sizing
77
+
78
+ // Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric`
79
+ uiView.proposedSize = .init(
80
+ width: proposal.width ?? ViewType.noIntrinsicMetric,
81
+ height: proposal.height ?? ViewType.noIntrinsicMetric)
82
+
83
+ return uiView.measuredFittingSize
84
+ }
85
+ #endif
86
+ }
87
+
88
+ #elseif os(macOS)
89
+ @available(macOS 10.15, *)
90
+ extension MeasuringViewRepresentable {
91
+ func _overrideSizeThatFits(
92
+ _ size: inout CGSize,
93
+ in proposedSize: _ProposedSize,
94
+ nsView: NSViewType)
95
+ {
96
+ nsView.strategy = sizing
97
+
98
+ let children = Mirror(reflecting: proposedSize).children
99
+
100
+ // Creates a `CGSize` by replacing `nil`s with `UIView.noIntrinsicMetric`
101
+ nsView.proposedSize = .init(
102
+ width: children.first { $0.label == "width" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric,
103
+ height: children.first { $0.label == "height" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric)
104
+
105
+ size = nsView.measuredFittingSize
106
+ }
107
+
108
+ // Proxy check for being built with the macOS 13 SDK.
109
+ #if swift(>=5.7.1)
110
+ @available(macOS 13.0, *)
111
+ func sizeThatFits(
112
+ _ proposal: ProposedViewSize,
113
+ nsView: NSViewType,
114
+ context _: Context)
115
+ -> CGSize?
116
+ {
117
+ nsView.strategy = sizing
118
+
119
+ // Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric`
120
+ nsView.proposedSize = .init(
121
+ width: proposal.width ?? ViewType.noIntrinsicMetric,
122
+ height: proposal.height ?? ViewType.noIntrinsicMetric)
123
+
124
+ return nsView.measuredFittingSize
125
+ }
126
+ #endif
127
+ }
128
+ #endif
@@ -0,0 +1,452 @@
1
+ // Created by Bryn Bodayle on 1/24/22.
2
+ // Copyright © 2022 Airbnb Inc. All rights reserved.
3
+
4
+ import SwiftUI
5
+
6
+ // MARK: - SwiftUIMeasurementContainer
7
+
8
+ /// A view that has an `intrinsicContentSize` of the `uiView`'s `systemLayoutSizeFitting(…)` and
9
+ /// supports double layout pass sizing and content size category changes.
10
+ ///
11
+ /// This container view uses an injected proposed width to measure the view and return its ideal
12
+ /// height through the `SwiftUISizingContext` binding.
13
+ ///
14
+ /// - SeeAlso: ``MeasuringViewRepresentable``
15
+ @available(iOS 13.0, tvOS 13.0, macOS 10.15, *)
16
+ final class SwiftUIMeasurementContainer<Content: ViewType>: ViewType {
17
+
18
+ // MARK: Lifecycle
19
+
20
+ init(content: Content, strategy: SwiftUIMeasurementContainerStrategy) {
21
+ self.content = content
22
+ self.strategy = strategy
23
+
24
+ // On iOS 15 and below, passing zero can result in a constraint failure the first time a view
25
+ // is displayed, but the system gracefully recovers afterwards. On iOS 16, it's fine to pass
26
+ // zero.
27
+ let initialSize: CGSize
28
+ if #available(iOS 16, tvOS 16, macOS 13, *) {
29
+ initialSize = .zero
30
+ } else {
31
+ initialSize = .init(width: 375, height: 150)
32
+ }
33
+ super.init(frame: .init(origin: .zero, size: initialSize))
34
+
35
+ addSubview(content)
36
+ setUpConstraints()
37
+ }
38
+
39
+ @available(*, unavailable)
40
+ required init?(coder _: NSCoder) {
41
+ fatalError("init(coder:) has not been implemented")
42
+ }
43
+
44
+ // MARK: Internal
45
+
46
+ /// The most recently measured fitting size of the `uiView` that fits within the current
47
+ /// `proposedSize`.
48
+ ///
49
+ /// Contains `proposedSize`/`bounds.size` fallbacks for dimensions with no intrinsic size, as
50
+ /// compared to `intrinsicContentSize` which has `UIView.noIntrinsicMetric` fields in the case of
51
+ /// no intrinsic size.
52
+ var measuredFittingSize: CGSize {
53
+ _measuredFittingSize ?? measureView()
54
+ }
55
+
56
+ /// The `UIView` content that's being measured by this container.
57
+ var content: Content {
58
+ didSet {
59
+ guard content !== oldValue else { return }
60
+ oldValue.removeFromSuperview()
61
+ addSubview(content)
62
+ // Invalidate the strategy since it's derived from this view.
63
+ _resolvedStrategy = nil
64
+ // Re-configure the constraints since they depend on the resolved strategy.
65
+ setUpConstraints()
66
+ // Finally, we need to re-measure the view.
67
+ _measuredFittingSize = nil
68
+ }
69
+ }
70
+
71
+ /// The proposed size at the time of the latest measurement.
72
+ ///
73
+ /// Has a side-effect of updating the `measuredIntrinsicContentSize` if it's changed.
74
+ var proposedSize = CGSize.noIntrinsicMetric {
75
+ didSet {
76
+ guard oldValue != proposedSize else { return }
77
+ // The proposed size is only used by the measurement, so just re-measure.
78
+ _measuredFittingSize = nil
79
+ }
80
+ }
81
+
82
+ /// The measurement strategy of this container.
83
+ ///
84
+ /// Has a side-effect of updating the `measuredIntrinsicContentSize` if it's changed.
85
+ var strategy: SwiftUIMeasurementContainerStrategy {
86
+ didSet {
87
+ guard oldValue != strategy else { return }
88
+ // Invalidate the resolved strategy since it's derived from this strategy.
89
+ _resolvedStrategy = nil
90
+ // Then, re-measure the view.
91
+ _measuredFittingSize = nil
92
+ }
93
+ }
94
+
95
+ override var intrinsicContentSize: CGSize {
96
+ _intrinsicContentSize
97
+ }
98
+
99
+ #if os(macOS)
100
+ override func layout() {
101
+ super.layout()
102
+
103
+ // We need to re-measure the view whenever the size of the bounds changes, as the previous size
104
+ // may now be incorrect.
105
+ if latestMeasurementBoundsSize != nil, bounds.size != latestMeasurementBoundsSize {
106
+ // This will trigger SwiftUI to re-measure the view.
107
+ super.invalidateIntrinsicContentSize()
108
+ }
109
+ }
110
+ #else
111
+ override func layoutSubviews() {
112
+ super.layoutSubviews()
113
+
114
+ // We need to re-measure the view whenever the size of the bounds changes, as the previous size
115
+ // may now be incorrect.
116
+ if latestMeasurementBoundsSize != nil, bounds.size != latestMeasurementBoundsSize {
117
+ // This will trigger SwiftUI to re-measure the view.
118
+ super.invalidateIntrinsicContentSize()
119
+ }
120
+ }
121
+ #endif
122
+
123
+ override func invalidateIntrinsicContentSize() {
124
+ super.invalidateIntrinsicContentSize()
125
+
126
+ // Invalidate the resolved strategy in case it changes with the re-measurement as it relies on
127
+ // the intrinsic size.
128
+ _resolvedStrategy = nil
129
+ _measuredFittingSize = nil
130
+ }
131
+
132
+ // MARK: Private
133
+
134
+ /// The most recently measured intrinsic content size of the `uiView`, else `noIntrinsicMetric` if
135
+ /// it has not yet been measured.
136
+ ///
137
+ /// Contains `UIView.noIntrinsicMetric` fallbacks for dimensions with no intrinsic size,
138
+ /// as compared to `measuredFittingSize` which has `proposedSize`/`bounds.size` fallbacks.
139
+ private var _intrinsicContentSize = CGSize.noIntrinsicMetric
140
+
141
+ /// The bounds size at the time of the latest measurement.
142
+ private var latestMeasurementBoundsSize: CGSize?
143
+
144
+ /// The most recently updated set of constraints constraining `uiView` to `self`.
145
+ private var uiViewConstraints = [NSLayoutConstraint.Attribute: NSLayoutConstraint]()
146
+
147
+ /// The cached `resolvedStrategy` to prevent unnecessary re-measurements.
148
+ private var _resolvedStrategy: ResolvedSwiftUIMeasurementContainerStrategy?
149
+
150
+ /// The cached `measuredFittingSize` to prevent unnecessary re-measurements.
151
+ private var _measuredFittingSize: CGSize?
152
+
153
+ /// The resolved measurement strategy.
154
+ private var resolvedStrategy: ResolvedSwiftUIMeasurementContainerStrategy {
155
+ if let resolvedStrategy = _resolvedStrategy {
156
+ return resolvedStrategy
157
+ }
158
+
159
+ let resolved: ResolvedSwiftUIMeasurementContainerStrategy
160
+ switch strategy {
161
+ case .automatic:
162
+ // Perform an intrinsic size measurement pass, which gives us valid values for
163
+ // `UILabel.preferredMaxLayoutWidth`.
164
+ let intrinsicSize = content.systemLayoutFittingIntrinsicSize()
165
+
166
+ // If the view has a intrinsic width and contains a double layout pass subview, give it the
167
+ // proposed width to allow the label content to gracefully wrap to multiple lines.
168
+ if intrinsicSize.width > 0, content.containsDoubleLayoutPassSubviews() {
169
+ resolved = .intrinsicHeightProposedWidth
170
+ } else {
171
+ let zero = CGFloat(0)
172
+ switch (width: intrinsicSize.width, height: intrinsicSize.height) {
173
+ case (width: ...zero, height: ...zero):
174
+ resolved = .proposed
175
+ case (width: ...zero, height: zero.nextUp...):
176
+ resolved = .intrinsicHeightProposedWidth
177
+ case (width: zero.nextUp..., height: ...zero):
178
+ resolved = .intrinsicWidthProposedHeight
179
+ default:
180
+ resolved = .intrinsic(intrinsicSize)
181
+ }
182
+ }
183
+ case .proposed:
184
+ resolved = .proposed
185
+ case .intrinsicHeightProposedWidth:
186
+ resolved = .intrinsicHeightProposedWidth
187
+ case .intrinsicWidthProposedHeight:
188
+ resolved = .intrinsicWidthProposedHeight
189
+ case .intrinsic:
190
+ resolved = .intrinsic(content.systemLayoutFittingIntrinsicSize())
191
+ }
192
+ _resolvedStrategy = resolved
193
+ return resolved
194
+ }
195
+
196
+ private func setUpConstraints() {
197
+ content.translatesAutoresizingMaskIntoConstraints = false
198
+
199
+ let leading = content.leadingAnchor.constraint(equalTo: leadingAnchor)
200
+ let top = content.topAnchor.constraint(equalTo: topAnchor)
201
+ let trailing = content.trailingAnchor.constraint(equalTo: trailingAnchor)
202
+ let bottom = content.bottomAnchor.constraint(equalTo: bottomAnchor)
203
+ let newConstraints: [NSLayoutConstraint.Attribute: NSLayoutConstraint] = [
204
+ .leading: leading, .top: top, .trailing: trailing, .bottom: bottom,
205
+ ]
206
+ // Start with the lowest priority constraints so we aren't measuring the view too early, the
207
+ // priorities will be updated later on.
208
+ prioritizeConstraints(newConstraints, strategy: .intrinsic(.zero))
209
+
210
+ NSLayoutConstraint.deactivate(Array(uiViewConstraints.values))
211
+ uiViewConstraints = newConstraints
212
+ NSLayoutConstraint.activate(Array(uiViewConstraints.values))
213
+ }
214
+
215
+ /// Prioritizes the given constraints based on the provided resolved strategy.
216
+ private func prioritizeConstraints(
217
+ _ constraints: [NSLayoutConstraint.Attribute: NSLayoutConstraint],
218
+ strategy: ResolvedSwiftUIMeasurementContainerStrategy)
219
+ {
220
+ // Give a required constraint in the dimensions that are fixed to the bounds, otherwise almost
221
+ // required.
222
+ switch strategy {
223
+ case .proposed:
224
+ constraints[.trailing]?.priority = .required
225
+ constraints[.bottom]?.priority = .required
226
+ case .intrinsicHeightProposedWidth:
227
+ constraints[.trailing]?.priority = .required
228
+ constraints[.bottom]?.priority = .almostRequired
229
+ case .intrinsicWidthProposedHeight:
230
+ constraints[.trailing]?.priority = .almostRequired
231
+ constraints[.bottom]?.priority = .required
232
+ case .intrinsic:
233
+ constraints[.trailing]?.priority = .almostRequired
234
+ constraints[.bottom]?.priority = .almostRequired
235
+ }
236
+
237
+ #if os(macOS)
238
+ // On macOS, views default to having required constraints setting their height / width
239
+ // equal to their intrinsic content size. These have to be disabled in favor of the constraints
240
+ // we create here.
241
+ content.isVerticalContentSizeConstraintActive = false
242
+ content.isHorizontalContentSizeConstraintActive = false
243
+ #endif
244
+ }
245
+
246
+ /// Measures the `uiView`, storing the resulting size in `measuredIntrinsicContentSize`.
247
+ private func measureView() -> CGSize {
248
+ latestMeasurementBoundsSize = bounds.size
249
+ prioritizeConstraints(uiViewConstraints, strategy: resolvedStrategy)
250
+
251
+ var measuredSize: CGSize
252
+ let proposedSizeElseBounds = proposedSize.replacingNoIntrinsicMetric(with: bounds.size)
253
+
254
+ switch resolvedStrategy {
255
+ case .proposed:
256
+ measuredSize = .noIntrinsicMetric
257
+
258
+ case .intrinsicHeightProposedWidth:
259
+ measuredSize = content.systemLayoutFittingIntrinsicHeightFixedWidth(proposedSizeElseBounds.width)
260
+ measuredSize.width = ViewType.noIntrinsicMetric
261
+
262
+ case .intrinsicWidthProposedHeight:
263
+ measuredSize = content.systemLayoutFittingIntrinsicWidthFixedHeight(proposedSizeElseBounds.height)
264
+ measuredSize.height = ViewType.noIntrinsicMetric
265
+
266
+ case .intrinsic(let size):
267
+ measuredSize = size
268
+
269
+ // If the measured size exceeds an available width or height, set the measured size to
270
+ // `noIntrinsicMetric` to ensure that the component can be compressed, otherwise it will
271
+ // overflow beyond the proposed size.
272
+ // - If the previous intrinsic content size is the same as the new proposed size, we don't
273
+ // do this as SwiftUI sometimes "proposes" the same intrinsic size back to the component and
274
+ // we don't want that scenario to prevent size changes when there is actually more space
275
+ // available.
276
+ if
277
+ proposedSize.width != ViewType.noIntrinsicMetric,
278
+ measuredSize.width > proposedSizeElseBounds.width,
279
+ _intrinsicContentSize.width != proposedSize.width
280
+ {
281
+ measuredSize.width = ViewType.noIntrinsicMetric
282
+ }
283
+ if
284
+ proposedSize.height != ViewType.noIntrinsicMetric,
285
+ measuredSize.height > proposedSizeElseBounds.height,
286
+ _intrinsicContentSize.height != proposedSize.height
287
+ {
288
+ measuredSize.height = ViewType.noIntrinsicMetric
289
+ }
290
+ }
291
+
292
+ _intrinsicContentSize = measuredSize
293
+
294
+ let measuredFittingSize = measuredSize.replacingNoIntrinsicMetric(with: proposedSizeElseBounds)
295
+ _measuredFittingSize = measuredFittingSize
296
+ return measuredFittingSize
297
+ }
298
+ }
299
+
300
+ // MARK: - SwiftUIMeasurementContainerStrategy
301
+
302
+ /// The measurement strategy of a `SwiftUIMeasurementContainer`.
303
+ enum SwiftUIMeasurementContainerStrategy {
304
+ /// The container makes a best effort to correctly choose the measurement strategy of the view.
305
+ ///
306
+ /// The best effort is based on a number of heuristics:
307
+ /// - The `uiView` will be given its intrinsic width and/or height when measurement in that
308
+ /// dimension produces a positive value, while zero/negative values will result in that
309
+ /// dimension receiving the available space proposed by the parent.
310
+ /// - If the view contains `UILabel` subviews that require a double layout pass as determined by
311
+ /// a `preferredMaxLayoutWidth` that's greater than zero after a layout, then the view will
312
+ /// default to `intrinsicHeightProposedWidth` to allow the labels to wrap.
313
+ ///
314
+ /// If you would like to opt out of automatic sizing for performance or to override the default
315
+ /// behavior, choose another strategy.
316
+ case automatic
317
+
318
+ /// The `uiView` is sized to fill the area proposed by its parent.
319
+ ///
320
+ /// Typically used for views that should expand greedily in both axes, e.g. a background view.
321
+ case proposed
322
+
323
+ /// The `uiView` is sized with its intrinsic height and expands horizontally to fill the width
324
+ /// proposed by its parent.
325
+ ///
326
+ /// Typically used for views that have a height that's a function of their width, e.g. a row with
327
+ /// text that can wrap to multiple lines.
328
+ case intrinsicHeightProposedWidth
329
+
330
+ /// The `uiView` is sized with its intrinsic width and expands vertically to fill the height
331
+ /// proposed by its parent.
332
+ ///
333
+ /// Typically used for views that are free to grow vertically but have a fixed width, e.g. a view
334
+ /// in a horizontal carousel.
335
+ case intrinsicWidthProposedHeight
336
+
337
+ /// The `uiView` is sized to its intrinsic width and height.
338
+ ///
339
+ /// Typically used for components with a specific intrinsic size in both axes, e.g. controls or
340
+ /// inputs.
341
+ case intrinsic
342
+ }
343
+
344
+ // MARK: - ResolvedSwiftUIMeasurementContainerStrategy
345
+
346
+ /// The resolved measurement strategy of a `SwiftUIMeasurementContainer`, matching the cases of the
347
+ /// `SwiftUIMeasurementContainerStrategy` without the automatic case.
348
+ private enum ResolvedSwiftUIMeasurementContainerStrategy {
349
+ case proposed, intrinsicHeightProposedWidth, intrinsicWidthProposedHeight, intrinsic(CGSize)
350
+ }
351
+
352
+ // MARK: - UILayoutPriority
353
+
354
+ extension LayoutPriorityType {
355
+ /// An "almost required" constraint, useful for creating near-required constraints that don't
356
+ /// error when unable to be satisfied.
357
+ @nonobjc
358
+ fileprivate static var almostRequired: LayoutPriorityType { .init(rawValue: required.rawValue - 1) }
359
+ }
360
+
361
+ // MARK: - UIView
362
+
363
+ extension ViewType {
364
+ /// The `systemLayoutSizeFitting(…)` of this view with a compressed size and fitting priorities.
365
+ @nonobjc
366
+ fileprivate func systemLayoutFittingIntrinsicSize() -> CGSize {
367
+ #if os(macOS)
368
+ intrinsicContentSize
369
+ #else
370
+ systemLayoutSizeFitting(
371
+ UIView.layoutFittingCompressedSize,
372
+ withHorizontalFittingPriority: .fittingSizeLevel,
373
+ verticalFittingPriority: .fittingSizeLevel)
374
+ #endif
375
+ }
376
+
377
+ /// The `systemLayoutSizeFitting(…)` of this view with a compressed height with a fitting size
378
+ /// priority and with the given fixed width and fitting priority.
379
+ @nonobjc
380
+ fileprivate func systemLayoutFittingIntrinsicHeightFixedWidth(
381
+ _ width: CGFloat,
382
+ priority: LayoutPriorityType = .almostRequired)
383
+ -> CGSize
384
+ {
385
+ #if os(macOS)
386
+ return CGSize(width: width, height: intrinsicContentSize.height)
387
+ #else
388
+ let targetSize = CGSize(width: width, height: UIView.layoutFittingCompressedSize.height)
389
+
390
+ return systemLayoutSizeFitting(
391
+ targetSize,
392
+ withHorizontalFittingPriority: priority,
393
+ verticalFittingPriority: .fittingSizeLevel)
394
+ #endif
395
+ }
396
+
397
+ /// The `systemLayoutSizeFitting(…)` of this view with a compressed width with a fitting size
398
+ /// priority and with the given fixed height and fitting priority.
399
+ @nonobjc
400
+ fileprivate func systemLayoutFittingIntrinsicWidthFixedHeight(
401
+ _ height: CGFloat,
402
+ priority: LayoutPriorityType = .almostRequired)
403
+ -> CGSize
404
+ {
405
+ #if os(macOS)
406
+ return CGSize(width: intrinsicContentSize.width, height: height)
407
+ #else
408
+ let targetSize = CGSize(width: UIView.layoutFittingCompressedSize.width, height: height)
409
+
410
+ return systemLayoutSizeFitting(
411
+ targetSize,
412
+ withHorizontalFittingPriority: .fittingSizeLevel,
413
+ verticalFittingPriority: priority)
414
+ #endif
415
+ }
416
+
417
+ /// Whether this view or any of its subviews has a subview that has a double layout pass `UILabel`
418
+ /// as determined by a non-zero `preferredMaxLayoutWidth`, which implies that it should get a
419
+ /// `intrinsicHeightProposedWidth` sizing strategy to allow the label to wrap and grow.
420
+ @nonobjc
421
+ fileprivate func containsDoubleLayoutPassSubviews() -> Bool {
422
+ #if os(macOS)
423
+ return false
424
+ #else
425
+ var contains = false
426
+ if let label = self as? UILabel, label.preferredMaxLayoutWidth > 0 {
427
+ contains = true
428
+ }
429
+ for subview in subviews {
430
+ contains = contains || subview.containsDoubleLayoutPassSubviews()
431
+ }
432
+ return contains
433
+ #endif
434
+ }
435
+ }
436
+
437
+ // MARK: - CGSize
438
+
439
+ extension CGSize {
440
+ /// A `CGSize` with `noIntrinsicMetric` for both its width and height.
441
+ fileprivate static var noIntrinsicMetric: CGSize {
442
+ .init(width: ViewType.noIntrinsicMetric, height: ViewType.noIntrinsicMetric)
443
+ }
444
+
445
+ /// Returns a `CGSize` with its width and/or height replaced with the corresponding field of the
446
+ /// provided `fallback` size if they are `UIView.noIntrinsicMetric`.
447
+ fileprivate func replacingNoIntrinsicMetric(with fallback: CGSize) -> CGSize {
448
+ .init(
449
+ width: width == ViewType.noIntrinsicMetric ? fallback.width : width,
450
+ height: height == ViewType.noIntrinsicMetric ? fallback.height : height)
451
+ }
452
+ }