@expo/ui 56.0.7 → 56.0.9

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 (208) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +44 -3
  4. package/android/src/main/java/expo/modules/ui/HostView.kt +2 -0
  5. package/android/src/main/java/expo/modules/ui/LoadingView.kt +80 -0
  6. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +50 -4
  7. package/android/src/main/java/expo/modules/ui/RNHostView.kt +8 -3
  8. package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +28 -0
  9. package/android/src/main/java/expo/modules/ui/SnackbarView.kt +126 -0
  10. package/android/src/main/java/expo/modules/ui/state/ObservableState.kt +10 -0
  11. package/assets/keyboard_arrow_down.xml +10 -0
  12. package/build/State/useNativeState.d.ts +32 -3
  13. package/build/State/useNativeState.d.ts.map +1 -1
  14. package/build/community/bottom-sheet/BottomSheet.ios.d.ts.map +1 -1
  15. package/build/community/menu/MenuView.android.d.ts +16 -0
  16. package/build/community/menu/MenuView.android.d.ts.map +1 -0
  17. package/build/community/menu/MenuView.d.ts +19 -0
  18. package/build/community/menu/MenuView.d.ts.map +1 -0
  19. package/build/community/menu/MenuView.ios.d.ts +10 -0
  20. package/build/community/menu/MenuView.ios.d.ts.map +1 -0
  21. package/build/community/menu/index.d.ts +5 -0
  22. package/build/community/menu/index.d.ts.map +1 -0
  23. package/build/community/menu/types.d.ts +166 -0
  24. package/build/community/menu/types.d.ts.map +1 -0
  25. package/build/jetpack-compose/LoadingIndicator/index.d.ts +41 -0
  26. package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -0
  27. package/build/jetpack-compose/Snackbar/index.d.ts +94 -0
  28. package/build/jetpack-compose/Snackbar/index.d.ts.map +1 -0
  29. package/build/jetpack-compose/index.d.ts +2 -0
  30. package/build/jetpack-compose/index.d.ts.map +1 -1
  31. package/build/jetpack-compose/modifiers/index.d.ts +21 -2
  32. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  33. package/build/swift-ui/Alert/index.d.ts +42 -0
  34. package/build/swift-ui/Alert/index.d.ts.map +1 -0
  35. package/build/swift-ui/BottomSheet/index.d.ts +5 -1
  36. package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
  37. package/build/swift-ui/SlotView.d.ts +5 -2
  38. package/build/swift-ui/SlotView.d.ts.map +1 -1
  39. package/build/swift-ui/SwipeActions/index.d.ts +38 -0
  40. package/build/swift-ui/SwipeActions/index.d.ts.map +1 -0
  41. package/build/swift-ui/index.d.ts +3 -0
  42. package/build/swift-ui/index.d.ts.map +1 -1
  43. package/build/swift-ui/modifiers/index.d.ts +3 -1
  44. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  45. package/build/swift-ui/modifiers/symbolEffect.d.ts +103 -0
  46. package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -0
  47. package/build/swift-ui/withAnimation.d.ts +26 -0
  48. package/build/swift-ui/withAnimation.d.ts.map +1 -0
  49. package/build/universal/BottomSheet/index.android.d.ts +1 -1
  50. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  51. package/build/universal/BottomSheet/index.d.ts +1 -1
  52. package/build/universal/BottomSheet/index.d.ts.map +1 -1
  53. package/build/universal/BottomSheet/index.ios.d.ts +1 -1
  54. package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
  55. package/build/universal/BottomSheet/types.d.ts +27 -0
  56. package/build/universal/BottomSheet/types.d.ts.map +1 -1
  57. package/build/universal/Collapsible/index.android.d.ts +8 -0
  58. package/build/universal/Collapsible/index.android.d.ts.map +1 -0
  59. package/build/universal/Collapsible/index.d.ts +8 -0
  60. package/build/universal/Collapsible/index.d.ts.map +1 -0
  61. package/build/universal/Collapsible/index.ios.d.ts +7 -0
  62. package/build/universal/Collapsible/index.ios.d.ts.map +1 -0
  63. package/build/universal/Collapsible/types.d.ts +23 -0
  64. package/build/universal/Collapsible/types.d.ts.map +1 -0
  65. package/build/universal/Column/index.d.ts.map +1 -1
  66. package/build/universal/Host/index.d.ts +5 -7
  67. package/build/universal/Host/index.d.ts.map +1 -1
  68. package/build/universal/Host/types.d.ts +72 -0
  69. package/build/universal/Host/types.d.ts.map +1 -0
  70. package/build/universal/List/index.android.d.ts +9 -0
  71. package/build/universal/List/index.android.d.ts.map +1 -0
  72. package/build/universal/List/index.d.ts +8 -0
  73. package/build/universal/List/index.d.ts.map +1 -0
  74. package/build/universal/List/index.ios.d.ts +8 -0
  75. package/build/universal/List/index.ios.d.ts.map +1 -0
  76. package/build/universal/List/types.d.ts +26 -0
  77. package/build/universal/List/types.d.ts.map +1 -0
  78. package/build/universal/ListItem/ListItem.android.d.ts +8 -0
  79. package/build/universal/ListItem/ListItem.android.d.ts.map +1 -0
  80. package/build/universal/ListItem/ListItem.d.ts +9 -0
  81. package/build/universal/ListItem/ListItem.d.ts.map +1 -0
  82. package/build/universal/ListItem/ListItem.ios.d.ts +8 -0
  83. package/build/universal/ListItem/ListItem.ios.d.ts.map +1 -0
  84. package/build/universal/ListItem/ListItemSlots.d.ts +21 -0
  85. package/build/universal/ListItem/ListItemSlots.d.ts.map +1 -0
  86. package/build/universal/ListItem/index.d.ts +10 -0
  87. package/build/universal/ListItem/index.d.ts.map +1 -0
  88. package/build/universal/ListItem/types.d.ts +59 -0
  89. package/build/universal/ListItem/types.d.ts.map +1 -0
  90. package/build/universal/Picker/Picker.android.d.ts +9 -0
  91. package/build/universal/Picker/Picker.android.d.ts.map +1 -0
  92. package/build/universal/Picker/Picker.d.ts +8 -0
  93. package/build/universal/Picker/Picker.d.ts.map +1 -0
  94. package/build/universal/Picker/Picker.ios.d.ts +9 -0
  95. package/build/universal/Picker/Picker.ios.d.ts.map +1 -0
  96. package/build/universal/Picker/PickerItem.d.ts +9 -0
  97. package/build/universal/Picker/PickerItem.d.ts.map +1 -0
  98. package/build/universal/Picker/index.d.ts +8 -0
  99. package/build/universal/Picker/index.d.ts.map +1 -0
  100. package/build/universal/Picker/types.d.ts +69 -0
  101. package/build/universal/Picker/types.d.ts.map +1 -0
  102. package/build/universal/index.d.ts +4 -0
  103. package/build/universal/index.d.ts.map +1 -1
  104. package/expo-module.config.json +1 -1
  105. package/ios/Alert/Alert.swift +56 -0
  106. package/ios/Alert/AlertProps.swift +8 -0
  107. package/ios/BottomSheetView.swift +4 -1
  108. package/ios/ExpoUIModule.swift +43 -1
  109. package/ios/ExpoUITouchHandlerHelper.h +4 -1
  110. package/ios/ExpoUITouchHandlerHelper.mm +1 -0
  111. package/ios/Modifiers/AnimationConfig.swift +109 -0
  112. package/ios/Modifiers/SwipeActionsModifier.swift +97 -0
  113. package/ios/Modifiers/SymbolEffectModifier.swift +452 -0
  114. package/ios/Modifiers/ViewModifierRegistry.swift +5 -112
  115. package/ios/SlotView.swift +5 -0
  116. package/ios/State/ObservableState.swift +12 -1
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7-sources.jar → 56.0.9/expo.modules.ui-56.0.9-sources.jar} +0 -0
  118. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.md5 +1 -0
  119. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha1 +1 -0
  120. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha256 +1 -0
  121. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9-sources.jar.sha512 +1 -0
  122. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar +0 -0
  123. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.md5 +1 -0
  124. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha1 +1 -0
  125. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha256 +1 -0
  126. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.aar.sha512 +1 -0
  127. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.module → 56.0.9/expo.modules.ui-56.0.9.module} +22 -22
  128. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.md5 +1 -0
  129. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha1 +1 -0
  130. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha256 +1 -0
  131. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.module.sha512 +1 -0
  132. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.7/expo.modules.ui-56.0.7.pom → 56.0.9/expo.modules.ui-56.0.9.pom} +1 -1
  133. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.md5 +1 -0
  134. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha1 +1 -0
  135. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha256 +1 -0
  136. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.9/expo.modules.ui-56.0.9.pom.sha512 +1 -0
  137. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  138. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  139. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  140. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  141. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  142. package/package.json +7 -3
  143. package/src/State/useNativeState.ts +70 -10
  144. package/src/community/bottom-sheet/BottomSheet.ios.tsx +0 -17
  145. package/src/community/menu/MenuView.android.tsx +224 -0
  146. package/src/community/menu/MenuView.ios.tsx +149 -0
  147. package/src/community/menu/MenuView.tsx +36 -0
  148. package/src/community/menu/index.tsx +14 -0
  149. package/src/community/menu/types.tsx +171 -0
  150. package/src/jetpack-compose/LoadingIndicator/index.tsx +92 -0
  151. package/src/jetpack-compose/Snackbar/index.tsx +135 -0
  152. package/src/jetpack-compose/index.ts +2 -0
  153. package/src/jetpack-compose/modifiers/index.ts +30 -2
  154. package/src/swift-ui/Alert/index.tsx +87 -0
  155. package/src/swift-ui/BottomSheet/index.tsx +32 -15
  156. package/src/swift-ui/SlotView.tsx +17 -4
  157. package/src/swift-ui/SwipeActions/index.tsx +73 -0
  158. package/src/swift-ui/index.tsx +3 -0
  159. package/src/swift-ui/modifiers/index.ts +3 -0
  160. package/src/swift-ui/modifiers/symbolEffect.ts +181 -0
  161. package/src/swift-ui/withAnimation.ts +71 -0
  162. package/src/ts-declarations/react-native-web.d.ts +27 -0
  163. package/src/universal/BottomSheet/index.android.tsx +27 -3
  164. package/src/universal/BottomSheet/index.ios.tsx +30 -12
  165. package/src/universal/BottomSheet/index.tsx +46 -4
  166. package/src/universal/BottomSheet/types.ts +25 -0
  167. package/src/universal/Collapsible/index.android.tsx +72 -0
  168. package/src/universal/Collapsible/index.ios.tsx +16 -0
  169. package/src/universal/Collapsible/index.tsx +58 -0
  170. package/src/universal/Collapsible/types.ts +25 -0
  171. package/src/universal/Column/index.tsx +3 -1
  172. package/src/universal/Host/index.tsx +69 -5
  173. package/src/universal/Host/types.ts +70 -0
  174. package/src/universal/List/index.android.tsx +44 -0
  175. package/src/universal/List/index.ios.tsx +19 -0
  176. package/src/universal/List/index.tsx +26 -0
  177. package/src/universal/List/types.ts +28 -0
  178. package/src/universal/ListItem/ListItem.android.tsx +52 -0
  179. package/src/universal/ListItem/ListItem.ios.tsx +58 -0
  180. package/src/universal/ListItem/ListItem.tsx +72 -0
  181. package/src/universal/ListItem/ListItemSlots.tsx +66 -0
  182. package/src/universal/ListItem/index.ts +15 -0
  183. package/src/universal/ListItem/types.ts +67 -0
  184. package/src/universal/Picker/Picker.android.tsx +69 -0
  185. package/src/universal/Picker/Picker.ios.tsx +45 -0
  186. package/src/universal/Picker/Picker.tsx +52 -0
  187. package/src/universal/Picker/PickerItem.tsx +27 -0
  188. package/src/universal/Picker/index.ts +11 -0
  189. package/src/universal/Picker/types.ts +79 -0
  190. package/src/universal/index.ts +4 -0
  191. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.md5 +0 -1
  192. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha1 +0 -1
  193. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha256 +0 -1
  194. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7-sources.jar.sha512 +0 -1
  195. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar +0 -0
  196. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.md5 +0 -1
  197. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha1 +0 -1
  198. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha256 +0 -1
  199. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.aar.sha512 +0 -1
  200. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.md5 +0 -1
  201. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha1 +0 -1
  202. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha256 +0 -1
  203. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.module.sha512 +0 -1
  204. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.md5 +0 -1
  205. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha1 +0 -1
  206. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha256 +0 -1
  207. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.7/expo.modules.ui-56.0.7.pom.sha512 +0 -1
  208. package/src/community/bottom-sheet/CLAUDE.md +0 -55
@@ -0,0 +1,97 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+ import SwiftUI
5
+
6
+ internal enum SwipeActionsEdge: String, Enumerable {
7
+ case leading
8
+ case trailing
9
+
10
+ func toNativeEdge() -> HorizontalEdge {
11
+ switch self {
12
+ case .leading:
13
+ return .leading
14
+ case .trailing:
15
+ return .trailing
16
+ }
17
+ }
18
+ }
19
+
20
+ internal final class SwipeActionsViewProps: UIBaseViewProps {
21
+ }
22
+
23
+ private struct SwipeActionsSlot {
24
+ let edge: SwipeActionsEdge
25
+ let allowsFullSwipe: Bool
26
+ let view: SlotView
27
+ }
28
+
29
+ internal struct SwipeActionsView: ExpoSwiftUI.View {
30
+ @ObservedObject var props: SwipeActionsViewProps
31
+
32
+ var body: some View {
33
+ let child = props.children?.withoutSlots().first
34
+ #if os(tvOS)
35
+ if let child {
36
+ let view: any View = child.childView
37
+ AnyView(view)
38
+ }
39
+ #else
40
+ if let child {
41
+ let view: any View = child.childView
42
+ contentWithSwipeActions(AnyView(view))
43
+ }
44
+ #endif
45
+ }
46
+
47
+ #if !os(tvOS)
48
+ @ViewBuilder
49
+ private func contentWithSwipeActions(_ content: AnyView) -> some View {
50
+ let leading = actionSlot(for: .leading)
51
+ let trailing = actionSlot(for: .trailing)
52
+
53
+ if let leading, let trailing {
54
+ content
55
+ .swipeActions(edge: .leading, allowsFullSwipe: leading.allowsFullSwipe) {
56
+ leading.view
57
+ }
58
+ .swipeActions(edge: .trailing, allowsFullSwipe: trailing.allowsFullSwipe) {
59
+ trailing.view
60
+ }
61
+ } else if let leading {
62
+ content
63
+ .swipeActions(edge: .leading, allowsFullSwipe: leading.allowsFullSwipe) {
64
+ leading.view
65
+ }
66
+ } else if let trailing {
67
+ content
68
+ .swipeActions(edge: .trailing, allowsFullSwipe: trailing.allowsFullSwipe) {
69
+ trailing.view
70
+ }
71
+ } else {
72
+ content
73
+ }
74
+ }
75
+ #endif
76
+
77
+ private func actionSlot(for edge: SwipeActionsEdge) -> SwipeActionsSlot? {
78
+ props.children?
79
+ .compactMap { $0.childView as? SlotView }
80
+ .compactMap(parseActionSlot)
81
+ .first { $0.edge == edge }
82
+ }
83
+
84
+ private func parseActionSlot(_ slot: SlotView) -> SwipeActionsSlot? {
85
+ guard slot.props.name == "actions",
86
+ let edgeName = slot.extra("edge", as: String.self),
87
+ let edge = SwipeActionsEdge(rawValue: edgeName) else {
88
+ return nil
89
+ }
90
+
91
+ return SwipeActionsSlot(
92
+ edge: edge,
93
+ allowsFullSwipe: slot.extra("allowsFullSwipe", as: Bool.self) ?? true,
94
+ view: slot
95
+ )
96
+ }
97
+ }
@@ -0,0 +1,452 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesCore
4
+ import SwiftUI
5
+
6
+ // MARK: - Effect kind
7
+
8
+ internal enum SymbolEffectKind: String, Enumerable {
9
+ case appear
10
+ case bounce
11
+ case breathe
12
+ case disappear
13
+ case drawOff
14
+ case drawOn
15
+ case pulse
16
+ case rotate
17
+ case scale
18
+ case variableColor
19
+ case wiggle
20
+ }
21
+
22
+ // MARK: - Effect option enums
23
+
24
+ internal enum SymbolEffectScope: String, Enumerable {
25
+ case byLayer
26
+ case wholeSymbol
27
+ case individually
28
+ }
29
+
30
+ internal enum SymbolEffectDirection: String, Enumerable {
31
+ case up
32
+ case down
33
+ case left
34
+ case right
35
+ case forward
36
+ case backward
37
+ case clockwise
38
+ case counterClockwise
39
+ }
40
+
41
+ internal enum SymbolEffectScale: String, Enumerable {
42
+ case up
43
+ case down
44
+ }
45
+
46
+ internal enum SymbolEffectBreatheStyle: String, Enumerable {
47
+ case plain
48
+ case pulse
49
+ }
50
+
51
+ internal enum SymbolEffectFillStyle: String, Enumerable {
52
+ case iterative
53
+ case cumulative
54
+ }
55
+
56
+ internal enum SymbolEffectPlaybackStyle: String, Enumerable {
57
+ case reversing
58
+ case nonReversing
59
+ case reversed
60
+ case nonReversed
61
+ }
62
+
63
+ internal enum SymbolEffectInactiveLayers: String, Enumerable {
64
+ case dim
65
+ case hide
66
+ }
67
+
68
+ // MARK: - Options config
69
+
70
+ internal enum SymbolEffectRepeatKind: String, Enumerable {
71
+ case continuous
72
+ case nonRepeating
73
+ case periodic
74
+ }
75
+
76
+ internal struct SymbolEffectOptionsConfig: Record {
77
+ @Field var repeatKind: SymbolEffectRepeatKind?
78
+ @Field var repeatCount: Int?
79
+ @Field var repeatDelay: Double?
80
+ @Field var speed: Double?
81
+
82
+ @available(iOS 17.0, tvOS 17.0, *)
83
+ func toSwiftUI() -> SymbolEffectOptions {
84
+ var options: SymbolEffectOptions = .default
85
+
86
+ switch repeatKind {
87
+ case .nonRepeating:
88
+ options = .nonRepeating
89
+ case .continuous:
90
+ if #available(iOS 18.0, tvOS 18.0, *) {
91
+ options = .repeat(.continuous)
92
+ }
93
+ // iOS 17: indefinite effects loop by default, so `.default` is fine here.
94
+ case .periodic:
95
+ if #available(iOS 18.0, tvOS 18.0, *) {
96
+ options = .repeat(.periodic(repeatCount, delay: repeatDelay))
97
+ }
98
+ case .none:
99
+ break
100
+ }
101
+
102
+ if let speed {
103
+ options = options.speed(speed)
104
+ }
105
+ return options
106
+ }
107
+ }
108
+
109
+ // MARK: - Effect config
110
+
111
+ internal struct SymbolEffectConfig: Record {
112
+ @Field var effect: SymbolEffectKind = .pulse
113
+ @Field var direction: SymbolEffectDirection?
114
+ @Field var scale: SymbolEffectScale?
115
+ @Field var style: SymbolEffectBreatheStyle?
116
+ @Field var customAngle: Double?
117
+ @Field var fillStyle: SymbolEffectFillStyle?
118
+ @Field var playbackStyle: SymbolEffectPlaybackStyle?
119
+ @Field var inactiveLayers: SymbolEffectInactiveLayers?
120
+ @Field var scope: SymbolEffectScope?
121
+ }
122
+
123
+ // MARK: - Modifier
124
+
125
+ internal struct SymbolEffectModifier: ViewModifier, Record {
126
+ @Field var effect: SymbolEffectConfig
127
+ @Field var options: SymbolEffectOptionsConfig?
128
+ @Field var isActive: ObservableState?
129
+ @Field var value: ObservableState?
130
+
131
+ @ViewBuilder
132
+ func body(content: Content) -> some View {
133
+ if #available(iOS 17.0, tvOS 17.0, *) {
134
+ let resolvedOptions = options?.toSwiftUI() ?? .default
135
+ if let value {
136
+ DiscreteEffectView(
137
+ config: effect,
138
+ options: resolvedOptions,
139
+ state: value
140
+ ) {
141
+ content
142
+ }
143
+ } else if let isActive {
144
+ IndefiniteEffectView(
145
+ config: effect,
146
+ options: resolvedOptions,
147
+ state: isActive
148
+ ) {
149
+ content
150
+ }
151
+ } else {
152
+ applyIndefiniteEffect(
153
+ to: content,
154
+ config: effect,
155
+ options: resolvedOptions,
156
+ isActive: true
157
+ )
158
+ }
159
+ } else {
160
+ content
161
+ }
162
+ }
163
+ }
164
+
165
+ // MARK: - Per-effect builders
166
+
167
+ @available(iOS 17.0, tvOS 17.0, *)
168
+ private func buildPulseEffect(_ config: SymbolEffectConfig) -> PulseSymbolEffect {
169
+ return switch config.scope {
170
+ case .byLayer: .pulse.byLayer
171
+ case .wholeSymbol: .pulse.wholeSymbol
172
+ default: .pulse
173
+ }
174
+ }
175
+
176
+ @available(iOS 17.0, tvOS 17.0, *)
177
+ private func buildBounceEffect(_ config: SymbolEffectConfig) -> BounceSymbolEffect {
178
+ let directed: BounceSymbolEffect = switch config.direction {
179
+ case .up: .bounce.up
180
+ case .down: .bounce.down
181
+ default: .bounce
182
+ }
183
+ return switch config.scope {
184
+ case .byLayer: directed.byLayer
185
+ case .wholeSymbol: directed.wholeSymbol
186
+ default: directed
187
+ }
188
+ }
189
+
190
+ @available(iOS 17.0, tvOS 17.0, *)
191
+ private func buildVariableColorEffect(_ config: SymbolEffectConfig) -> VariableColorSymbolEffect {
192
+ let filled: VariableColorSymbolEffect = switch config.fillStyle {
193
+ case .iterative: .variableColor.iterative
194
+ case .cumulative: .variableColor.cumulative
195
+ default: .variableColor
196
+ }
197
+ let playing: VariableColorSymbolEffect = switch config.playbackStyle {
198
+ case .reversing: filled.reversing
199
+ case .nonReversing: filled.nonReversing
200
+ default: filled
201
+ }
202
+ return switch config.inactiveLayers {
203
+ case .dim: playing.dimInactiveLayers
204
+ case .hide: playing.hideInactiveLayers
205
+ default: playing
206
+ }
207
+ }
208
+
209
+ @available(iOS 17.0, tvOS 17.0, *)
210
+ private func buildScaleEffect(_ config: SymbolEffectConfig) -> ScaleSymbolEffect {
211
+ let scaled: ScaleSymbolEffect = switch config.scale {
212
+ case .up: .scale.up
213
+ case .down: .scale.down
214
+ default: .scale
215
+ }
216
+ return switch config.scope {
217
+ case .byLayer: scaled.byLayer
218
+ case .wholeSymbol: scaled.wholeSymbol
219
+ default: scaled
220
+ }
221
+ }
222
+
223
+ @available(iOS 17.0, tvOS 17.0, *)
224
+ private func buildAppearEffect(_ config: SymbolEffectConfig) -> AppearSymbolEffect {
225
+ let scaled: AppearSymbolEffect = switch config.scale {
226
+ case .up: .appear.up
227
+ case .down: .appear.down
228
+ default: .appear
229
+ }
230
+ return switch config.scope {
231
+ case .byLayer: scaled.byLayer
232
+ case .wholeSymbol: scaled.wholeSymbol
233
+ default: scaled
234
+ }
235
+ }
236
+
237
+ @available(iOS 17.0, tvOS 17.0, *)
238
+ private func buildDisappearEffect(_ config: SymbolEffectConfig) -> DisappearSymbolEffect {
239
+ let scaled: DisappearSymbolEffect = switch config.scale {
240
+ case .up: .disappear.up
241
+ case .down: .disappear.down
242
+ default: .disappear
243
+ }
244
+ return switch config.scope {
245
+ case .byLayer: scaled.byLayer
246
+ case .wholeSymbol: scaled.wholeSymbol
247
+ default: scaled
248
+ }
249
+ }
250
+
251
+ @available(iOS 18.0, tvOS 18.0, *)
252
+ private func buildWiggleEffect(_ config: SymbolEffectConfig) -> WiggleSymbolEffect {
253
+ let directed: WiggleSymbolEffect = if let angle = config.customAngle {
254
+ .wiggle.custom(angle: angle)
255
+ } else {
256
+ switch config.direction {
257
+ case .up: .wiggle.up
258
+ case .down: .wiggle.down
259
+ case .left: .wiggle.left
260
+ case .right: .wiggle.right
261
+ case .forward: .wiggle.forward
262
+ case .backward: .wiggle.backward
263
+ case .clockwise: .wiggle.clockwise
264
+ case .counterClockwise: .wiggle.counterClockwise
265
+ default: .wiggle
266
+ }
267
+ }
268
+ return switch config.scope {
269
+ case .byLayer: directed.byLayer
270
+ case .wholeSymbol: directed.wholeSymbol
271
+ default: directed
272
+ }
273
+ }
274
+
275
+ @available(iOS 18.0, tvOS 18.0, *)
276
+ private func buildBreatheEffect(_ config: SymbolEffectConfig) -> BreatheSymbolEffect {
277
+ let styled: BreatheSymbolEffect = switch config.style {
278
+ case .pulse: .breathe.pulse
279
+ case .plain: .breathe.plain
280
+ default: .breathe
281
+ }
282
+ return switch config.scope {
283
+ case .byLayer: styled.byLayer
284
+ case .wholeSymbol: styled.wholeSymbol
285
+ default: styled
286
+ }
287
+ }
288
+
289
+ @available(iOS 18.0, tvOS 18.0, *)
290
+ private func buildRotateEffect(_ config: SymbolEffectConfig) -> RotateSymbolEffect {
291
+ let directed: RotateSymbolEffect = switch config.direction {
292
+ case .clockwise: .rotate.clockwise
293
+ case .counterClockwise: .rotate.counterClockwise
294
+ default: .rotate
295
+ }
296
+ return switch config.scope {
297
+ case .byLayer: directed.byLayer
298
+ case .wholeSymbol: directed.wholeSymbol
299
+ default: directed
300
+ }
301
+ }
302
+
303
+ @available(iOS 26.0, tvOS 26.0, *)
304
+ private func buildDrawOnEffect(_ config: SymbolEffectConfig) -> DrawOnSymbolEffect {
305
+ return switch config.scope {
306
+ case .byLayer: .drawOn.byLayer
307
+ case .individually: .drawOn.individually
308
+ case .wholeSymbol: .drawOn.wholeSymbol
309
+ default: .drawOn
310
+ }
311
+ }
312
+
313
+ @available(iOS 26.0, tvOS 26.0, *)
314
+ private func buildDrawOffEffect(_ config: SymbolEffectConfig) -> DrawOffSymbolEffect {
315
+ let played: DrawOffSymbolEffect = switch config.playbackStyle {
316
+ case .reversed: .drawOff.reversed
317
+ case .nonReversed: .drawOff.nonReversed
318
+ default: .drawOff
319
+ }
320
+ return switch config.scope {
321
+ case .byLayer: played.byLayer
322
+ case .individually: played.individually
323
+ case .wholeSymbol: played.wholeSymbol
324
+ default: played
325
+ }
326
+ }
327
+
328
+ // MARK: - Dispatch
329
+
330
+ @available(iOS 17.0, tvOS 17.0, *)
331
+ @ViewBuilder
332
+ private func applyIndefiniteEffect<TargetView: View>(
333
+ to view: TargetView,
334
+ config: SymbolEffectConfig,
335
+ options: SymbolEffectOptions,
336
+ isActive: Bool
337
+ ) -> some View {
338
+ switch config.effect {
339
+ case .pulse:
340
+ view.symbolEffect(buildPulseEffect(config), options: options, isActive: isActive)
341
+ case .bounce:
342
+ view.symbolEffect(buildBounceEffect(config), options: options, isActive: isActive)
343
+ case .variableColor:
344
+ view.symbolEffect(buildVariableColorEffect(config), options: options, isActive: isActive)
345
+ case .scale:
346
+ view.symbolEffect(buildScaleEffect(config), options: options, isActive: isActive)
347
+ case .appear:
348
+ view.symbolEffect(buildAppearEffect(config), options: options, isActive: isActive)
349
+ case .disappear:
350
+ view.symbolEffect(buildDisappearEffect(config), options: options, isActive: isActive)
351
+ case .wiggle:
352
+ if #available(iOS 18.0, tvOS 18.0, *) {
353
+ view.symbolEffect(buildWiggleEffect(config), options: options, isActive: isActive)
354
+ } else {
355
+ view
356
+ }
357
+ case .breathe:
358
+ if #available(iOS 18.0, tvOS 18.0, *) {
359
+ view.symbolEffect(buildBreatheEffect(config), options: options, isActive: isActive)
360
+ } else {
361
+ view
362
+ }
363
+ case .rotate:
364
+ if #available(iOS 18.0, tvOS 18.0, *) {
365
+ view.symbolEffect(buildRotateEffect(config), options: options, isActive: isActive)
366
+ } else {
367
+ view
368
+ }
369
+ case .drawOn:
370
+ if #available(iOS 26.0, tvOS 26.0, *) {
371
+ view.symbolEffect(buildDrawOnEffect(config), options: options, isActive: isActive)
372
+ } else {
373
+ view
374
+ }
375
+ case .drawOff:
376
+ if #available(iOS 26.0, tvOS 26.0, *) {
377
+ view.symbolEffect(buildDrawOffEffect(config), options: options, isActive: isActive)
378
+ } else {
379
+ view
380
+ }
381
+ }
382
+ }
383
+
384
+ @available(iOS 17.0, tvOS 17.0, *)
385
+ @ViewBuilder
386
+ private func applyDiscreteEffect<TargetView: View>(
387
+ to view: TargetView,
388
+ config: SymbolEffectConfig,
389
+ options: SymbolEffectOptions,
390
+ value: AnyHashable
391
+ ) -> some View {
392
+ switch config.effect {
393
+ case .pulse:
394
+ view.symbolEffect(buildPulseEffect(config), options: options, value: value)
395
+ case .bounce:
396
+ view.symbolEffect(buildBounceEffect(config), options: options, value: value)
397
+ case .variableColor:
398
+ view.symbolEffect(buildVariableColorEffect(config), options: options, value: value)
399
+ case .wiggle:
400
+ if #available(iOS 18.0, tvOS 18.0, *) {
401
+ view.symbolEffect(buildWiggleEffect(config), options: options, value: value)
402
+ } else {
403
+ view
404
+ }
405
+ case .breathe:
406
+ if #available(iOS 18.0, tvOS 18.0, *) {
407
+ view.symbolEffect(buildBreatheEffect(config), options: options, value: value)
408
+ } else {
409
+ view
410
+ }
411
+ case .rotate:
412
+ if #available(iOS 18.0, tvOS 18.0, *) {
413
+ view.symbolEffect(buildRotateEffect(config), options: options, value: value)
414
+ } else {
415
+ view
416
+ }
417
+ // Scale, Appear, Disappear, DrawOn, DrawOff don't conform to
418
+ // DiscreteSymbolEffect — they no-op when bound to a `value`.
419
+ case .scale, .appear, .disappear, .drawOn, .drawOff:
420
+ view
421
+ }
422
+ }
423
+
424
+ // MARK: - Discrete wrapper
425
+
426
+ @available(iOS 17.0, tvOS 17.0, *)
427
+ private struct DiscreteEffectView<WrappedContent: View>: View {
428
+ let config: SymbolEffectConfig
429
+ let options: SymbolEffectOptions
430
+ @ObservedObject var state: ObservableState
431
+ @ViewBuilder let content: () -> WrappedContent
432
+
433
+ var body: some View {
434
+ let trigger = (state.value as? AnyHashable) ?? AnyHashable(0)
435
+ applyDiscreteEffect(to: content(), config: config, options: options, value: trigger)
436
+ }
437
+ }
438
+
439
+ // MARK: - Indefinite wrapper
440
+
441
+ @available(iOS 17.0, tvOS 17.0, *)
442
+ private struct IndefiniteEffectView<WrappedContent: View>: View {
443
+ let config: SymbolEffectConfig
444
+ let options: SymbolEffectOptions
445
+ @ObservedObject var state: ObservableState
446
+ @ViewBuilder let content: () -> WrappedContent
447
+
448
+ var body: some View {
449
+ let active = (state.value as? Bool) ?? true
450
+ applyIndefiniteEffect(to: content(), config: config, options: options, isActive: active)
451
+ }
452
+ }
@@ -621,38 +621,12 @@ internal struct AnyViewModifier: ViewModifier {
621
621
  }
622
622
  }
623
623
 
624
- internal enum AnimationType: String, Enumerable {
625
- case easeInOut
626
- case easeIn
627
- case easeOut
628
- case linear
629
- case spring
630
- case interpolatingSpring
631
- case `default`
632
- }
633
-
634
- internal struct AnimationConfig: Record {
635
- @Field var type: AnimationType = .default
636
- @Field var duration: Double?
637
- @Field var response: Double?
638
- @Field var dampingFraction: Double?
639
- @Field var blendDuration: Double?
640
- @Field var bounce: Double?
641
- @Field var mass: Double?
642
- @Field var stiffness: Double?
643
- @Field var damping: Double?
644
- @Field var initialVelocity: Double?
645
- @Field var delay: Double?
646
- @Field var repeatCount: Int?
647
- @Field var autoreverses: Bool?
648
- }
649
-
650
624
  internal struct AnimationModifier: ViewModifier, Record {
651
625
  @Field var animation: AnimationConfig
652
626
  @Field var animatedValue: Either<Double, Bool>?
653
627
 
654
628
  func body(content: Content) -> some View {
655
- let animationValue = parseAnimation(animation)
629
+ let animationValue = animation.toSwiftUIAnimation()
656
630
  if let value: Bool = animatedValue?.get() {
657
631
  content.animation(animationValue, value: value)
658
632
  } else if let value: Double = animatedValue?.get() {
@@ -661,91 +635,6 @@ internal struct AnimationModifier: ViewModifier, Record {
661
635
  content
662
636
  }
663
637
  }
664
-
665
- private func parseAnimation(_ config: AnimationConfig) -> Animation {
666
- let type = config.type
667
-
668
- var animation: Animation
669
-
670
- switch type {
671
- case .easeIn:
672
- if let duration = config.duration {
673
- animation = .easeIn(duration: duration)
674
- } else {
675
- animation = .easeIn
676
- }
677
-
678
- case .easeOut:
679
- if let duration = config.duration {
680
- animation = .easeOut(duration: duration)
681
- } else {
682
- animation = .easeOut
683
- }
684
-
685
- case .linear:
686
- if let duration = config.duration {
687
- animation = .linear(duration: duration)
688
- } else {
689
- animation = .linear
690
- }
691
-
692
- case .easeInOut:
693
- if let duration = config.duration {
694
- animation = .easeInOut(duration: duration)
695
- } else {
696
- animation = .easeInOut
697
- }
698
-
699
- case .spring:
700
- let duration = config.duration
701
- let bounce = config.bounce
702
- let response = config.response
703
- let dampingFraction = config.dampingFraction
704
- let blendDuration = config.blendDuration
705
-
706
- if response != nil || dampingFraction != nil {
707
- // default values are 0.5, 0.825, 0.0
708
- animation = .spring(response: response ?? 0.5, dampingFraction: dampingFraction ?? 0.825, blendDuration: blendDuration ?? 0.0)
709
- } else if duration != nil || bounce != nil {
710
- // default values are 0.5, 0.0, 0.0
711
- animation = .spring(duration: duration ?? 0.5, bounce: bounce ?? 0.0, blendDuration: blendDuration ?? 0.0)
712
- } else if let blendDuration = blendDuration {
713
- animation = .spring(blendDuration: blendDuration)
714
- } else {
715
- animation = .spring
716
- }
717
-
718
- case .interpolatingSpring:
719
- let duration = config.duration
720
- let bounce = config.bounce
721
- let mass = config.mass
722
- let stiffness = config.stiffness
723
- let damping = config.damping
724
- let initialVelocity = config.initialVelocity
725
-
726
- if duration != nil || bounce != nil {
727
- animation = .interpolatingSpring(duration: duration ?? 0.5, bounce: bounce ?? 0.0, initialVelocity: initialVelocity ?? 0.0)
728
- } else if let stiffness, let damping {
729
- animation = .interpolatingSpring(mass: mass ?? 1.0, stiffness: stiffness, damping: damping, initialVelocity: initialVelocity ?? 0.0)
730
- } else {
731
- animation = .interpolatingSpring
732
- }
733
-
734
- default:
735
- animation = .default
736
- }
737
-
738
- if let delay = config.delay {
739
- animation = animation.delay(delay)
740
- }
741
-
742
- if let repeatCount = config.repeatCount {
743
- let autoreverses = config.autoreverses ?? false
744
- animation = animation.repeatCount(repeatCount, autoreverses: autoreverses)
745
- }
746
-
747
- return animation
748
- }
749
638
  }
750
639
 
751
640
  internal enum ScrollContentBackgroundTypes: String, Enumerable {
@@ -1957,5 +1846,9 @@ extension ViewModifierRegistry {
1957
1846
  register("containerBackground") { params, appContext, _ in
1958
1847
  return try ContainerBackgroundModifier(from: params, appContext: appContext)
1959
1848
  }
1849
+
1850
+ register("symbolEffect") { params, appContext, _ in
1851
+ return try SymbolEffectModifier(from: params, appContext: appContext)
1852
+ }
1960
1853
  }
1961
1854
  }
@@ -5,6 +5,7 @@ import ExpoModulesCore
5
5
 
6
6
  internal final class SlotViewProps: ExpoSwiftUI.ViewProps {
7
7
  @Field var name: String = ""
8
+ @Field var extraProps: [String: Any]?
8
9
  }
9
10
 
10
11
  internal struct SlotView: ExpoSwiftUI.View {
@@ -17,6 +18,10 @@ internal struct SlotView: ExpoSwiftUI.View {
17
18
  var body: some View {
18
19
  Children()
19
20
  }
21
+
22
+ func extra<T>(_ key: String, as type: T.Type = T.self) -> T? {
23
+ return props.extraProps?[key] as? T
24
+ }
20
25
  }
21
26
 
22
27
  extension [any ExpoSwiftUI.AnyChild] {