@expo/ui 0.0.1 → 0.0.2

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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/android/build.gradle +5 -13
  3. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +24 -1
  4. package/android/src/main/java/expo/modules/ui/PickerView.kt +108 -0
  5. package/android/src/main/java/expo/modules/ui/SliderView.kt +73 -0
  6. package/android/src/main/java/expo/modules/ui/SwitchView.kt +118 -0
  7. package/android/src/main/java/expo/modules/ui/Utils.kt +52 -0
  8. package/android/src/main/java/expo/modules/ui/button/Button.kt +140 -0
  9. package/android/src/main/java/expo/modules/ui/menu/ContextMenu.kt +160 -0
  10. package/android/src/main/java/expo/modules/ui/menu/ContextMenuRecords.kt +55 -0
  11. package/build/components/Button/index.d.ts +81 -0
  12. package/build/components/Button/index.d.ts.map +1 -0
  13. package/build/components/ContextMenu/index.d.ts +80 -0
  14. package/build/components/ContextMenu/index.d.ts.map +1 -0
  15. package/build/components/ContextMenu/utils.d.ts +24 -0
  16. package/build/components/ContextMenu/utils.d.ts.map +1 -0
  17. package/build/components/Picker/index.d.ts +65 -0
  18. package/build/components/Picker/index.d.ts.map +1 -0
  19. package/build/components/Section/index.d.ts +12 -0
  20. package/build/components/Section/index.d.ts.map +1 -0
  21. package/build/components/Section/index.ios.d.ts +8 -0
  22. package/build/components/Section/index.ios.d.ts.map +1 -0
  23. package/build/components/Slider/index.d.ts +54 -0
  24. package/build/components/Slider/index.d.ts.map +1 -0
  25. package/build/components/Switch/index.d.ts +101 -0
  26. package/build/components/Switch/index.d.ts.map +1 -0
  27. package/build/src/index.d.ts +13 -0
  28. package/build/src/index.d.ts.map +1 -0
  29. package/components/Button/index.tsx +132 -0
  30. package/components/ContextMenu/index.tsx +147 -0
  31. package/components/ContextMenu/utils.ts +139 -0
  32. package/components/Picker/index.tsx +83 -0
  33. package/components/Section/index.ios.tsx +58 -0
  34. package/components/Section/index.tsx +56 -0
  35. package/components/Slider/index.tsx +85 -0
  36. package/components/Switch/index.tsx +144 -0
  37. package/expo-module.config.json +3 -10
  38. package/ios/Button/Button.swift +54 -0
  39. package/ios/Button/ButtonProps.swift +43 -0
  40. package/ios/ContextMenu/ContextMenu.swift +106 -0
  41. package/ios/ContextMenu/ContextMenuRecords.swift +29 -0
  42. package/ios/ExpoUI.podspec +2 -1
  43. package/ios/ExpoUIModule.swift +6 -1
  44. package/ios/PickerView.swift +52 -0
  45. package/ios/SectionView.swift +27 -0
  46. package/ios/SliderView.swift +51 -0
  47. package/ios/SwitchView.swift +74 -0
  48. package/package.json +2 -2
  49. package/src/index.ts +22 -3
  50. package/tsconfig.json +1 -1
  51. package/android/src/main/java/expo/modules/ui/SingleChoiceSegmentedControlView.kt +0 -47
  52. package/build/ExpoUI.types.d.ts +0 -13
  53. package/build/ExpoUI.types.d.ts.map +0 -1
  54. package/build/ExpoUIModule.d.ts +0 -6
  55. package/build/ExpoUIModule.d.ts.map +0 -1
  56. package/build/ExpoUIView.d.ts +0 -4
  57. package/build/ExpoUIView.d.ts.map +0 -1
  58. package/build/index.d.ts +0 -4
  59. package/build/index.d.ts.map +0 -1
  60. package/ios/SingleChoiceSegmentedControlProps.swift +0 -10
  61. package/ios/SingleChoiceSegmentedControlView.swift +0 -29
  62. package/src/ExpoUI.types.ts +0 -8
  63. package/src/ExpoUIModule.ts +0 -5
  64. package/src/ExpoUIView.tsx +0 -10
@@ -0,0 +1,85 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ import { ViewEvent } from '../../src';
5
+
6
+ export type SliderProps = {
7
+ /**
8
+ * Custom styles for the slider component.
9
+ */
10
+ style?: StyleProp<ViewStyle>;
11
+ /**
12
+ * The current value of the slider.
13
+ * @default 0
14
+ */
15
+ value?: number;
16
+ /**
17
+ * The number of steps between the minimum and maximum values. 0 signifies infinite steps.
18
+ * @default 0
19
+ */
20
+ steps?: number;
21
+ /**
22
+ * The mininum value of the slider. Updating this value does not trigger callbacks if the current value is below `min`.
23
+ * @default 0
24
+ */
25
+ min?: number;
26
+ /**
27
+ * The maximum value of the slider. Updating this value does not trigger callbacks if the current value is above `max`.
28
+ * @default 1
29
+ */
30
+ max?: number;
31
+ /**
32
+ * Colors for slider's core elements.
33
+ * @platform android
34
+ */
35
+ elementColors?: {
36
+ thumbColor?: string;
37
+ activeTrackColor?: string;
38
+ inactiveTrackColor?: string;
39
+ activeTickColor?: string;
40
+ inactiveTickColor?: string;
41
+ };
42
+ /**
43
+ * Slider color.
44
+ */
45
+ color?: string;
46
+ /**
47
+ * Callback triggered on dragging along the slider.
48
+ */
49
+ onValueChange?: (value: number) => void;
50
+ };
51
+
52
+ type NativeSliderProps = Omit<SliderProps, 'onValueChange'> &
53
+ ViewEvent<'onValueChanged', { value: number }>;
54
+
55
+ const SliderNativeView: React.ComponentType<NativeSliderProps> = requireNativeView(
56
+ 'ExpoUI',
57
+ 'SliderView'
58
+ );
59
+
60
+ export function transformSliderProps(props: SliderProps): NativeSliderProps {
61
+ return {
62
+ ...props,
63
+ min: props.min ?? 0,
64
+ max: props.max ?? 1,
65
+ steps: props.steps ?? 0,
66
+ value: props.value ?? 0,
67
+ onValueChanged: ({ nativeEvent: { value } }) => {
68
+ props?.onValueChange?.(value);
69
+ },
70
+ elementColors: props.elementColors
71
+ ? props.elementColors
72
+ : props.color
73
+ ? {
74
+ thumbColor: props.color,
75
+ activeTrackColor: props.color,
76
+ activeTickColor: props.color,
77
+ }
78
+ : undefined,
79
+ color: props.color,
80
+ };
81
+ }
82
+
83
+ export function Slider(props: SliderProps) {
84
+ return <SliderNativeView {...transformSliderProps(props)} />;
85
+ }
@@ -0,0 +1,144 @@
1
+ import { requireNativeView } from 'expo';
2
+ import { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
3
+
4
+ type SwitchElementColors = {
5
+ /**
6
+ * Only for switch.
7
+ */
8
+ checkedThumbColor?: string;
9
+ /**
10
+ * Only for switch.
11
+ */
12
+ checkedTrackColor?: string;
13
+ /**
14
+ * Only for switch.
15
+ */
16
+ uncheckedThumbColor?: string;
17
+ /**
18
+ * Only for switch.
19
+ */
20
+ uncheckedTrackColor?: string;
21
+ };
22
+
23
+ type CheckboxElementColors = {
24
+ /**
25
+ * Only for checkbox.
26
+ */
27
+ checkedColor?: string;
28
+ /**
29
+ * Only for checkbox.
30
+ */
31
+ disabledCheckedColor?: string;
32
+ /**
33
+ * Only for checkbox.
34
+ */
35
+ uncheckedColor?: string;
36
+ /**
37
+ * Only for checkbox.
38
+ */
39
+ disabledUncheckedColor?: string;
40
+ /**
41
+ * Only for checkbox.
42
+ */
43
+ checkmarkColor?: string;
44
+ /**
45
+ * Only for checkbox.
46
+ */
47
+ disabledIndeterminateColor?: string;
48
+ };
49
+
50
+ export type SwitchProps = {
51
+ /**
52
+ * Indicates whether the switch is checked.
53
+ */
54
+ value: boolean;
55
+ /**
56
+ * Label for the switch.
57
+ *
58
+ * > On Android the label has an effect only when the `Switch` is used inside a `ContextMenu`.
59
+ * @platform ios
60
+ */
61
+ label?: string;
62
+
63
+ /**
64
+ * Type of the switch component. Can be 'checkbox', 'switch', or 'button'. The 'button' style is iOS only.
65
+ * @default 'switch'
66
+ */
67
+ variant?: 'checkbox' | 'switch' | 'button';
68
+ /**
69
+ * Callback function that is called when the checked state changes.
70
+ */
71
+ onValueChange?: (value: boolean) => void;
72
+ /**
73
+ * Optional style for the switch component.
74
+ */
75
+ style?: StyleProp<ViewStyle>;
76
+ /**
77
+ * Picker color. On iOS it only applies to the `menu` variant.
78
+ */
79
+ color?: string;
80
+ } & (
81
+ | {
82
+ variant?: 'switch';
83
+ /**
84
+ * Colors for switch's core elements.
85
+ * @platform android
86
+ */
87
+ elementColors?: SwitchElementColors;
88
+ }
89
+ | {
90
+ variant: 'checkbox';
91
+ /**
92
+ * Colors for checkbox core elements.
93
+ * @platform android
94
+ */
95
+ elementColors?: CheckboxElementColors;
96
+ }
97
+ | {
98
+ variant: 'button';
99
+ elementColors?: undefined;
100
+ }
101
+ );
102
+
103
+ type NativeSwitchProps = Omit<SwitchProps, 'onValueChange'> & {
104
+ onValueChange: (event: NativeSyntheticEvent<{ value: boolean }>) => void;
105
+ };
106
+
107
+ const SwitchNativeView: React.ComponentType<NativeSwitchProps> = requireNativeView(
108
+ 'ExpoUI',
109
+ 'SwitchView'
110
+ );
111
+
112
+ function getElementColors(props: SwitchProps) {
113
+ if (props.variant === 'button') {
114
+ return undefined;
115
+ }
116
+ if (!props.elementColors) {
117
+ if (props.variant === 'switch') {
118
+ return {
119
+ checkedTrackColor: props.color,
120
+ };
121
+ } else {
122
+ return {
123
+ checkedColor: props.color,
124
+ };
125
+ }
126
+ }
127
+ return props.elementColors;
128
+ }
129
+
130
+ export function transformSwitchProps(props: SwitchProps): NativeSwitchProps {
131
+ return {
132
+ ...props,
133
+ variant: props.variant ?? 'switch',
134
+ elementColors: getElementColors(props),
135
+ color: props.color,
136
+ onValueChange: ({ nativeEvent: { value } }) => {
137
+ props?.onValueChange?.(value);
138
+ },
139
+ } as NativeSwitchProps;
140
+ }
141
+
142
+ export function Switch(props: SwitchProps) {
143
+ return <SwitchNativeView {...transformSwitchProps(props)} />;
144
+ }
@@ -1,16 +1,9 @@
1
1
  {
2
- "platforms": [
3
- "apple",
4
- "android"
5
- ],
2
+ "platforms": ["apple", "android"],
6
3
  "android": {
7
- "modules": [
8
- "expo.modules.ui.ExpoUIModule"
9
- ]
4
+ "modules": ["expo.modules.ui.ExpoUIModule"]
10
5
  },
11
6
  "apple": {
12
- "modules": [
13
- "ExpoUIModule"
14
- ]
7
+ "modules": ["ExpoUIModule"]
15
8
  }
16
9
  }
@@ -0,0 +1,54 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ struct Button: ExpoSwiftUI.View {
7
+ @EnvironmentObject var props: ButtonProps
8
+
9
+ var body: some View {
10
+ SwiftUI.Button(
11
+ role: props.buttonRole?.toNativeRole(),
12
+ action: {
13
+ props.onButtonPressed()
14
+ },
15
+ label: {
16
+ if let systemImage = props.systemImage {
17
+ Label(props.text, systemImage: systemImage)
18
+ } else {
19
+ Text(props.text)
20
+ }
21
+ })
22
+ .tint(props.color)
23
+ // TODO: Maybe there is a way to do a switch statement similarly to the `if` extension?
24
+ .if(props.variant == .bordered, {
25
+ $0.buttonStyle(.bordered)
26
+ })
27
+ .if(props.variant == .plain, {
28
+ $0.buttonStyle(.plain)
29
+ })
30
+ .if(props.variant == .borderedProminent, {
31
+ $0.buttonStyle(.borderedProminent)
32
+ })
33
+ #if !os(tvOS)
34
+ .if(props.variant == .borderless, {
35
+ $0.buttonStyle(.borderless)
36
+ })
37
+ #endif
38
+
39
+ #if os(macOS)
40
+ .if(props.variant == .accessoryBar, {
41
+ $0.buttonStyle(.accessoryBar)
42
+ })
43
+ .if(props.variant == .accessoryBarAction, {
44
+ $0.buttonStyle(.accessoryBarAction)
45
+ })
46
+ .if(props.variant == .card, {
47
+ $0.buttonStyle(.card)
48
+ })
49
+ .if(props.variant == .link, {
50
+ $0.buttonStyle(.link)
51
+ })
52
+ #endif
53
+ }
54
+ }
@@ -0,0 +1,43 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ internal enum ButtonRole: String, Enumerable {
7
+ case `default`
8
+ case destructive
9
+ case cancel
10
+
11
+ func toNativeRole() -> SwiftUI.ButtonRole? {
12
+ switch self {
13
+ case .default:
14
+ return nil
15
+ case .destructive:
16
+ return SwiftUI.ButtonRole.destructive
17
+ case .cancel:
18
+ return SwiftUI.ButtonRole.cancel
19
+ }
20
+ }
21
+ }
22
+
23
+ internal enum ButtonVariant: String, Enumerable {
24
+ case `default`
25
+ case bordered
26
+ case accessoryBar
27
+ case accessoryBarAction
28
+ case borderedProminent
29
+ case borderless
30
+ case card
31
+ case link
32
+ case plain
33
+ }
34
+
35
+ class ButtonProps: ExpoSwiftUI.ViewProps, Observable {
36
+ required init() {}
37
+ @Field var text: String = ""
38
+ @Field var systemImage: String?
39
+ @Field var color: Color?
40
+ @Field var buttonRole: ButtonRole? = .default
41
+ @Field var variant: ButtonVariant? = .default
42
+ var onButtonPressed = EventDispatcher()
43
+ }
@@ -0,0 +1,106 @@
1
+ import SwiftUI
2
+ import ExpoModulesCore
3
+
4
+ struct MenuItems: View {
5
+ let fromElements: [ContextMenuElement]?
6
+ let props: ContextMenuProps?
7
+ // We have to create a non-functional shadow node proxy, so that the elements don't send sizing changes to the
8
+ // root proxy - we won't be leaving the SwiftUI hierarchy.
9
+ let shadowNodeProxy = ExpoSwiftUI.ShadowNodeProxy()
10
+
11
+ init(fromElements: [ContextMenuElement]?, props: ContextMenuProps?) {
12
+ self.fromElements = fromElements
13
+ self.props = props
14
+
15
+ fromElements?.forEach { element in
16
+ let id = element.contextMenuElementID
17
+ if let button = element.button {
18
+ button.onButtonPressed.onEventSent = { _ in
19
+ props?.onContextMenuButtonPressed(addId(id, toMap: nil))
20
+ }
21
+ }
22
+ if let `switch` = element.switch {
23
+ `switch`.onValueChange.onEventSent = { map in
24
+ props?.onContextMenuSwitchCheckedChanged(addId(id, toMap: map))
25
+ }
26
+ }
27
+ if let picker = element.picker {
28
+ picker.onOptionSelected.onEventSent = { map in
29
+ props?.onContextMenuPickerOptionSelected(addId(id, toMap: map))
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ var body: some View {
36
+ ForEach(fromElements ?? []) { elem in
37
+ if let button = elem.button {
38
+ ExpoUI.Button().environmentObject(button)
39
+ }
40
+
41
+ if let picker = elem.picker {
42
+ ExpoUI.PickerView().environmentObject(picker)
43
+ }
44
+
45
+ if let `switch` = elem.switch {
46
+ ExpoUI.SwitchView().environmentObject(`switch`).environmentObject(shadowNodeProxy)
47
+ }
48
+
49
+ if let submenu = elem.submenu {
50
+ SinglePressContextMenu(
51
+ elements: submenu.elements,
52
+ activationElement: ExpoUI.Button().environmentObject(submenu.button),
53
+ props: props
54
+ )
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ struct SinglePressContextMenu<ActivationElement: View>: View {
61
+ let elements: [ContextMenuElement]?
62
+ let activationElement: ActivationElement
63
+ let props: ContextMenuProps?
64
+
65
+ var body: some View {
66
+ #if !os(tvOS)
67
+ SwiftUI.Menu {
68
+ MenuItems(fromElements: elements, props: props)
69
+ } label: {
70
+ activationElement
71
+ }
72
+ #else
73
+ Text("SinglePressContextMenu is not supported on this platform")
74
+ #endif
75
+ }
76
+ }
77
+
78
+ struct LongPressContextMenu<ActivationElement: View>: View {
79
+ let elements: [ContextMenuElement]?
80
+ let activationElement: ActivationElement
81
+ let props: ContextMenuProps?
82
+
83
+ var body: some View {
84
+ activationElement.contextMenu(menuItems: {
85
+ MenuItems(fromElements: elements, props: props)
86
+ })
87
+ }
88
+ }
89
+
90
+ struct ContextMenu: ExpoSwiftUI.View {
91
+ @EnvironmentObject var props: ContextMenuProps
92
+
93
+ var body: some View {
94
+ if props.activationMethod == .singlePress {
95
+ SinglePressContextMenu(elements: props.elements, activationElement: Children(), props: props)
96
+ } else {
97
+ LongPressContextMenu(elements: props.elements, activationElement: Children(), props: props)
98
+ }
99
+ }
100
+ }
101
+
102
+ private func addId(_ id: String?, toMap initialMap: [String: Any]?) -> [String: Any] {
103
+ var newMap = initialMap ?? [:]
104
+ newMap["contextMenuElementID"] = id
105
+ return newMap
106
+ }
@@ -0,0 +1,29 @@
1
+ import ExpoModulesCore
2
+
3
+ internal enum ActivationMethod: String, Enumerable {
4
+ case singlePress
5
+ case longPress
6
+ }
7
+
8
+ internal class Submenu: Record, Identifiable {
9
+ required init() { }
10
+ @Field var elements: [ContextMenuElement]
11
+ @Field var button: ButtonProps
12
+ }
13
+
14
+ internal class ContextMenuElement: Record, Identifiable {
15
+ required init() { }
16
+ @Field var button: ButtonProps?
17
+ @Field var picker: PickerProps?
18
+ @Field var `switch`: SwitchProps?
19
+ @Field var submenu: Submenu?
20
+ @Field var contextMenuElementID: String?
21
+ }
22
+
23
+ internal class ContextMenuProps: ExpoSwiftUI.ViewProps {
24
+ @Field var elements: [ContextMenuElement]
25
+ var onContextMenuButtonPressed = EventDispatcher()
26
+ var onContextMenuPickerOptionSelected = EventDispatcher()
27
+ var onContextMenuSwitchCheckedChanged = EventDispatcher()
28
+ @Field var activationMethod: ActivationMethod? = .singlePress
29
+ }
@@ -11,7 +11,8 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
13
  s.platforms = {
14
- :ios => '15.1'
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
15
16
  }
16
17
  s.swift_version = '5.4'
17
18
  s.source = { git: 'https://github.com/expo/expo.git' }
@@ -6,6 +6,11 @@ public class ExpoUIModule: Module {
6
6
  public func definition() -> ModuleDefinition {
7
7
  Name("ExpoUI")
8
8
 
9
- View(SingleChoiceSegmentedControlView.self)
9
+ View(Button.self)
10
+ View(PickerView.self)
11
+ View(SwitchView.self)
12
+ View(SectionView.self)
13
+ View(SliderView.self)
14
+ View(ExpoUI.ContextMenu.self)
10
15
  }
11
16
  }
@@ -0,0 +1,52 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ class PickerProps: ExpoSwiftUI.ViewProps {
7
+ @Field var options: [String] = []
8
+ @Field var selectedIndex: Int?
9
+ @Field var variant: String?
10
+ @Field var label: String?
11
+ @Field var color: Color?
12
+ var onOptionSelected = EventDispatcher()
13
+ }
14
+
15
+ struct PickerView: ExpoSwiftUI.View {
16
+ @State var selection: Int?
17
+ @State var prevSelectedIndex: Int?
18
+ @EnvironmentObject var props: PickerProps
19
+
20
+ var body: some View {
21
+ if #available(iOS 17.0, tvOS 17.0, *) {
22
+ Picker(props.label ?? "", selection: $selection) {
23
+ ForEach(Array(props.options.enumerated()), id: \.element) { index, option in
24
+ Text(option).tag(index)
25
+ }
26
+ }
27
+ .tint(props.color)
28
+ #if !os(tvOS)
29
+ .if(props.variant == "wheel", { $0.pickerStyle(.wheel) })
30
+ #endif
31
+ .if(props.variant == "segmented", { $0.pickerStyle(.segmented) })
32
+ .if(props.variant == "menu", { $0.pickerStyle(.menu) })
33
+ .onChange(of: selection, perform: { newValue in
34
+ if props.selectedIndex == newValue {
35
+ return
36
+ }
37
+ let payload = [
38
+ "index": newValue ?? 0,
39
+ "label": props.options[newValue ?? 0]
40
+ ]
41
+ props.onOptionSelected(payload)
42
+ })
43
+ .onReceive(props.selectedIndex.publisher, perform: { newValue in
44
+ if prevSelectedIndex == newValue {
45
+ return
46
+ }
47
+ selection = newValue
48
+ prevSelectedIndex = newValue
49
+ })
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ class SectionProps: ExpoSwiftUI.ViewProps {
7
+ @Field var title: String?
8
+ @Field var heightOffset: CGFloat = 0
9
+ }
10
+
11
+ struct SectionView: ExpoSwiftUI.View {
12
+ @EnvironmentObject var props: SectionProps
13
+
14
+ var body: some View {
15
+ let form = Form {
16
+ Section(header: Text(props.title ?? "")) {
17
+ Children().padding(EdgeInsets(top: 0, leading: 0, bottom: props.heightOffset, trailing: 0))
18
+ }
19
+ }
20
+
21
+ if #available(iOS 16.0, tvOS 16.0, *) {
22
+ form.scrollDisabled(true)
23
+ } else {
24
+ form
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import SwiftUI
4
+ import ExpoModulesCore
5
+
6
+ class SliderProps: ExpoSwiftUI.ViewProps {
7
+ @Field var value: Float?
8
+ @Field var steps: Int = 0
9
+ @Field var min: Float = 0.0
10
+ @Field var max: Float = 1.0
11
+ @Field var color: Color?
12
+ var onValueChanged = EventDispatcher()
13
+ }
14
+
15
+ func getStep(_ min: Float, _ max: Float, _ steps: Int) -> Float {
16
+ if steps == 0 {
17
+ // Continous (no steps)
18
+ return 0.00001
19
+ }
20
+ // Matching Jetpack Compose where steps is the number of discreete points
21
+ return (max - min) / Float(steps + 1)
22
+ }
23
+
24
+ struct SliderView: ExpoSwiftUI.View {
25
+ @EnvironmentObject var props: SliderProps
26
+ @State var value: Float = 0.0
27
+
28
+ var body: some View {
29
+ #if !os(tvOS)
30
+ Slider(value: $value, in: props.min...props.max, step: getStep(props.min, props.max, props.steps) )
31
+ .onChange(of: value, perform: { newValue in
32
+ if props.value == newValue {
33
+ return
34
+ }
35
+ // TODO: onChange(of: Float) action tried to update multiple times per frame.
36
+ props.onValueChanged([
37
+ "value": newValue
38
+ ])
39
+ })
40
+ .tint(props.color)
41
+ .onReceive(props.value.publisher, perform: { newValue in
42
+ var sliderValue = newValue
43
+ sliderValue = max(sliderValue, props.min)
44
+ sliderValue = min(sliderValue, props.max)
45
+ value = sliderValue
46
+ })
47
+ #else
48
+ Text("Slider not supported on this platform")
49
+ #endif
50
+ }
51
+ }