@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 +23 -3
- package/android/src/main/java/expo/modules/sheets/RNToolsSheetsView.kt +2 -2
- package/ios/RNToolsSheets.podspec +6 -2
- package/ios/RNToolsSheetsView.swift +187 -86
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.h +11 -0
- package/ios/Sources/RNTSurfaceTouchHandlerWrapper.mm +43 -0
- package/package.json +1 -2
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
|
|
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(
|
|
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.
|
|
92
|
+
Color.WHITE
|
|
93
93
|
}
|
|
94
|
-
} ?: Color.
|
|
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.
|
|
15
|
-
:tvos => '16.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
217
|
+
struct SheetInternalVCRepresentable: UIViewControllerRepresentable {
|
|
218
|
+
let controller: SheetInternalViewController
|
|
182
219
|
|
|
183
|
-
func
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
func makeUIViewController(context: Context) -> SheetInternalViewController {
|
|
221
|
+
controller
|
|
222
|
+
}
|
|
223
|
+
func updateUIViewController(
|
|
224
|
+
_ uiViewController: SheetInternalViewController,
|
|
225
|
+
context: Context
|
|
226
|
+
) {}
|
|
227
|
+
}
|
|
186
228
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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,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