@likith99/react-native-adaptive-liquid-glass 0.1.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 (77) hide show
  1. package/AdaptiveLiquidGlass.podspec +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/android/build.gradle +21 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/adaptiveglass/ALGMenuManager.kt +31 -0
  7. package/android/src/main/java/com/adaptiveglass/ALGMenuView.kt +184 -0
  8. package/android/src/main/java/com/adaptiveglass/ALGSliderManager.kt +32 -0
  9. package/android/src/main/java/com/adaptiveglass/ALGSliderView.kt +132 -0
  10. package/android/src/main/java/com/adaptiveglass/ALGTabsManager.kt +24 -0
  11. package/android/src/main/java/com/adaptiveglass/ALGTabsView.kt +161 -0
  12. package/android/src/main/java/com/adaptiveglass/AdaptiveLiquidGlassPackage.kt +11 -0
  13. package/android/src/main/res/drawable/alg_tab_circle.xml +3 -0
  14. package/android/src/main/res/drawable/alg_tab_favorites.xml +3 -0
  15. package/android/src/main/res/drawable/alg_tab_home.xml +3 -0
  16. package/android/src/main/res/drawable/alg_tab_inbox.xml +3 -0
  17. package/android/src/main/res/drawable/alg_tab_library.xml +3 -0
  18. package/android/src/main/res/drawable/alg_tab_search.xml +3 -0
  19. package/android/src/main/res/drawable/alg_tab_settings.xml +3 -0
  20. package/ios/ALGActionClusterComponentView.h +3 -0
  21. package/ios/ALGActionClusterComponentView.mm +47 -0
  22. package/ios/ALGActionClusterView.swift +252 -0
  23. package/ios/ALGButtonComponentView.h +3 -0
  24. package/ios/ALGButtonComponentView.mm +37 -0
  25. package/ios/ALGButtonView.swift +113 -0
  26. package/ios/ALGMenuComponentView.h +3 -0
  27. package/ios/ALGMenuComponentView.mm +34 -0
  28. package/ios/ALGMenuView.swift +221 -0
  29. package/ios/ALGSegmentedComponentView.h +3 -0
  30. package/ios/ALGSegmentedComponentView.mm +36 -0
  31. package/ios/ALGSegmentedView.swift +58 -0
  32. package/ios/ALGSliderComponentView.h +3 -0
  33. package/ios/ALGSliderComponentView.mm +47 -0
  34. package/ios/ALGSliderView.swift +102 -0
  35. package/ios/ALGSurfaceComponentView.h +3 -0
  36. package/ios/ALGSurfaceComponentView.mm +45 -0
  37. package/ios/ALGSurfaceView.swift +108 -0
  38. package/ios/ALGTabsComponentView.h +3 -0
  39. package/ios/ALGTabsComponentView.mm +33 -0
  40. package/ios/ALGTabsView.swift +194 -0
  41. package/package.json +73 -0
  42. package/react-native.config.js +3 -0
  43. package/src/GlassActionCluster.ios.tsx +18 -0
  44. package/src/GlassActionCluster.tsx +1 -0
  45. package/src/GlassButton.ios.tsx +18 -0
  46. package/src/GlassButton.tsx +7 -0
  47. package/src/GlassContainer.ios.tsx +9 -0
  48. package/src/GlassContainer.tsx +6 -0
  49. package/src/GlassMenuButton.tsx +26 -0
  50. package/src/GlassPressable.tsx +17 -0
  51. package/src/GlassSegmentedControl.ios.tsx +21 -0
  52. package/src/GlassSegmentedControl.tsx +1 -0
  53. package/src/GlassSlider.tsx +24 -0
  54. package/src/GlassTabBar.tsx +25 -0
  55. package/src/GlassToolbar.tsx +25 -0
  56. package/src/GlassView.ios.tsx +13 -0
  57. package/src/GlassView.tsx +1 -0
  58. package/src/adaptiveHeight.ts +15 -0
  59. package/src/fallback/GlassActionCluster.tsx +25 -0
  60. package/src/fallback/GlassSegmentedControl.tsx +26 -0
  61. package/src/fallback/GlassView.tsx +12 -0
  62. package/src/fallback/NativeGlassButton.tsx +21 -0
  63. package/src/index.ts +17 -0
  64. package/src/menuTree.ts +35 -0
  65. package/src/sliderMath.ts +12 -0
  66. package/src/specs/ALGActionClusterNativeComponent.ts +15 -0
  67. package/src/specs/ALGButtonNativeComponent.ts +15 -0
  68. package/src/specs/ALGMenuNativeComponent.ts +17 -0
  69. package/src/specs/ALGSegmentedNativeComponent.ts +12 -0
  70. package/src/specs/ALGSliderNativeComponent.ts +19 -0
  71. package/src/specs/ALGSurfaceNativeComponent.ts +13 -0
  72. package/src/specs/ALGTabsNativeComponent.ts +11 -0
  73. package/src/support.ts +5 -0
  74. package/src/types.ts +181 -0
  75. package/src/validateActions.ts +10 -0
  76. package/src/validateSegments.ts +11 -0
  77. package/src/validateTabs.ts +17 -0
@@ -0,0 +1,113 @@
1
+ import UIKit
2
+ import SwiftUI
3
+
4
+ private final class ButtonModel: ObservableObject {
5
+ @Published var title = ""
6
+ @Published var symbol = ""
7
+ @Published var prominent = false
8
+ @Published var disabled = false
9
+ @Published var loading = false
10
+ @Published var tint: UIColor?
11
+ @Published var label = ""
12
+ @Published var hint = ""
13
+ @Published var identifier = ""
14
+ var activate: () -> Void = {}
15
+ }
16
+
17
+ @available(iOS 26.0, *)
18
+ private struct NativeButtonContent: View {
19
+ @ObservedObject var model: ButtonModel
20
+ @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
21
+
22
+ private var button: some View {
23
+ Button {
24
+ if !model.disabled && !model.loading { model.activate() }
25
+ } label: {
26
+ HStack(spacing: 8) {
27
+ if model.loading { ProgressView().accessibilityHidden(true) }
28
+ else if !model.symbol.isEmpty { Image(systemName: model.symbol).accessibilityHidden(true) }
29
+ Text(model.title)
30
+ }
31
+ .frame(maxWidth: .infinity)
32
+ }
33
+ .disabled(model.disabled || model.loading)
34
+ .accessibilityLabel(model.label.isEmpty ? model.title : model.label)
35
+ .accessibilityHint(model.hint)
36
+ .accessibilityValue(model.loading ? "Loading" : "")
37
+ .accessibilityIdentifier(model.identifier)
38
+ }
39
+
40
+ var body: some View {
41
+ Group {
42
+ if reduceTransparency {
43
+ if model.prominent { button.buttonStyle(.borderedProminent) }
44
+ else { button.buttonStyle(.bordered) }
45
+ } else if model.prominent {
46
+ button.buttonStyle(.glassProminent)
47
+ } else {
48
+ button.buttonStyle(.glass)
49
+ }
50
+ }
51
+ .controlSize(.large)
52
+ .tint(model.tint.map { Color(uiColor: $0) } ?? .accentColor)
53
+ .padding(.horizontal, 4)
54
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
55
+ }
56
+ }
57
+
58
+ @objc(ALGButtonView)
59
+ public final class ALGButtonView: UIView {
60
+ private let model = ButtonModel()
61
+ private var host: UIViewController?
62
+ @objc public var onActivate: (() -> Void)?
63
+
64
+ public override init(frame: CGRect) {
65
+ super.init(frame: frame)
66
+ model.activate = { [weak self] in self?.onActivate?() }
67
+ if #available(iOS 26.0, *) {
68
+ let controller = UIHostingController(rootView: NativeButtonContent(model: model))
69
+ controller.view.backgroundColor = .clear
70
+ host = controller
71
+ addSubview(controller.view)
72
+ }
73
+ }
74
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
75
+
76
+ @objc public func configure(_ title: String, symbol: String, prominent: Bool, disabled: Bool,
77
+ loading: Bool, tint: UIColor?, scheme: String, label: String, hint: String, identifier: String) {
78
+ model.title = title
79
+ model.symbol = symbol
80
+ model.prominent = prominent
81
+ model.disabled = disabled
82
+ model.loading = loading
83
+ model.tint = tint
84
+ model.label = label
85
+ model.hint = hint
86
+ model.identifier = identifier
87
+ overrideUserInterfaceStyle = scheme == "dark" ? .dark : scheme == "light" ? .light : .unspecified
88
+ }
89
+
90
+ public override func layoutSubviews() { super.layoutSubviews(); host?.view.frame = bounds }
91
+ public override func didMoveToWindow() {
92
+ super.didMoveToWindow()
93
+ guard let host else { return }
94
+ if window == nil {
95
+ host.willMove(toParent: nil)
96
+ host.removeFromParent()
97
+ return
98
+ }
99
+ var responder: UIResponder? = superview
100
+ while let current = responder {
101
+ if let parent = current as? UIViewController {
102
+ if host.parent !== parent {
103
+ host.willMove(toParent: nil)
104
+ host.removeFromParent()
105
+ parent.addChild(host)
106
+ host.didMove(toParent: parent)
107
+ }
108
+ break
109
+ }
110
+ responder = current.next
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ @interface ALGMenuComponentView : RCTViewComponentView
3
+ @end
@@ -0,0 +1,34 @@
1
+ #import "ALGMenuComponentView.h"
2
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/ComponentDescriptors.h>
3
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/Props.h>
4
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/EventEmitters.h>
5
+ #import <React/RCTConversions.h>
6
+ #import "AdaptiveLiquidGlass-Swift.h"
7
+ using namespace facebook::react;
8
+ @implementation ALGMenuComponentView {
9
+ ALGMenuView *_menu;
10
+ }
11
+ + (ComponentDescriptorProvider)componentDescriptorProvider { return concreteComponentDescriptorProvider<ALGMenuComponentDescriptor>(); }
12
+ - (instancetype)initWithFrame:(CGRect)frame {
13
+ if (self = [super initWithFrame:frame]) {
14
+ _props = std::make_shared<const ALGMenuProps>();
15
+ _menu = [[ALGMenuView alloc] initWithFrame:CGRectZero];
16
+ self.contentView = _menu;
17
+ __weak ALGMenuComponentView *weakSelf = self;
18
+ _menu.onAction = ^(NSString *identifier) {
19
+ ALGMenuComponentView *self = weakSelf;
20
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGMenuEventEmitter>(self->_eventEmitter)->onMenuAction({std::string(identifier.UTF8String)});
21
+ };
22
+ }
23
+ return self;
24
+ }
25
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
26
+ const auto &p = *std::static_pointer_cast<const ALGMenuProps>(props);
27
+ [_menu configure:@(p.title.c_str()) itemsJSON:@(p.itemsJSON.c_str()) symbol:@(p.systemImage.c_str())
28
+ disabled:p.disabled tint:RCTUIColorFromSharedColor(p.glassTint) forceFallback:p.forceFallback
29
+ label:@(p.controlLabel.c_str()) hint:@(p.controlHint.c_str()) identifier:@(p.controlTestID.c_str())
30
+ toolbar:p.toolbar maxVisibleItems:p.maxVisibleItems mergingEnabled:p.mergingEnabled];
31
+ [super updateProps:props oldProps:oldProps];
32
+ }
33
+ + (BOOL)shouldBeRecycled { return NO; }
34
+ @end
@@ -0,0 +1,221 @@
1
+ import UIKit
2
+
3
+ private struct MenuItem: Decodable, Equatable {
4
+ let id: String
5
+ let title: String
6
+ let kind: String?
7
+ let systemImage: String?
8
+ let disabled: Bool?
9
+ let destructive: Bool?
10
+ let checked: Bool?
11
+ let items: [MenuItem]?
12
+ let placement: String?
13
+ var isGroup: Bool { kind == "section" || kind == "submenu" }
14
+ }
15
+
16
+ @objc(ALGMenuView)
17
+ public final class ALGMenuView: UIView {
18
+ private let button = UIButton(type: .system)
19
+ private let bar = UIToolbar()
20
+ private var items: [MenuItem] = []
21
+ private var lastJSON = ""
22
+ private var revision = 0
23
+ private var title = ""
24
+ private var symbol = ""
25
+ private var tint: UIColor?
26
+ private var forceFallback = false
27
+ private var disabled = false
28
+ private var toolbar = false
29
+ private var maxVisibleItems = 3
30
+ private var mergingEnabled = false
31
+ private var identifier = ""
32
+ private var configured = false
33
+ private var needsBarUpdate = true
34
+ private var lastWidth: CGFloat = -1
35
+ @objc public var onAction: ((String) -> Void)?
36
+
37
+ public override init(frame: CGRect) {
38
+ super.init(frame: frame)
39
+ addSubview(button)
40
+ addSubview(bar)
41
+ bar.isHidden = true
42
+ button.showsMenuAsPrimaryAction = true
43
+ button.changesSelectionAsPrimaryAction = false
44
+ // Keeping the authored order needs iOS 16; below it UIKit picks the order itself.
45
+ if #available(iOS 16.0, *) { button.preferredMenuElementOrder = .fixed }
46
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
47
+ NotificationCenter.default.addObserver(self, selector: #selector(updateAppearance),
48
+ name: UIAccessibility.reduceTransparencyStatusDidChangeNotification, object: nil)
49
+ NotificationCenter.default.addObserver(self, selector: #selector(updateAppearance),
50
+ name: UIContentSizeCategory.didChangeNotification, object: nil)
51
+ }
52
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
53
+ deinit { NotificationCenter.default.removeObserver(self) }
54
+
55
+ @objc public func configure(_ title: String, itemsJSON: String, symbol: String, disabled: Bool,
56
+ tint: UIColor?, forceFallback: Bool, label: String, hint: String, identifier: String,
57
+ toolbar: Bool, maxVisibleItems: Int, mergingEnabled: Bool) {
58
+ let appearanceChanged = !configured || self.title != title || self.symbol != symbol || self.tint != tint || self.forceFallback != forceFallback
59
+ let structureChanged = lastJSON != itemsJSON || self.toolbar != toolbar || self.disabled != disabled ||
60
+ self.maxVisibleItems != maxVisibleItems || self.mergingEnabled != mergingEnabled || self.identifier != identifier
61
+ self.title = title; self.symbol = symbol; self.tint = tint; self.forceFallback = forceFallback
62
+ self.disabled = disabled; self.toolbar = toolbar; self.maxVisibleItems = maxVisibleItems
63
+ self.mergingEnabled = mergingEnabled; self.identifier = identifier
64
+ if lastJSON != itemsJSON {
65
+ lastJSON = itemsJSON
66
+ items = (try? JSONDecoder().decode([MenuItem].self, from: Data(itemsJSON.utf8))) ?? []
67
+ }
68
+ if structureChanged || appearanceChanged {
69
+ revision += 1
70
+ dismissMenus()
71
+ button.menu = UIMenu(children: elements(items, revision: revision))
72
+ needsBarUpdate = true
73
+ }
74
+ button.isHidden = toolbar; bar.isHidden = !toolbar
75
+ button.isEnabled = !disabled && !items.isEmpty
76
+ button.accessibilityLabel = label.isEmpty ? title : label
77
+ button.accessibilityHint = hint
78
+ button.accessibilityIdentifier = identifier
79
+ bar.accessibilityIdentifier = identifier
80
+ // Keep individual native controls exposed to accessibility.
81
+ bar.isAccessibilityElement = false
82
+ if appearanceChanged { updateAppearance() }
83
+ configured = true
84
+ setNeedsLayout()
85
+ }
86
+
87
+ private func enabledAction(_ id: String, in nodes: [MenuItem]) -> MenuItem? {
88
+ for item in nodes where item.disabled != true {
89
+ if item.isGroup {
90
+ if let found = enabledAction(id, in: item.items ?? []) { return found }
91
+ } else if item.id == id { return item }
92
+ }
93
+ return nil
94
+ }
95
+ private func action(_ item: MenuItem, revision: Int) -> UIAction {
96
+ var attributes: UIMenuElement.Attributes = []
97
+ if item.disabled == true || item.isGroup { attributes.insert(.disabled) }
98
+ if item.destructive == true { attributes.insert(.destructive) }
99
+ return UIAction(title: item.title, image: item.systemImage.flatMap { UIImage(systemName: $0) },
100
+ identifier: UIAction.Identifier(item.id), attributes: attributes, state: item.checked == true ? .on : .off) { [weak self] _ in
101
+ guard let self, self.window != nil, !self.disabled, self.revision == revision,
102
+ self.enabledAction(item.id, in: self.items) != nil else { return }
103
+ self.onAction?(item.id)
104
+ }
105
+ }
106
+ private func elements(_ nodes: [MenuItem], revision: Int) -> [UIMenuElement] {
107
+ nodes.map { item in
108
+ if item.isGroup && item.disabled != true {
109
+ return UIMenu(title: item.title, image: item.systemImage.flatMap { UIImage(systemName: $0) },
110
+ identifier: UIMenu.Identifier(item.id), options: item.kind == "section" ? [.displayInline] : [],
111
+ children: elements(item.items ?? [], revision: revision))
112
+ }
113
+ return action(item, revision: revision)
114
+ }
115
+ }
116
+ @objc private func updateAppearance() {
117
+ var configuration: UIButton.Configuration
118
+ let appearance = UIToolbarAppearance()
119
+ if #available(iOS 26.0, *), !forceFallback, !UIAccessibility.isReduceTransparencyEnabled {
120
+ configuration = .glass()
121
+ appearance.configureWithDefaultBackground()
122
+ bar.isTranslucent = true
123
+ } else {
124
+ configuration = .tinted()
125
+ appearance.configureWithOpaqueBackground()
126
+ bar.isTranslucent = false
127
+ }
128
+ configuration.title = title
129
+ configuration.image = symbol.isEmpty ? nil : UIImage(systemName: symbol)
130
+ configuration.imagePadding = 8
131
+ configuration.cornerStyle = .capsule
132
+ configuration.buttonSize = .large
133
+ configuration.baseForegroundColor = tint
134
+ button.configuration = configuration
135
+ bar.tintColor = tint
136
+ bar.standardAppearance = appearance
137
+ bar.scrollEdgeAppearance = appearance
138
+ bar.compactAppearance = appearance
139
+ needsBarUpdate = true
140
+ setNeedsLayout()
141
+ }
142
+
143
+ private func updateBar() {
144
+ guard toolbar, bounds.width > 0, needsBarUpdate || lastWidth != bounds.width else { return }
145
+ dismissMenus()
146
+ needsBarUpdate = false; lastWidth = bounds.width
147
+ let font = UIFont.preferredFont(forTextStyle: .body)
148
+ func width(_ item: MenuItem) -> CGFloat {
149
+ let hasSymbol = item.systemImage.flatMap { UIImage(systemName: $0) } != nil
150
+ return hasSymbol ? 52 : max(52, ceil((item.title as NSString).size(withAttributes: [.font: font]).width) + 40)
151
+ }
152
+ // Reserve overflow space whenever there are hidden items. On narrow layouts,
153
+ // move actions to a native menu instead of clipping or shrinking their labels.
154
+ var visible: [MenuItem] = []
155
+ var used: CGFloat = 0
156
+ let available = max(0, bounds.width - 32)
157
+ for item in items where item.placement != "overflow" {
158
+ let candidate = width(item)
159
+ let needsOverflow = visible.count + 1 < items.count
160
+ if visible.count < maxVisibleItems && used + candidate + (needsOverflow ? 60 : 0) <= available {
161
+ visible.append(item); used += candidate
162
+ }
163
+ }
164
+ let visibleIDs = Set(visible.map(\.id))
165
+ let overflow = items.filter { !visibleIDs.contains($0.id) }
166
+ var controls = visible.map { item -> UIBarButtonItem in
167
+ let control: UIBarButtonItem
168
+ if item.isGroup {
169
+ let menu = UIMenu(children: elements(item.items ?? [], revision: revision))
170
+ if let image = item.systemImage.flatMap({ UIImage(systemName: $0) }) {
171
+ control = UIBarButtonItem(image: image, menu: menu)
172
+ } else { control = UIBarButtonItem(title: item.title, menu: menu) }
173
+ } else {
174
+ control = UIBarButtonItem(primaryAction: action(item, revision: revision))
175
+ if control.image != nil { control.title = nil }
176
+ control.isSelected = item.checked == true
177
+ }
178
+ control.width = width(item)
179
+ control.isEnabled = !disabled && item.disabled != true
180
+ control.accessibilityLabel = item.title
181
+ control.accessibilityIdentifier = identifier.isEmpty ? item.id : "\(identifier)-\(item.id)"
182
+ if item.destructive == true { control.tintColor = .systemRed }
183
+ return control
184
+ }
185
+ if !overflow.isEmpty {
186
+ let more = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: UIMenu(children: elements(overflow, revision: revision)))
187
+ more.accessibilityLabel = "More actions"
188
+ more.accessibilityIdentifier = identifier.isEmpty ? "toolbar-overflow" : "\(identifier)-overflow"
189
+ more.isEnabled = !disabled
190
+ more.width = 52
191
+ controls.append(more)
192
+ }
193
+ for control in controls {
194
+ if #available(iOS 16.0, *) { control.preferredMenuElementOrder = .fixed }
195
+ if #available(iOS 26.0, *) {
196
+ control.sharesBackground = mergingEnabled
197
+ control.hidesSharedBackground = forceFallback || UIAccessibility.isReduceTransparencyEnabled
198
+ }
199
+ }
200
+ bar.setItems(controls, animated: false)
201
+ }
202
+ private func dismissMenus() {
203
+ func dismiss(in view: UIView) {
204
+ for interaction in view.interactions {
205
+ (interaction as? UIContextMenuInteraction)?.dismissMenu()
206
+ }
207
+ for child in view.subviews { dismiss(in: child) }
208
+ }
209
+ dismiss(in: self)
210
+ }
211
+ public override func layoutSubviews() {
212
+ super.layoutSubviews()
213
+ button.frame = bounds.insetBy(dx: 4, dy: min(8, bounds.height / 4))
214
+ bar.frame = bounds
215
+ updateBar()
216
+ }
217
+ public override func didMoveToWindow() {
218
+ super.didMoveToWindow()
219
+ if window == nil { dismissMenus() }
220
+ }
221
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ @interface ALGSegmentedComponentView : RCTViewComponentView
3
+ @end
@@ -0,0 +1,36 @@
1
+ #import "ALGSegmentedComponentView.h"
2
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/ComponentDescriptors.h>
3
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/Props.h>
4
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/EventEmitters.h>
5
+ #import <React/RCTConversions.h>
6
+ #import "AdaptiveLiquidGlass-Swift.h"
7
+ using namespace facebook::react;
8
+ @implementation ALGSegmentedComponentView {
9
+ ALGSegmentedView *_segmented;
10
+ }
11
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
12
+ return concreteComponentDescriptorProvider<ALGSegmentedComponentDescriptor>();
13
+ }
14
+ - (instancetype)initWithFrame:(CGRect)frame {
15
+ if (self = [super initWithFrame:frame]) {
16
+ _props = std::make_shared<const ALGSegmentedProps>();
17
+ _segmented = [[ALGSegmentedView alloc] initWithFrame:CGRectZero];
18
+ self.contentView = _segmented;
19
+ __weak ALGSegmentedComponentView *weakSelf = self;
20
+ _segmented.onSelectionChange = ^(NSString *value) {
21
+ ALGSegmentedComponentView *strongSelf = weakSelf;
22
+ if (!strongSelf || !strongSelf->_eventEmitter) return;
23
+ std::static_pointer_cast<const ALGSegmentedEventEmitter>(strongSelf->_eventEmitter)->onSelectionChange({std::string(value.UTF8String)});
24
+ };
25
+ }
26
+ return self;
27
+ }
28
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
29
+ const auto &p = *std::static_pointer_cast<const ALGSegmentedProps>(props);
30
+ NSString *scheme = p.colorScheme == ALGSegmentedColorScheme::Dark ? @"dark" : p.colorScheme == ALGSegmentedColorScheme::Light ? @"light" : @"system";
31
+ [_segmented configure:@(p.optionsJSON.c_str()) selectedValue:@(p.selectedValue.c_str()) disabled:p.disabled
32
+ tint:RCTUIColorFromSharedColor(p.glassTint) scheme:scheme label:@(p.controlLabel.c_str()) identifier:@(p.controlTestID.c_str())];
33
+ [super updateProps:props oldProps:oldProps];
34
+ }
35
+ + (BOOL)shouldBeRecycled { return NO; }
36
+ @end
@@ -0,0 +1,58 @@
1
+ import UIKit
2
+
3
+ private struct SegmentOption: Decodable {
4
+ let value: String
5
+ let label: String
6
+ let disabled: Bool?
7
+ }
8
+
9
+ /// Native segmented-control interaction and selected-thumb rendering belong to UIKit.
10
+ @objc(ALGSegmentedView)
11
+ public final class ALGSegmentedView: UIView {
12
+ private let control = UISegmentedControl(items: [])
13
+ private var options: [SegmentOption] = []
14
+ private var lastJSON = ""
15
+ private var selectedValue = ""
16
+ @objc public var onSelectionChange: ((String) -> Void)?
17
+
18
+ public override init(frame: CGRect) {
19
+ super.init(frame: frame)
20
+ addSubview(control)
21
+ control.addTarget(self, action: #selector(selectionChanged), for: .valueChanged)
22
+ }
23
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
24
+
25
+ @objc public func configure(_ json: String, selectedValue: String, disabled: Bool,
26
+ tint: UIColor?, scheme: String, label: String, identifier: String) {
27
+ if lastJSON != json {
28
+ options = (try? JSONDecoder().decode([SegmentOption].self, from: Data(json.utf8))) ?? []
29
+ control.removeAllSegments()
30
+ for (index, option) in options.enumerated() {
31
+ control.insertSegment(withTitle: option.label, at: index, animated: false)
32
+ control.setEnabled(!(option.disabled ?? false), forSegmentAt: index)
33
+ }
34
+ lastJSON = json
35
+ }
36
+ self.selectedValue = selectedValue
37
+ control.selectedSegmentIndex = options.firstIndex { $0.value == selectedValue } ?? UISegmentedControl.noSegment
38
+ control.isEnabled = !disabled
39
+ control.selectedSegmentTintColor = tint
40
+ control.accessibilityLabel = label.isEmpty ? nil : label
41
+ control.accessibilityIdentifier = identifier
42
+ overrideUserInterfaceStyle = scheme == "dark" ? .dark : scheme == "light" ? .light : .unspecified
43
+ }
44
+
45
+ @objc private func selectionChanged() {
46
+ let index = control.selectedSegmentIndex
47
+ guard control.isEnabled, options.indices.contains(index), !(options[index].disabled ?? false) else { return }
48
+ let next = options[index].value
49
+ // React remains authoritative, including when a callback declines a change.
50
+ control.selectedSegmentIndex = options.firstIndex { $0.value == selectedValue } ?? UISegmentedControl.noSegment
51
+ if next != selectedValue { onSelectionChange?(next) }
52
+ }
53
+
54
+ public override func layoutSubviews() {
55
+ super.layoutSubviews()
56
+ control.frame = bounds.insetBy(dx: 0, dy: max(0, (bounds.height - 44) / 2))
57
+ }
58
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ @interface ALGSliderComponentView : RCTViewComponentView
3
+ @end
@@ -0,0 +1,47 @@
1
+ #import "ALGSliderComponentView.h"
2
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/ComponentDescriptors.h>
3
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/Props.h>
4
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/EventEmitters.h>
5
+ #import <React/RCTConversions.h>
6
+ #import "AdaptiveLiquidGlass-Swift.h"
7
+ using namespace facebook::react;
8
+ @implementation ALGSliderComponentView {
9
+ ALGSliderView *_slider;
10
+ }
11
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
12
+ return concreteComponentDescriptorProvider<ALGSliderComponentDescriptor>();
13
+ }
14
+ - (instancetype)initWithFrame:(CGRect)frame {
15
+ if (self = [super initWithFrame:frame]) {
16
+ _props = std::make_shared<const ALGSliderProps>();
17
+ _slider = [[ALGSliderView alloc] initWithFrame:CGRectZero];
18
+ self.contentView = _slider;
19
+ __weak ALGSliderComponentView *weakSelf = self;
20
+ _slider.onChange = ^(double value) {
21
+ ALGSliderComponentView *self = weakSelf;
22
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGSliderEventEmitter>(self->_eventEmitter)->onSliderChange({value});
23
+ };
24
+ _slider.onStart = ^(double value) {
25
+ ALGSliderComponentView *self = weakSelf;
26
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGSliderEventEmitter>(self->_eventEmitter)->onSliderStart({value});
27
+ };
28
+ _slider.onComplete = ^(double value) {
29
+ ALGSliderComponentView *self = weakSelf;
30
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGSliderEventEmitter>(self->_eventEmitter)->onSliderComplete({value});
31
+ };
32
+ _slider.onCancel = ^(double value) {
33
+ ALGSliderComponentView *self = weakSelf;
34
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGSliderEventEmitter>(self->_eventEmitter)->onSliderCancel({value});
35
+ };
36
+ }
37
+ return self;
38
+ }
39
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
40
+ const auto &p = *std::static_pointer_cast<const ALGSliderProps>(props);
41
+ [_slider configure:p.value minimum:p.minimumValue maximum:p.maximumValue step:p.step disabled:p.disabled
42
+ tint:RCTUIColorFromSharedColor(p.glassTint) label:@(p.controlLabel.c_str())
43
+ hint:@(p.controlHint.c_str()) identifier:@(p.controlTestID.c_str())];
44
+ [super updateProps:props oldProps:oldProps];
45
+ }
46
+ + (BOOL)shouldBeRecycled { return NO; }
47
+ @end
@@ -0,0 +1,102 @@
1
+ import UIKit
2
+
3
+ private final class AccessibleGlassSlider: UISlider {
4
+ var adjust: ((Double) -> Void)?
5
+ override func accessibilityIncrement() { adjust?(1) }
6
+ override func accessibilityDecrement() { adjust?(-1) }
7
+ }
8
+
9
+ @objc(ALGSliderView)
10
+ public final class ALGSliderView: UIView {
11
+ private let slider = AccessibleGlassSlider()
12
+ private var minimum = 0.0
13
+ private var maximum = 1.0
14
+ private var step = 0.0
15
+ private var controlledValue = 0.0
16
+ private var dragging = false
17
+ private var lastEmitted: Double?
18
+ @objc public var onChange: ((Double) -> Void)?
19
+ @objc public var onStart: ((Double) -> Void)?
20
+ @objc public var onComplete: ((Double) -> Void)?
21
+ @objc public var onCancel: ((Double) -> Void)?
22
+
23
+ public override init(frame: CGRect) {
24
+ super.init(frame: frame)
25
+ addSubview(slider)
26
+ slider.addTarget(self, action: #selector(start), for: .touchDown)
27
+ slider.addTarget(self, action: #selector(change), for: .valueChanged)
28
+ slider.addTarget(self, action: #selector(finish), for: [.touchUpInside, .touchUpOutside])
29
+ slider.addTarget(self, action: #selector(cancel), for: .touchCancel)
30
+ slider.adjust = { [weak self] direction in
31
+ guard let self, self.slider.isEnabled else { return }
32
+ self.onStart?(self.controlledValue)
33
+ let increment = self.step > 0 ? self.step : (self.maximum - self.minimum) / 20
34
+ let value = self.normalized(self.controlledValue + direction * increment)
35
+ self.position(value)
36
+ if value != self.controlledValue { self.onChange?(value) }
37
+ self.onComplete?(value)
38
+ }
39
+ }
40
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
41
+
42
+ @objc public func configure(_ value: Double, minimum: Double, maximum: Double, step: Double,
43
+ disabled: Bool, tint: UIColor?, label: String, hint: String, identifier: String) {
44
+ guard minimum.isFinite, maximum.isFinite, value.isFinite, step.isFinite,
45
+ (maximum - minimum).isFinite, maximum > minimum, step >= 0, step <= maximum - minimum else { return }
46
+ if dragging && (disabled || self.minimum != minimum || self.maximum != maximum || self.step != step) { cancel() }
47
+ self.minimum = minimum
48
+ self.maximum = maximum
49
+ self.step = step
50
+ controlledValue = normalized(value)
51
+ slider.isEnabled = !disabled
52
+ slider.minimumTrackTintColor = tint
53
+ slider.accessibilityLabel = label.isEmpty ? nil : label
54
+ slider.accessibilityHint = hint.isEmpty ? nil : hint
55
+ slider.accessibilityIdentifier = identifier
56
+ // UIKit keeps ownership of the thumb during tracking, avoiding JS echo jitter.
57
+ if !dragging { position(controlledValue) }
58
+ }
59
+ private func normalized(_ value: Double) -> Double {
60
+ let clamped = min(maximum, max(minimum, value))
61
+ if clamped == maximum || step == 0 { return clamped }
62
+ return min(maximum, max(minimum, minimum + ((clamped - minimum) / step).rounded() * step))
63
+ }
64
+ private var current: Double { normalized(minimum + Double(slider.value) * (maximum - minimum)) }
65
+ private func position(_ value: Double) {
66
+ slider.value = Float((value - minimum) / (maximum - minimum))
67
+ slider.accessibilityValue = String(format: "%.6g", value)
68
+ }
69
+ @objc private func start() {
70
+ guard slider.isEnabled, !dragging else { return }
71
+ dragging = true
72
+ lastEmitted = current
73
+ onStart?(current)
74
+ }
75
+ @objc private func change() {
76
+ guard slider.isEnabled else { return }
77
+ let value = current
78
+ // Snap only for stepped sliders; preserve UIKit momentum for continuous tracking.
79
+ if step > 0 { position(value) }
80
+ else { slider.accessibilityValue = String(format: "%.6g", value) }
81
+ if value != lastEmitted { lastEmitted = value; onChange?(value) }
82
+ if !dragging { onComplete?(value) }
83
+ }
84
+ @objc private func finish() {
85
+ guard dragging else { return }
86
+ change()
87
+ let value = current
88
+ dragging = false
89
+ onComplete?(value)
90
+ }
91
+ @objc private func cancel() {
92
+ guard dragging else { return }
93
+ dragging = false
94
+ position(controlledValue)
95
+ onCancel?(controlledValue)
96
+ }
97
+ public override func didMoveToWindow() {
98
+ super.didMoveToWindow()
99
+ if window == nil { cancel() }
100
+ }
101
+ public override func layoutSubviews() { super.layoutSubviews(); slider.frame = bounds }
102
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ @interface ALGSurfaceComponentView : RCTViewComponentView
3
+ @end
@@ -0,0 +1,45 @@
1
+ #import "ALGSurfaceComponentView.h"
2
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/ComponentDescriptors.h>
3
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/Props.h>
4
+ #import <React/RCTConversions.h>
5
+ #import "AdaptiveLiquidGlass-Swift.h"
6
+
7
+ using namespace facebook::react;
8
+
9
+ @implementation ALGSurfaceComponentView {
10
+ ALGSurfaceView *_glass;
11
+ }
12
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
13
+ return concreteComponentDescriptorProvider<ALGSurfaceComponentDescriptor>();
14
+ }
15
+ - (instancetype)initWithFrame:(CGRect)frame {
16
+ if (self = [super initWithFrame:frame]) {
17
+ _props = std::make_shared<const ALGSurfaceProps>();
18
+ _glass = [[ALGSurfaceView alloc] initWithFrame:CGRectZero];
19
+ self.contentView = _glass;
20
+ }
21
+ return self;
22
+ }
23
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
24
+ const auto &p = *std::static_pointer_cast<const ALGSurfaceProps>(props);
25
+ NSString *material = p.material == ALGSurfaceMaterial::Clear ? @"clear" : p.material == ALGSurfaceMaterial::None ? @"none" : @"regular";
26
+ NSString *scheme = p.colorScheme == ALGSurfaceColorScheme::Dark ? @"dark" : p.colorScheme == ALGSurfaceColorScheme::Light ? @"light" : @"system";
27
+ [_glass configure:material interactive:p.interactive tint:RCTUIColorFromSharedColor(p.glassTint)
28
+ radius:p.glassRadius container:p.container mergingEnabled:p.mergingEnabled spacing:p.spacing duration:p.animationDuration scheme:scheme];
29
+ [super updateProps:props oldProps:oldProps];
30
+ }
31
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index {
32
+ [_glass.reactContentView insertSubview:child atIndex:index];
33
+ }
34
+ - (void)layoutSubviews {
35
+ [super layoutSubviews];
36
+ // Fabric's default contentView frame excludes padding. React children already
37
+ // carry Yoga's padding offsets, so the material must occupy the full bounds.
38
+ _glass.frame = self.bounds;
39
+ }
40
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)child index:(NSInteger)index {
41
+ [child removeFromSuperview];
42
+ }
43
+ // UIKit effects retain state; a fresh instance also avoids stale effects on remount.
44
+ + (BOOL)shouldBeRecycled { return NO; }
45
+ @end