@rn-tools/sheets 0.1.2 → 0.1.4

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.
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  An expo module for rendering native bottom sheet components in iOS and Android.
4
4
 
5
- Uses SwiftUI's sheet API and Android's BottomSheetDialog component to render React Native children in a modal bottom sheet
5
+ Uses SwiftUI's sheet API and Android's BottomSheetDialog to render React Native children in a modal bottom sheet
6
+
7
+
8
+ https://github.com/user-attachments/assets/426c77e6-74c6-4748-8010-477267fa9433
9
+
6
10
 
7
11
  ## Motivation
8
12
 
@@ -12,8 +16,24 @@ Uses SwiftUI's sheet API and Android's BottomSheetDialog component to render Rea
12
16
 
13
17
  ## Installation
14
18
 
15
- `yarn add @rntools/sheets`
19
+ `yarn add @rntools/sheets expo-build-properties`
20
+
21
+ Update your minimum iOS deployment target to 16 in `app.json`:
22
+
23
+ ```json
24
+ {
25
+ "plugins": [
26
+ [
27
+ "expo-build-properties",
28
+ {
29
+ "ios": {
30
+ "deploymentTarget": "16.0"
31
+ }
32
+ }
33
+ ]
34
+ }
16
35
 
36
+ ```
17
37
 
18
38
  As with most non-core expo modules this requires a new native build
19
39
 
@@ -28,7 +48,7 @@ export default function App() {
28
48
 
29
49
  return (
30
50
  <View className="flex-1">
31
- <Button title="Show sheet" onPress={() => setIsOpen(!isOpen)} />
51
+ <Button title="Show sheet" onPress={() => setIsOpen(true)} />
32
52
 
33
53
  <BottomSheet
34
54
  isOpen={isOpen}
@@ -89,9 +89,9 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
89
89
  try {
90
90
  Color.parseColor(it) // Convert hex string to Color
91
91
  } catch (e: IllegalArgumentException) {
92
- Color.TRANSPARENT
92
+ Color.WHITE
93
93
  }
94
- } ?: Color.TRANSPARENT
94
+ } ?: Color.WHITE
95
95
 
96
96
  val drawable = GradientDrawable().apply {
97
97
  setColor(backgroundColor)
@@ -1,6 +1,8 @@
1
1
  require 'json'
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+ new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
5
+
4
6
 
5
7
  Pod::Spec.new do |s|
6
8
  s.name = 'RNToolsSheets'
@@ -11,18 +13,20 @@ Pod::Spec.new do |s|
11
13
  s.author = package['author']
12
14
  s.homepage = package['homepage']
13
15
  s.platforms = {
14
- :ios => '16.4',
15
- :tvos => '16.4'
16
+ :ios => '16.0',
17
+ :tvos => '16.0'
16
18
  }
17
19
  s.swift_version = '5.4'
18
20
  s.source = { git: 'https://github.com/ajsmth/rn-tools' }
19
21
  s.static_framework = true
20
22
 
21
23
  s.dependency 'ExpoModulesCore'
24
+ s.public_header_files = 'Sources/RNTSurfaceTouchHandlerWrapper.h'
22
25
 
23
26
  # Swift/Objective-C compatibility
24
27
  s.pod_target_xcconfig = {
25
28
  'DEFINES_MODULE' => 'YES',
29
+ 'OTHER_SWIFT_FLAGS' => new_arch_enabled ? '-DRCT_NEW_ARCH_ENABLED' : ''
26
30
  }
27
31
 
28
32
  s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
@@ -1,4 +1,5 @@
1
1
  import ExpoModulesCore
2
+ import React
2
3
  import SwiftUI
3
4
 
4
5
  public class SheetProps: ObservableObject {
@@ -11,7 +12,6 @@ public class SheetProps: ObservableObject {
11
12
  @Published var grabberVisible: Bool = true
12
13
  @Published var backgroundColor: String? = nil
13
14
  @Published var cornerRadius: Float? = nil
14
-
15
15
  }
16
16
 
17
17
  struct SheetAppearance: Record {
@@ -20,7 +20,7 @@ struct SheetAppearance: Record {
20
20
 
21
21
  @Field
22
22
  var backgroundColor: String?
23
-
23
+
24
24
  @Field
25
25
  var cornerRadius: Float?
26
26
  }
@@ -32,50 +32,84 @@ public class RNToolsSheetsView: ExpoView {
32
32
 
33
33
  var touchHandler: RCTTouchHandler?
34
34
 
35
+ private lazy var sheetVC = SheetInternalViewController()
36
+
35
37
  lazy var hostingController = UIHostingController(
36
38
  rootView: ContentView(
37
- props: props, onDismiss: onDismiss, onStateChange: onStateChange))
39
+ props: props, sheetVC: sheetVC, onDismiss: onDismiss,
40
+ onStateChange: onStateChange))
38
41
 
39
42
  required init(appContext: AppContext? = nil) {
40
43
  super.init(appContext: appContext)
41
44
 
42
45
  if let bridge = appContext?.reactBridge {
43
- touchHandler = RCTTouchHandler(bridge: bridge)
46
+ sheetVC.bridge = bridge
44
47
  }
45
48
 
46
49
  hostingController.view.autoresizingMask = [
47
50
  .flexibleWidth, .flexibleHeight,
48
51
  ]
49
52
  hostingController.view.backgroundColor = UIColor.clear
50
- hostingController.rootView = ContentView(
51
- props: props, onDismiss: onDismiss, onStateChange: onStateChange)
53
+ }
54
+
55
+ public override func layoutSubviews() {
56
+ super.layoutSubviews()
57
+ hostingController.view.frame = bounds
58
+ }
59
+
60
+ public override func didMoveToSuperview() {
61
+ super.didMoveToSuperview()
62
+ let parentViewController = findParentViewControllerOrNil()
52
63
 
64
+ parentViewController.apply {
65
+ $0.addChild(hostingController)
66
+ }
67
+
68
+ hostingController.view.layer.removeAllAnimations()
69
+ hostingController.view.frame = bounds
53
70
  addSubview(hostingController.view)
71
+ parentViewController.apply {
72
+ hostingController.didMove(toParent: $0)
73
+ }
54
74
  }
55
75
 
56
76
  public override func reactSubviews() -> [UIView]! {
57
77
  return []
58
78
  }
59
79
 
60
- public override func insertReactSubview(_ subview: UIView!, at atIndex: Int)
61
- {
62
- super.insertReactSubview(subview, at: atIndex)
63
- props.children.insert(subview, at: atIndex)
64
- if atIndex == 0 {
65
- touchHandler?.attach(to: subview)
80
+ #if RCT_NEW_ARCH_ENABLED
81
+ public override func mountChildComponentView(
82
+ _ childComponentView: UIView,
83
+ index: Int
84
+ ) {
85
+ sheetVC.insertChild(childComponentView, at: index)
66
86
  }
67
- }
68
87
 
69
- public override func removeReactSubview(_ subview: UIView!) {
70
- super.removeReactSubview(subview)
71
- if let index = props.children.firstIndex(of: subview) {
72
- props.children.remove(at: index)
88
+ public override func unmountChildComponentView(
89
+ _ childComponentView: UIView,
90
+ index: Int
91
+ ) {
92
+ childComponentView.removeFromSuperview()
73
93
  }
74
- }
94
+
95
+ #else
96
+ public override func insertReactSubview(
97
+ _ subview: UIView!, at atIndex: Int
98
+ ) {
99
+ sheetVC.insertChild(subview, at: atIndex)
100
+ }
101
+
102
+ public override func removeReactSubview(_ subview: UIView!) {
103
+ sheetVC.removeChild(subview)
104
+ }
105
+
106
+ #endif
107
+
75
108
  }
76
109
 
77
110
  struct ContentView: View {
78
111
  @ObservedObject var props: SheetProps
112
+ var sheetVC: SheetInternalViewController
79
113
  var onDismiss: EventDispatcher
80
114
  var onStateChange: EventDispatcher
81
115
 
@@ -119,101 +153,168 @@ struct ContentView: View {
119
153
  onStateChange(["type": "HIDDEN"])
120
154
  }
121
155
  ) {
122
- VStack {
123
- ForEach(Array(props.children.enumerated()), id: \.offset) {
124
- index, child in
125
- RepresentableView(view: child)
126
- }
127
- }
128
- .background(
129
- GeometryReader { geometry in
130
- Color.clear
131
- .onChange(of: geometry.size.height) { newHeight in
132
- if abs(newHeight - lastHeight) > 2 {
133
- if !isDragging {
134
- isDragging = true
135
- onStateChange(["type": "DRAGGING"])
136
- }
156
+ SheetInternalVCRepresentable(controller: sheetVC)
157
+ .background(
158
+ GeometryReader { geometry in
159
+ Color.clear
160
+ .onChange(of: geometry.size.height) {
161
+ newHeight in
162
+ if abs(newHeight - lastHeight) > 2 {
163
+ if !isDragging {
164
+ isDragging = true
165
+ onStateChange(["type": "DRAGGING"])
166
+ }
137
167
 
138
- settleTimer?.invalidate()
139
- settleTimer = Timer.scheduledTimer(
140
- withTimeInterval: 0.15, repeats: false
141
- ) { _ in
142
- isDragging = false
143
- onStateChange(["type": "SETTLING"])
144
-
145
- DispatchQueue.main.asyncAfter(
146
- deadline: .now() + 0.15
147
- ) {
148
- let idx = upperSnapIndex(
149
- for: newHeight,
150
- snapPoints: props.snapPoints
151
- )
152
- onStateChange([
153
- "type": "OPEN",
154
- "payload": ["index": idx],
155
- ])
168
+ settleTimer?.invalidate()
169
+ settleTimer = Timer.scheduledTimer(
170
+ withTimeInterval: 0.15,
171
+ repeats: false
172
+ ) { _ in
173
+ isDragging = false
174
+ onStateChange(["type": "SETTLING"])
175
+
176
+ DispatchQueue.main.asyncAfter(
177
+ deadline: .now() + 0.15
178
+ ) {
179
+ let idx = upperSnapIndex(
180
+ for: newHeight,
181
+ snapPoints: props.snapPoints
182
+ )
183
+ onStateChange([
184
+ "type": "OPEN",
185
+ "payload": ["index": idx],
186
+ ])
187
+ }
156
188
  }
157
189
  }
158
- }
159
190
 
160
- lastHeight = newHeight
161
- }
191
+ lastHeight = newHeight
192
+ }
193
+ }
194
+ )
195
+ .presentationBackground16_4(
196
+ props.backgroundColor != nil
197
+ ? Color(hex: props.backgroundColor!) : Color.white
198
+ )
199
+ .presentationCornerRadius16_4(
200
+ props.cornerRadius.map { CGFloat($0) }
201
+ )
202
+ .presentationDragIndicator(
203
+ props.grabberVisible ? .visible : .hidden
204
+ )
205
+ .presentationDetents(
206
+ Set(detents),
207
+ selection: $selectedDetent
208
+ )
209
+
210
+ .onAppear {
211
+ selectedDetent = detent(for: props.openToIndex)
162
212
  }
163
- )
164
- .presentationBackground(props.backgroundColor != nil ? Color(hex: props.backgroundColor!) : Color.white)
165
- .presentationDragIndicator(
166
- props.grabberVisible ? .visible : .hidden
167
- )
168
- .presentationDetents(
169
- Set(detents),
170
- selection: $selectedDetent
171
- )
172
- .presentationCornerRadius(props.cornerRadius.map { CGFloat($0) })
173
- .onAppear {
174
- selectedDetent = detent(for: props.openToIndex)
175
- }
176
213
  }
177
214
  }
178
215
  }
179
216
 
180
- struct RepresentableView: UIViewRepresentable {
181
- var view: UIView
217
+ struct SheetInternalVCRepresentable: UIViewControllerRepresentable {
218
+ let controller: SheetInternalViewController
182
219
 
183
- func makeUIView(context: Context) -> UIView {
184
- let containerView = UIView()
185
- containerView.backgroundColor = .clear
220
+ func makeUIViewController(context: Context) -> SheetInternalViewController {
221
+ controller
222
+ }
223
+ func updateUIViewController(
224
+ _ uiViewController: SheetInternalViewController,
225
+ context: Context
226
+ ) {}
227
+ }
186
228
 
187
- view.translatesAutoresizingMaskIntoConstraints = false
188
- containerView.addSubview(view)
229
+ final class SheetInternalViewController: UIViewController {
230
+ var bridge: RCTBridge? {
231
+ didSet {
232
+ touchHandler = RCTTouchHandler(bridge: bridge)
233
+ self.touchHandler?.attach(to: self.view)
234
+ }
235
+ }
236
+ var surfaceTouchHandler = RNTSurfaceTouchHandlerWrapper()
237
+ var touchHandler: RCTTouchHandler?
238
+
239
+ init() {
240
+ super.init(nibName: nil, bundle: nil)
241
+ view.backgroundColor = .clear
242
+ }
243
+
244
+ override func loadView() {
245
+ self.view = UIView()
246
+
247
+ self.surfaceTouchHandler.attach(to: self.view)
248
+ }
249
+
250
+ @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
189
251
 
190
- NSLayoutConstraint.activate([
191
- view.topAnchor.constraint(equalTo: containerView.topAnchor),
192
- view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
193
- view.trailingAnchor.constraint(
194
- equalTo: containerView.trailingAnchor),
195
- view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
196
- ])
252
+ func insertChild(_ child: UIView, at index: Int) {
253
+ view.insertSubview(child, at: index)
197
254
 
198
- return containerView
199
255
  }
200
256
 
201
- func updateUIView(_ uiView: UIView, context: Context) {}
257
+ func removeChild(_ child: UIView) {
258
+ child.removeFromSuperview()
259
+ }
202
260
  }
203
261
 
262
+ extension Optional {
263
+ func apply(_ fn: (Wrapped) -> Void) {
264
+ if case let .some(val) = self {
265
+ fn(val)
266
+ }
267
+ }
268
+ }
269
+
270
+ extension UIView {
271
+ // Walks the responder chain to find the parent UIViewController
272
+ // or null if not in a heirarchy yet
273
+ func findParentViewControllerOrNil() -> UIViewController? {
274
+ var nextResponder: UIResponder? = next
275
+ while nextResponder != nil && nextResponder as? UIViewController == nil
276
+ {
277
+ nextResponder = nextResponder?.next
278
+ }
279
+
280
+ return nextResponder as? UIViewController
281
+ }
282
+ }
204
283
 
205
284
  extension Color {
206
285
  init(hex: String) {
207
286
  var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
208
287
  hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
209
-
288
+
210
289
  var rgb: UInt64 = 0
211
290
  Scanner(string: hexSanitized).scanHexInt64(&rgb)
212
-
291
+
213
292
  let red = Double((rgb & 0xFF0000) >> 16) / 255.0
214
293
  let green = Double((rgb & 0x00FF00) >> 8) / 255.0
215
294
  let blue = Double(rgb & 0x0000FF) / 255.0
216
-
295
+
217
296
  self.init(red: red, green: green, blue: blue)
218
297
  }
219
298
  }
299
+
300
+
301
+ extension View {
302
+ @ViewBuilder
303
+ func presentationBackground16_4(_ color: Color?) -> some View {
304
+ if #available(iOS 16.4, *) {
305
+ self.presentationBackground(color ?? .white)
306
+ } else {
307
+ self
308
+ }
309
+ }
310
+
311
+ @ViewBuilder
312
+ func presentationCornerRadius16_4(_ radius: CGFloat?) -> some View {
313
+ if #available(iOS 16.4, *) {
314
+ self.presentationCornerRadius(radius)
315
+ } else {
316
+ self
317
+ }
318
+ }
319
+ }
320
+
@@ -0,0 +1,11 @@
1
+ // RNTSurfaceTouchHandlerWrapper.h (PUBLIC)
2
+ #import <Foundation/Foundation.h>
3
+ @class UIView;
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+ @interface RNTSurfaceTouchHandlerWrapper : NSObject
7
+ - (void)attachToView:(UIView *)view;
8
+ @end
9
+ NS_ASSUME_NONNULL_END
10
+
11
+
@@ -0,0 +1,43 @@
1
+ // RNTSurfaceTouchHandlerWrapper.mm (Objective‑C++, .mm extension!)
2
+
3
+
4
+ #import "RNTSurfaceTouchHandlerWrapper.h"
5
+ #if RCT_NEW_ARCH_ENABLED
6
+ #import <React/RCTSurfaceTouchHandler.h>
7
+
8
+
9
+ @implementation RNTSurfaceTouchHandlerWrapper {
10
+
11
+ RCTSurfaceTouchHandler *_handler;
12
+ }
13
+
14
+ - (instancetype)init {
15
+ if (self = [super init]) {
16
+ _handler = [[RCTSurfaceTouchHandler alloc] init];
17
+ }
18
+ return self;
19
+ }
20
+
21
+ - (void)attachToView:(UIView *)view {
22
+ [_handler attachToView:view];
23
+ }
24
+ @end
25
+ #else
26
+
27
+ @implementation RNTSurfaceTouchHandlerWrapper {
28
+ }
29
+
30
+ - (instancetype)init {
31
+ if (self = [super init]) {
32
+ }
33
+ return self;
34
+ }
35
+
36
+ - (void)attachToView:(UIView *)view {
37
+ }
38
+ @end
39
+
40
+ #endif
41
+
42
+
43
+
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@rn-tools/sheets",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "My new module",
5
5
  "main": "src/index.ts",
6
- "private": false,
7
6
  "scripts": {
8
7
  "build": "expo-module build",
9
8
  "clean": "expo-module clean",