@rn-tools/sheets 0.1.4 → 3.0.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e62a766: Initial changeset publish
8
+ - Updated dependencies [e62a766]
9
+ - @rn-tools/core@3.0.1
10
+
3
11
  ## Unpublished
4
12
 
5
13
  ### 🛠 Breaking changes
package/README.md CHANGED
@@ -2,8 +2,9 @@
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 to render React Native children in a modal bottom sheet
5
+ Uses native iOS sheet presentation and Android's BottomSheetDialog to render React Native children in a modal bottom sheet.
6
6
 
7
+ Supports stacking multiple sheets on top of each other
7
8
 
8
9
  https://github.com/user-attachments/assets/426c77e6-74c6-4748-8010-477267fa9433
9
10
 
@@ -52,9 +53,11 @@ export default function App() {
52
53
 
53
54
  <BottomSheet
54
55
  isOpen={isOpen}
55
- onOpenChange={setIsOpen}
56
- openToIndex={1}
56
+ setIsOpen={setIsOpen}
57
+ initialIndex={1}
57
58
  onStateChange={(event) => console.log({ event })}
59
+ canDismiss={true}
60
+ onDismissPrevented={() => console.log("dismiss prevented")}
58
61
  snapPoints={[400, 600, 750]}
59
62
  appearanceAndroid={{
60
63
  dimAmount: 0,
@@ -76,16 +79,18 @@ export default function App() {
76
79
 
77
80
  ## Props
78
81
 
79
- - `isOpen / onOpenChange` - Controller props for toggling the sheet open and closed - this is required
82
+ - `isOpen / setIsOpen` - Controller props for toggling the sheet open and closed - this is required
80
83
 
81
- - `openToIndex` - will open the bottom sheet to the defined snapPoint index
84
+ - `initialIndex` - will open the bottom sheet to the defined snapPoint index
82
85
 
83
86
  - `onStateChange` - callback to track the internal state of the sheet. The following events are emitted:
84
87
 
85
88
  - { type: "HIDDEN" }
86
89
  - { type: "OPEN", payload: { index: number }}
87
- - { type: "SETTLING" }
88
- - { type: "DRAGGING" }
90
+
91
+ - `canDismiss` - controls whether the user can dismiss the sheet via swipe/back/gesture (default: true)
92
+
93
+ - `onDismissPrevented` - called when a dismiss gesture is blocked by `canDismiss={false}`
89
94
 
90
95
  - `snapPoints` - a list of sizes that the sheet will "snap" to
91
96
 
@@ -97,6 +102,12 @@ export default function App() {
97
102
 
98
103
  ## Caveats
99
104
 
105
+ - iOS uses an overlay window to present the sheet.
106
+
107
+ - Default appearance values if not provided:
108
+ - iOS: grabber visible, white background, system default corner radius unless set
109
+ - Android: white background, 32dp top corner radius, dim amount 0.56
110
+
100
111
  - (Android) can have a maximum of 2 snap points
101
112
 
102
113
  - (Android) use the `nestedScrollEnabled` prop for nested scrollviews
@@ -31,17 +31,21 @@ class RNToolsSheetsModule : Module() {
31
31
  }
32
32
  }
33
33
 
34
- Events("onDismiss", "onStateChange")
34
+ Events("onDismiss", "onStateChange", "onDismissPrevented")
35
35
 
36
36
  Prop("isOpen") { view: RNToolsSheetsView, isOpen: Boolean ->
37
37
  view.props.isOpen = isOpen
38
38
  }
39
39
 
40
- Prop("openToIndex") { view: RNToolsSheetsView, openToIndex: Int ->
41
- view.props.openToIndex = openToIndex
40
+ Prop("initialIndex") { view: RNToolsSheetsView, initialIndex: Int ->
41
+ view.props.initialIndex = initialIndex
42
42
  }
43
43
 
44
- Prop("snapPoints") { view: RNToolsSheetsView, snapPoints: List<Int> ->
44
+ Prop("canDismiss") { view: RNToolsSheetsView, canDismiss: Boolean ->
45
+ view.props.canDismiss = canDismiss
46
+ }
47
+
48
+ Prop("snapPoints") { view: RNToolsSheetsView, snapPoints: List<Double> ->
45
49
  view.props.snapPoints = snapPoints.map { view.convertToPx(it) }
46
50
  }
47
51
 
@@ -13,10 +13,12 @@ import expo.modules.kotlin.AppContext
13
13
  import expo.modules.kotlin.viewevent.EventDispatcher
14
14
  import expo.modules.kotlin.views.ExpoView
15
15
  import android.graphics.Color
16
+ import com.facebook.react.bridge.UiThreadUtil
16
17
 
17
18
  class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
18
19
  val onDismiss by EventDispatcher()
19
20
  val onStateChange by EventDispatcher()
21
+ val onDismissPrevented by EventDispatcher()
20
22
 
21
23
  var rootViewGroup = SheetRootView(context, appContext)
22
24
  private var composeView: ComposeView
@@ -42,6 +44,15 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
42
44
  hideSheet()
43
45
  }
44
46
  }
47
+
48
+ LaunchedEffect(props.canDismiss) {
49
+ UiThreadUtil.runOnUiThread {
50
+ bottomSheetDialog?.apply {
51
+ setCancelable(true)
52
+ behavior.isHideable = props.canDismiss
53
+ }
54
+ }
55
+ }
45
56
  }
46
57
  }
47
58
 
@@ -54,95 +65,116 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
54
65
  }
55
66
 
56
67
  private fun hideSheet() {
57
- bottomSheetDialog?.dismiss()
58
- bottomSheetDialog = null
68
+ UiThreadUtil.runOnUiThread {
69
+ bottomSheetDialog?.dismiss()
70
+ bottomSheetDialog = null
71
+ }
59
72
  }
60
73
 
61
74
  private fun showSheet() {
62
- (rootViewGroup.parent as? ViewGroup)?.removeView(rootViewGroup)
75
+ UiThreadUtil.runOnUiThread {
76
+ (rootViewGroup.parent as? ViewGroup)?.removeView(rootViewGroup)
63
77
 
64
- val frameLayout = FrameLayout(context).apply {
65
- layoutParams = LayoutParams(
66
- ViewGroup.LayoutParams.MATCH_PARENT,
67
- ViewGroup.LayoutParams.WRAP_CONTENT
68
- )
78
+ val frameLayout = FrameLayout(context).apply {
79
+ layoutParams = LayoutParams(
80
+ ViewGroup.LayoutParams.MATCH_PARENT,
81
+ ViewGroup.LayoutParams.WRAP_CONTENT
82
+ )
69
83
 
70
- addView(rootViewGroup)
71
- }
72
-
73
- val snapPoints = props.snapPoints
74
- val initialIndex = props.openToIndex
75
-
76
- val hasTwoSnapPoints = snapPoints.size >= 2
77
- val peekHeight = if (hasTwoSnapPoints) snapPoints[0] else -1
78
- val expandedHeight = if (snapPoints.isNotEmpty()) snapPoints.getOrNull(1) ?: snapPoints[0] else -1
79
- val initialHeight = snapPoints.getOrNull(initialIndex) ?: peekHeight
84
+ addView(rootViewGroup)
85
+ }
80
86
 
81
- bottomSheetDialog = BottomSheetDialog(context).apply {
82
- setContentView(frameLayout)
87
+ val snapPoints = props.snapPoints
88
+ val initialIndex = props.initialIndex
89
+
90
+ val hasTwoSnapPoints = snapPoints.size >= 2
91
+ val peekHeight = if (hasTwoSnapPoints) snapPoints[0] else -1
92
+ val expandedHeight = if (snapPoints.isNotEmpty()) snapPoints.getOrNull(1) ?: snapPoints[0] else -1
93
+ val initialHeight = snapPoints.getOrNull(initialIndex) ?: peekHeight
94
+
95
+ bottomSheetDialog = PreventDismissBottomSheetDialog(
96
+ context = context,
97
+ canDismiss = { props.canDismiss },
98
+ onDismissPrevented = { onDismissPrevented(mapOf()) }
99
+ ).apply {
100
+ setCancelable(true)
101
+ setContentView(frameLayout)
102
+
103
+ window?.setDimAmount(props.dimAmount)
104
+
105
+ behavior.isHideable = props.canDismiss
106
+
107
+ window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.let { bottomSheet ->
108
+ val backgroundColor = props.backgroundColor?.let {
109
+ try {
110
+ Color.parseColor(it)
111
+ } catch (e: IllegalArgumentException) {
112
+ Color.WHITE
113
+ }
114
+ } ?: Color.WHITE
115
+
116
+ val radius = props.cornerRadius ?: 32f
117
+
118
+ val drawable = GradientDrawable().apply {
119
+ setColor(backgroundColor)
120
+ cornerRadii = floatArrayOf(
121
+ radius, radius,
122
+ radius, radius,
123
+ 0f, 0f,
124
+ 0f, 0f
125
+ )
126
+ }
83
127
 
84
- window?.setDimAmount(props.dimAmount)
128
+ bottomSheet.background = drawable
129
+ }
85
130
 
86
- window?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)?.let { bottomSheet ->
131
+ val behavior = behavior
87
132
 
88
- val backgroundColor = props.backgroundColor?.let {
89
- try {
90
- Color.parseColor(it) // Convert hex string to Color
91
- } catch (e: IllegalArgumentException) {
92
- Color.WHITE
133
+ setOnDismissListener {
134
+ onDismiss(mapOf())
135
+ if (behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
136
+ onStateChange(mapOf(
137
+ "type" to "HIDDEN",
138
+ ))
93
139
  }
94
- } ?: Color.WHITE
95
-
96
- val drawable = GradientDrawable().apply {
97
- setColor(backgroundColor)
98
- cornerRadius = props.cornerRadius ?: 0f
99
140
  }
100
141
 
101
- bottomSheet.background = drawable
102
- }
103
-
104
- val behavior = behavior
142
+ if (peekHeight > 0) {
143
+ behavior.peekHeight = peekHeight
144
+ }
105
145
 
106
- setOnDismissListener {
107
- onDismiss(mapOf())
108
- if (behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
109
- onStateChange(mapOf(
110
- "type" to "HIDDEN",
111
- ))
146
+ if (expandedHeight > 0) {
147
+ frameLayout.layoutParams.height = expandedHeight
148
+ frameLayout.requestLayout()
112
149
  }
113
- }
114
150
 
115
- if (peekHeight > 0) {
116
- behavior.peekHeight = peekHeight
117
- }
151
+ behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
152
+ override fun onStateChanged(bottomSheet: android.view.View, newState: Int) {
153
+ if (!props.canDismiss && newState == BottomSheetBehavior.STATE_HIDDEN) {
154
+ onDismissPrevented(mapOf())
155
+ }
118
156
 
119
- if (expandedHeight > 0) {
120
- frameLayout.layoutParams.height = expandedHeight
121
- frameLayout.requestLayout()
122
- }
157
+ handleSheetStateChange(newState)
158
+ }
123
159
 
124
- behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
125
- override fun onStateChanged(bottomSheet: android.view.View, newState: Int) {
126
- handleSheetStateChange(newState)
127
- }
160
+ override fun onSlide(bottomSheet: android.view.View, slideOffset: Float) {
161
+ }
162
+ })
128
163
 
129
- override fun onSlide(bottomSheet: android.view.View, slideOffset: Float) {
130
- }
131
- })
164
+ show()
132
165
 
133
- show()
166
+ if (initialHeight == peekHeight) {
167
+ behavior.state = BottomSheetBehavior.STATE_COLLAPSED
168
+ } else {
169
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
170
+ }
134
171
 
135
- if (initialHeight == peekHeight) {
136
- behavior.state = BottomSheetBehavior.STATE_COLLAPSED
137
- } else {
138
- behavior.state = BottomSheetBehavior.STATE_EXPANDED
172
+ handleSheetStateChange(behavior.state)
139
173
  }
140
-
141
- handleSheetStateChange(behavior.state)
142
174
  }
143
175
  }
144
176
 
145
- fun convertToPx(height: Int): Int {
177
+ fun convertToPx(height: Double): Int {
146
178
  val density = context.resources.displayMetrics.density
147
179
  return (height * density).toInt()
148
180
  }
@@ -155,12 +187,6 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
155
187
  ))
156
188
 
157
189
  }
158
- BottomSheetBehavior.STATE_SETTLING -> {
159
- onStateChange(mapOf(
160
- "type" to "SETTLING",
161
- ))
162
- }
163
-
164
190
  BottomSheetBehavior.STATE_COLLAPSED -> {
165
191
  onStateChange(mapOf(
166
192
  "type" to "OPEN",
@@ -188,12 +214,20 @@ class RNToolsSheetsView(context: Context, appContext: AppContext) : ExpoView(con
188
214
  ))
189
215
  }
190
216
 
191
- BottomSheetBehavior.STATE_DRAGGING -> {
192
- onStateChange(mapOf(
193
- "type" to "DRAGGING",
194
- ))
195
- }
196
217
  }
197
218
  }
198
219
  }
199
220
 
221
+ class PreventDismissBottomSheetDialog(
222
+ context: Context,
223
+ private val canDismiss: () -> Boolean,
224
+ private val onDismissPrevented: () -> Unit
225
+ ) : BottomSheetDialog(context) {
226
+ override fun cancel() {
227
+ if (canDismiss()) super.cancel() else onDismissPrevented()
228
+ }
229
+
230
+ override fun onBackPressed() {
231
+ if (canDismiss()) super.onBackPressed() else onDismissPrevented()
232
+ }
233
+ }
@@ -9,9 +9,10 @@ import expo.modules.kotlin.records.Record
9
9
 
10
10
  class SheetProps {
11
11
  var isOpen by mutableStateOf(false)
12
- var openToIndex by mutableIntStateOf(0)
12
+ var initialIndex by mutableIntStateOf(0)
13
13
  var snapPoints by mutableStateOf<List<Int>>(emptyList())
14
14
  lateinit var rootViewGroup: SheetRootView
15
+ var canDismiss by mutableStateOf(true)
15
16
 
16
17
  // Appearance props
17
18
  var dimAmount by mutableStateOf(0.56f)
@@ -1,5 +1,6 @@
1
1
  require 'json'
2
2
 
3
+ ENV['RCT_NEW_ARCH_ENABLED'] ||= '1'
3
4
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
5
  new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'
5
6
 
@@ -21,12 +22,11 @@ Pod::Spec.new do |s|
21
22
  s.static_framework = true
22
23
 
23
24
  s.dependency 'ExpoModulesCore'
24
- s.public_header_files = 'Sources/RNTSurfaceTouchHandlerWrapper.h'
25
-
25
+ s.public_header_files = 'Sources/RNToolsTouchHandlerHelper.h'
26
26
  # Swift/Objective-C compatibility
27
27
  s.pod_target_xcconfig = {
28
28
  'DEFINES_MODULE' => 'YES',
29
- 'OTHER_SWIFT_FLAGS' => new_arch_enabled ? '-DRCT_NEW_ARCH_ENABLED' : ''
29
+ 'OTHER_SWIFT_FLAGS' => '$(inherited)'
30
30
  }
31
31
 
32
32
  s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
@@ -1,28 +1,45 @@
1
1
  import ExpoModulesCore
2
2
 
3
+ struct SheetAppearance: Record {
4
+ @Field
5
+ var grabberVisible: Bool?
6
+
7
+ @Field
8
+ var backgroundColor: String?
9
+
10
+ @Field
11
+ var cornerRadius: Float?
12
+ }
13
+
3
14
  public class RNToolsSheetsModule: Module {
4
15
  public func definition() -> ModuleDefinition {
5
16
  Name("RNToolsSheets")
6
17
 
7
18
  View(RNToolsSheetsView.self) {
8
- Events("onDismiss", "onStateChange")
19
+ Events("onDismiss", "onStateChange", "onDismissPrevented")
9
20
 
10
- Prop("snapPoints") { (view, snapPoints: [Int]) in
11
- view.props.snapPoints = snapPoints
21
+ Prop("snapPoints") { (view, snapPoints: [CGFloat]) in
22
+ view.updateSnapPoints(snapPoints)
12
23
  }
13
24
 
14
25
  Prop("isOpen") { (view, isOpen: Bool) in
15
- view.props.isOpen = isOpen
26
+ view.updateIsOpen(isOpen)
16
27
  }
17
28
 
18
- Prop("openToIndex") { (view, openToIndex: Int) in
19
- view.props.openToIndex = openToIndex
29
+ Prop("initialIndex") { (view, initialIndex: Int) in
30
+ view.updateInitialIndex(initialIndex)
20
31
  }
21
-
32
+
22
33
  Prop("appearanceIOS") { (view, appearance: SheetAppearance) in
23
- view.props.grabberVisible = appearance.grabberVisible ?? true
24
- view.props.backgroundColor = appearance.backgroundColor
25
- view.props.cornerRadius = appearance.cornerRadius
34
+ view.updateAppearance(
35
+ grabberVisible: appearance.grabberVisible ?? true,
36
+ backgroundColor: appearance.backgroundColor,
37
+ cornerRadius: appearance.cornerRadius
38
+ )
39
+ }
40
+
41
+ Prop("canDismiss") { (view, canDismiss: Bool) in
42
+ view.updateCanDismiss(canDismiss)
26
43
  }
27
44
  }
28
45
  }
@@ -1,83 +1,106 @@
1
1
  import ExpoModulesCore
2
2
  import React
3
- import SwiftUI
3
+ import UIKit
4
4
 
5
- public class SheetProps: ObservableObject {
6
- @Published var children: [UIView] = []
7
- @Published var isOpen: Bool = false
8
- @Published var openToIndex: Int = 0
9
- @Published var snapPoints: [Int] = []
5
+ public class SheetProps {
6
+ var isOpen: Bool = false
7
+ var initialIndex: Int = 0
8
+ var snapPoints: [CGFloat] = []
9
+ var canDismiss: Bool = true
10
10
 
11
11
  // Appearance props
12
- @Published var grabberVisible: Bool = true
13
- @Published var backgroundColor: String? = nil
14
- @Published var cornerRadius: Float? = nil
12
+ var grabberVisible: Bool = true
13
+ var backgroundColor: String? = nil
14
+ var cornerRadius: Float? = nil
15
15
  }
16
16
 
17
- struct SheetAppearance: Record {
18
- @Field
19
- var grabberVisible: Bool?
20
-
21
- @Field
22
- var backgroundColor: String?
23
-
24
- @Field
25
- var cornerRadius: Float?
17
+ protocol RNToolsSheetsViewDelegate: AnyObject {
18
+ func handleSheetStateChange(index: Int)
19
+ func handleSheetDismissed()
20
+ func handleSheetCanDismiss() -> Bool
26
21
  }
27
22
 
28
- public class RNToolsSheetsView: ExpoView {
23
+ public class RNToolsSheetsView: ExpoView, RNToolsSheetsViewDelegate {
29
24
  public var props = SheetProps()
25
+
30
26
  var onDismiss = EventDispatcher()
31
27
  var onStateChange = EventDispatcher()
28
+ var onDismissPrevented = EventDispatcher()
32
29
 
33
- var touchHandler: RCTTouchHandler?
34
-
35
- private lazy var sheetVC = SheetInternalViewController()
36
-
37
- lazy var hostingController = UIHostingController(
38
- rootView: ContentView(
39
- props: props, sheetVC: sheetVC, onDismiss: onDismiss,
40
- onStateChange: onStateChange))
30
+ private lazy var sheetVC = SheetViewController()
41
31
 
42
32
  required init(appContext: AppContext? = nil) {
43
33
  super.init(appContext: appContext)
44
34
 
45
- if let bridge = appContext?.reactBridge {
46
- sheetVC.bridge = bridge
35
+ sheetVC.appContext = appContext
36
+ sheetVC.delegate = self
37
+ }
38
+
39
+ deinit {
40
+ sheetVC.cleanup()
41
+ }
42
+
43
+ func updateSnapPoints(_ snapPoints: [CGFloat]) {
44
+ props.snapPoints = snapPoints
45
+ }
46
+
47
+ func updateIsOpen(_ isOpen: Bool) {
48
+ props.isOpen = isOpen
49
+ if isOpen {
50
+ sheetVC.presentSheet(
51
+ openTo: props.initialIndex,
52
+ snapPoints: props.snapPoints,
53
+ grabberVisible: props.grabberVisible,
54
+ backgroundColor: props.backgroundColor,
55
+ cornerRadius: props.cornerRadius
56
+ )
57
+ } else {
58
+ sheetVC.dismissSheet()
47
59
  }
60
+ }
48
61
 
49
- hostingController.view.autoresizingMask = [
50
- .flexibleWidth, .flexibleHeight,
51
- ]
52
- hostingController.view.backgroundColor = UIColor.clear
62
+ func updateInitialIndex(_ initialIndex: Int) {
63
+ props.initialIndex = initialIndex
53
64
  }
54
65
 
55
- public override func layoutSubviews() {
56
- super.layoutSubviews()
57
- hostingController.view.frame = bounds
66
+ func updateCanDismiss(_ canDismiss: Bool) {
67
+ props.canDismiss = canDismiss
58
68
  }
59
69
 
60
- public override func didMoveToSuperview() {
61
- super.didMoveToSuperview()
62
- let parentViewController = findParentViewControllerOrNil()
70
+ func updateAppearance(
71
+ grabberVisible: Bool,
72
+ backgroundColor: String?,
73
+ cornerRadius: Float?
74
+ ) {
75
+ props.grabberVisible = grabberVisible
76
+ props.backgroundColor = backgroundColor
77
+ props.cornerRadius = cornerRadius
78
+ }
63
79
 
64
- parentViewController.apply {
65
- $0.addChild(hostingController)
66
- }
80
+ func handleSheetDismissed() {
81
+ onDismiss([:])
82
+ onStateChange(["type": "HIDDEN"])
83
+ }
67
84
 
68
- hostingController.view.layer.removeAllAnimations()
69
- hostingController.view.frame = bounds
70
- addSubview(hostingController.view)
71
- parentViewController.apply {
72
- hostingController.didMove(toParent: $0)
85
+ func handleSheetStateChange(index: Int) {
86
+ onStateChange([
87
+ "type": "OPEN",
88
+ "payload": ["index": index],
89
+ ])
90
+ }
91
+
92
+ func handleSheetCanDismiss() -> Bool {
93
+ if !props.canDismiss {
94
+ onDismissPrevented([:])
73
95
  }
96
+ return props.canDismiss
74
97
  }
75
98
 
76
99
  public override func reactSubviews() -> [UIView]! {
77
100
  return []
78
101
  }
79
102
 
80
- #if RCT_NEW_ARCH_ENABLED
103
+
81
104
  public override func mountChildComponentView(
82
105
  _ childComponentView: UIView,
83
106
  index: Int
@@ -91,164 +114,109 @@ public class RNToolsSheetsView: ExpoView {
91
114
  ) {
92
115
  childComponentView.removeFromSuperview()
93
116
  }
117
+ // public override func insertReactSubview(
118
+ // _ subview: UIView!, at atIndex: Int
119
+ // ) {
120
+ // sheetVC.insertChild(subview, at: atIndex)
121
+ // }
122
+ //
123
+ // public override func removeReactSubview(_ subview: UIView!) {
124
+ // sheetVC.removeChild(subview)
125
+ // }
94
126
 
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
- }
127
+ }
105
128
 
106
- #endif
129
+ final class SheetViewController: UIViewController,
130
+ UISheetPresentationControllerDelegate
131
+ {
132
+ weak var delegate: RNToolsSheetsViewDelegate?
107
133
 
108
- }
134
+ var appContext: AppContext? {
135
+ didSet {
136
+ _ = view
137
+ }
138
+ }
109
139
 
110
- struct ContentView: View {
111
- @ObservedObject var props: SheetProps
112
- var sheetVC: SheetInternalViewController
113
- var onDismiss: EventDispatcher
114
- var onStateChange: EventDispatcher
140
+ @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
115
141
 
116
- @State private var selectedDetent: PresentationDetent = .height(400.0)
117
- @State private var lastHeight: CGFloat = 0
118
- @State private var isDragging = false
119
- @State private var settleTimer: Timer?
142
+ init() {
143
+ super.init(nibName: nil, bundle: nil)
144
+ view.backgroundColor = .white
145
+ }
120
146
 
121
- private var detents: [PresentationDetent] {
122
- props.snapPoints.map { .height(CGFloat($0)) }
147
+ override func loadView() {
148
+ self.view = UIView()
149
+ RNToolsTouchHandlerHelper.createAndAttachTouchHandler(for: self.view)
123
150
  }
151
+
124
152
 
125
- private func detent(for index: Int?) -> PresentationDetent {
126
- guard
127
- let i = index,
128
- detents.indices.contains(i)
129
- else { return detents.first! }
130
- return detents[i]
153
+ deinit {
154
+ overlayWindow = nil
131
155
  }
132
156
 
133
- private func upperSnapIndex(
134
- for height: CGFloat,
135
- snapPoints: [Int]
136
- ) -> Int {
137
- guard !snapPoints.isEmpty else { return 0 }
157
+ private let detentTag = UUID().uuidString
138
158
 
139
- let sorted = snapPoints.sorted()
140
- if let i = sorted.firstIndex(where: { CGFloat($0) >= height }) {
141
- return i
142
- }
143
- return sorted.count - 1
144
- }
159
+ private var overlayWindow: UIWindow?
145
160
 
146
- var body: some View {
147
-
148
- Color.clear
149
- .sheet(
150
- isPresented: $props.isOpen,
151
- onDismiss: {
152
- onDismiss([:])
153
- onStateChange(["type": "HIDDEN"])
154
- }
155
- ) {
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
- }
167
-
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
- }
188
- }
189
- }
190
-
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)
212
- }
161
+ func presentSheet(
162
+ openTo index: Int = 0,
163
+ snapPoints: [CGFloat],
164
+ grabberVisible: Bool,
165
+ backgroundColor: String?,
166
+ cornerRadius: Float?
167
+ ) {
168
+ guard overlayWindow == nil else { return }
169
+
170
+ modalPresentationStyle = .pageSheet
171
+
172
+ if let sheet = sheetPresentationController {
173
+ sheet.delegate = self
174
+ sheet.prefersGrabberVisible = grabberVisible
175
+ sheet.detents = makeDetents(from: snapPoints)
176
+
177
+ if let radius = cornerRadius {
178
+ sheet.preferredCornerRadius = CGFloat(radius)
213
179
  }
214
- }
215
- }
216
180
 
217
- struct SheetInternalVCRepresentable: UIViewControllerRepresentable {
218
- let controller: SheetInternalViewController
181
+ let detents = sheet.detents
182
+ if detents.indices.contains(index) {
183
+ sheet.selectedDetentIdentifier = detents[index].identifier
184
+ } else {
185
+ sheet.selectedDetentIdentifier = detents.first?.identifier
186
+ }
187
+ }
219
188
 
220
- func makeUIViewController(context: Context) -> SheetInternalViewController {
221
- controller
222
- }
223
- func updateUIViewController(
224
- _ uiViewController: SheetInternalViewController,
225
- context: Context
226
- ) {}
227
- }
189
+ view.backgroundColor = UIColor(hex: backgroundColor) ?? .white
228
190
 
229
- final class SheetInternalViewController: UIViewController {
230
- var bridge: RCTBridge? {
231
- didSet {
232
- touchHandler = RCTTouchHandler(bridge: bridge)
233
- self.touchHandler?.attach(to: self.view)
191
+ let w = UIWindow(frame: UIScreen.main.bounds)
192
+ w.windowLevel = .statusBar + 2
193
+ w.rootViewController = UIViewController()
194
+ w.makeKeyAndVisible()
195
+
196
+ overlayWindow = w
197
+
198
+ let host = UIViewController()
199
+ host.modalPresentationStyle = .overFullScreen
200
+ host.view.backgroundColor = .clear
201
+
202
+ w.rootViewController?.present(host, animated: false) {
203
+ host.present(self, animated: true)
234
204
  }
235
205
  }
236
- var surfaceTouchHandler = RNTSurfaceTouchHandlerWrapper()
237
- var touchHandler: RCTTouchHandler?
238
206
 
239
- init() {
240
- super.init(nibName: nil, bundle: nil)
241
- view.backgroundColor = .clear
207
+ func dismissSheet() {
208
+ dismiss(animated: true) { [weak self] in
209
+ self?.delegate?.handleSheetDismissed()
210
+ self?.overlayWindow?.isHidden = true
211
+ self?.overlayWindow = nil
212
+ }
242
213
  }
243
214
 
244
- override func loadView() {
245
- self.view = UIView()
246
-
247
- self.surfaceTouchHandler.attach(to: self.view)
215
+ func cleanup() {
216
+ overlayWindow?.isHidden = true
217
+ overlayWindow = nil
248
218
  }
249
219
 
250
- @available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
251
-
252
220
  func insertChild(_ child: UIView, at index: Int) {
253
221
  view.insertSubview(child, at: index)
254
222
 
@@ -257,64 +225,67 @@ final class SheetInternalViewController: UIViewController {
257
225
  func removeChild(_ child: UIView) {
258
226
  child.removeFromSuperview()
259
227
  }
260
- }
261
228
 
262
- extension Optional {
263
- func apply(_ fn: (Wrapped) -> Void) {
264
- if case let .some(val) = self {
265
- fn(val)
266
- }
229
+ func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
230
+ _ sheetPresentationController: UISheetPresentationController
231
+ ) {
232
+ guard
233
+ let selectedID = sheetPresentationController
234
+ .selectedDetentIdentifier,
235
+ let index = sheetPresentationController.detents
236
+ .firstIndex(where: { $0.identifier == selectedID })
237
+ else { return }
238
+
239
+ delegate?.handleSheetStateChange(index: index)
267
240
  }
268
- }
269
241
 
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
242
+ func presentationControllerShouldDismiss(
243
+ _ presentationController: UIPresentationController
244
+ ) -> Bool {
245
+ if let d = delegate {
246
+ return d.handleSheetCanDismiss()
278
247
  }
279
248
 
280
- return nextResponder as? UIViewController
249
+ return true
281
250
  }
282
- }
283
-
284
- extension Color {
285
- init(hex: String) {
286
- var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
287
- hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
288
251
 
289
- var rgb: UInt64 = 0
290
- Scanner(string: hexSanitized).scanHexInt64(&rgb)
252
+ func presentationControllerDidDismiss(
253
+ _ presentationController: UIPresentationController
254
+ ) {
255
+ delegate?.handleSheetDismissed()
256
+ cleanup()
257
+ }
291
258
 
292
- let red = Double((rgb & 0xFF0000) >> 16) / 255.0
293
- let green = Double((rgb & 0x00FF00) >> 8) / 255.0
294
- let blue = Double(rgb & 0x0000FF) / 255.0
259
+ private func makeDetents(from points: [CGFloat])
260
+ -> [UISheetPresentationController.Detent]
261
+ {
262
+ guard !points.isEmpty else { return [.large()] }
295
263
 
296
- self.init(red: red, green: green, blue: blue)
264
+ return points.enumerated().map { idx, raw in
265
+ .custom(identifier: .init("\(detentTag)_\(idx)")) { _ in
266
+ return raw
267
+ }
268
+ }
297
269
  }
270
+
298
271
  }
299
272
 
273
+ extension UIColor {
274
+ convenience init?(hex: String?) {
275
+ guard let hex, !hex.isEmpty else { return nil }
300
276
 
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
- }
277
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
278
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
310
279
 
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
- }
280
+ guard hexSanitized.count == 6 else { return nil }
281
+
282
+ var rgb: UInt64 = 0
283
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
284
+
285
+ let red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
286
+ let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
287
+ let blue = CGFloat(rgb & 0x0000FF) / 255.0
288
+
289
+ self.init(red: red, green: green, blue: blue, alpha: 1.0)
318
290
  }
319
291
  }
320
-
@@ -0,0 +1,15 @@
1
+ // RNToolsTouchHandlerHelper.h
2
+
3
+ #import <Foundation/Foundation.h>
4
+ #import <UIKit/UIKit.h>
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ @interface RNToolsTouchHandlerHelper : NSObject
9
+
10
+ + (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view;
11
+ + (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view;
12
+
13
+ @end
14
+
15
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,31 @@
1
+ // RNToolsTouchHandlerHelper.mm
2
+
3
+ #import "RNToolsTouchHandlerHelper.h"
4
+ #import <React/RCTSurfaceTouchHandler.h>
5
+
6
+ @implementation RNToolsTouchHandlerHelper
7
+
8
+ + (nullable UIGestureRecognizer *)createAndAttachTouchHandlerForView:(UIView *)view {
9
+ for (UIGestureRecognizer *recognizer in [view.gestureRecognizers copy]) {
10
+ if ([recognizer isKindOfClass:[RCTSurfaceTouchHandler class]]) {
11
+ return nil;
12
+ }
13
+ }
14
+
15
+ RCTSurfaceTouchHandler *touchHandler = [[RCTSurfaceTouchHandler alloc] init];
16
+ [touchHandler attachToView:view];
17
+ return touchHandler;
18
+ }
19
+
20
+ + (void)detachTouchHandler:(nullable UIGestureRecognizer *)handler fromView:(UIView *)view {
21
+ if (!handler) {
22
+ return;
23
+ }
24
+
25
+ if ([handler isKindOfClass:[RCTSurfaceTouchHandler class]]) {
26
+ RCTSurfaceTouchHandler *touchHandler = (RCTSurfaceTouchHandler *)handler;
27
+ [touchHandler detachFromView:view];
28
+ }
29
+ }
30
+
31
+ @end
package/package.json CHANGED
@@ -1,16 +1,9 @@
1
1
  {
2
2
  "name": "@rn-tools/sheets",
3
- "version": "0.1.4",
4
- "description": "My new module",
3
+ "version": "3.0.1",
4
+ "description": "A React Native library for creating and managing native sheets in Expo applications.",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
- "build": "expo-module build",
8
- "clean": "expo-module clean",
9
- "lint": "expo-module lint",
10
- "test": "expo-module test",
11
- "prepare": "expo-module prepare",
12
- "prepublishOnly": "expo-module prepublishOnly",
13
- "expo-module": "expo-module",
14
7
  "open:ios": "xed example/ios",
15
8
  "open:android": "open -a \"Android Studio\" example/android"
16
9
  },
@@ -28,10 +21,10 @@
28
21
  "license": "MIT",
29
22
  "homepage": "https://github.com/ajsmth/rn-tools#readme",
30
23
  "devDependencies": {
31
- "@types/react": "~18.3.12",
32
- "expo": "~52.0.0",
33
- "expo-module-scripts": "^4.0.3",
34
- "react-native": "0.76.0"
24
+ "@types/react": "18.3.12"
25
+ },
26
+ "dependencies": {
27
+ "@rn-tools/core": "3.0.1"
35
28
  },
36
29
  "peerDependencies": {
37
30
  "expo": "*",
@@ -6,8 +6,11 @@ import {
6
6
  ViewStyle,
7
7
  Platform,
8
8
  LayoutChangeEvent,
9
+ StyleSheet,
10
+ useWindowDimensions,
9
11
  } from "react-native";
10
12
  import { requireNativeViewManager } from "expo-modules-core";
13
+ import { useSafeAreaInsets } from "@rn-tools/core";
11
14
 
12
15
  type SheetState = "DRAGGING" | "OPEN" | "SETTLING" | "HIDDEN";
13
16
 
@@ -17,15 +20,9 @@ type ChangeEvent<T extends SheetState, P = unknown> = {
17
20
  };
18
21
 
19
22
  type OpenChangeEvent = ChangeEvent<"OPEN", { index: number }>;
20
- type DraggingChangeEvent = ChangeEvent<"DRAGGING">;
21
- type SettlingChangeEvent = ChangeEvent<"SETTLING">;
22
23
  type HiddenChangeEvent = ChangeEvent<"HIDDEN">;
23
24
 
24
- type SheetChangeEvent =
25
- | OpenChangeEvent
26
- | DraggingChangeEvent
27
- | SettlingChangeEvent
28
- | HiddenChangeEvent;
25
+ type SheetChangeEvent = OpenChangeEvent | HiddenChangeEvent;
29
26
 
30
27
  type NativeOnChangeEvent = NativeSyntheticEvent<SheetChangeEvent>;
31
28
 
@@ -45,8 +42,10 @@ type NativeSheetViewProps = {
45
42
  children: React.ReactNode;
46
43
  snapPoints?: number[];
47
44
  isOpen: boolean;
48
- openToIndex: number;
45
+ initialIndex: number;
49
46
  onDismiss: () => void;
47
+ canDismiss?: boolean;
48
+ onDismissPrevented: () => void;
50
49
  onStateChange: (event: NativeOnChangeEvent) => void;
51
50
  appearanceAndroid?: AppearanceAndroid;
52
51
  appearanceIOS?: AppearanceIOS;
@@ -60,17 +59,15 @@ export type BottomSheetProps = {
60
59
  containerStyle?: ViewStyle;
61
60
  snapPoints?: number[];
62
61
  isOpen: boolean;
63
- openToIndex?: number;
64
- onOpenChange: (isOpen: boolean) => void;
62
+ initialIndex?: number;
63
+ setIsOpen: (isOpen: boolean) => void;
64
+ canDismiss?: boolean;
65
+ onDismissPrevented?: () => void;
65
66
  onStateChange?: (event: SheetChangeEvent) => void;
66
67
  appearanceAndroid?: AppearanceAndroid;
67
68
  appearanceIOS?: AppearanceIOS;
68
69
  };
69
70
 
70
- // TODO:
71
- // - get sheet container height from native side and clamp maxHeight to that value
72
- //
73
-
74
71
  export function BottomSheet(props: BottomSheetProps) {
75
72
  const {
76
73
  onStateChange,
@@ -78,12 +75,21 @@ export function BottomSheet(props: BottomSheetProps) {
78
75
  snapPoints = [],
79
76
  containerStyle,
80
77
  isOpen,
81
- openToIndex = 0,
82
- onOpenChange: setIsOpen,
78
+ initialIndex = 0,
79
+ setIsOpen,
83
80
  appearanceAndroid,
84
81
  appearanceIOS,
82
+ canDismiss = true,
83
+ onDismissPrevented,
85
84
  } = props;
86
85
 
86
+ const { height: windowHeight } = useWindowDimensions();
87
+ const insets = useSafeAreaInsets();
88
+ const maxSheetHeight = React.useMemo(
89
+ () => Math.max(0, windowHeight - insets.top - insets.bottom),
90
+ [windowHeight, insets.top, insets.bottom],
91
+ );
92
+
87
93
  const [layout, setLayout] = React.useState<LayoutRectangle>({
88
94
  height: 0,
89
95
  width: 0,
@@ -92,12 +98,32 @@ export function BottomSheet(props: BottomSheetProps) {
92
98
  });
93
99
 
94
100
  const computedSnapPoints = React.useMemo(() => {
95
- if (snapPoints.length === 0 && layout.height > 0) {
96
- return [layout.height];
101
+ if (snapPoints.length === 0) {
102
+ if (layout.height === 0) {
103
+ return [];
104
+ }
105
+
106
+ return [Math.min(layout.height, maxSheetHeight)];
107
+ }
108
+
109
+ let effectiveSnapPoints =
110
+ Platform.OS === "android" ? snapPoints.slice(0, 2) : [...snapPoints];
111
+
112
+ const snapPointsExceedingMaxHeight = snapPoints.filter(
113
+ (snapPoint) => snapPoint >= maxSheetHeight,
114
+ );
115
+
116
+ if (snapPointsExceedingMaxHeight.length > 0) {
117
+ effectiveSnapPoints = [
118
+ ...effectiveSnapPoints.filter(
119
+ (snapPoint) => snapPoint < maxSheetHeight,
120
+ ),
121
+ maxSheetHeight,
122
+ ];
97
123
  }
98
124
 
99
- return Platform.OS === "android" ? snapPoints.slice(0, 2) : [...snapPoints];
100
- }, [snapPoints, layout]);
125
+ return effectiveSnapPoints;
126
+ }, [layout.height, maxSheetHeight, snapPoints]);
101
127
 
102
128
  const maxHeight = React.useMemo(
103
129
  () =>
@@ -110,9 +136,12 @@ export function BottomSheet(props: BottomSheetProps) {
110
136
  const style = React.useMemo(() => {
111
137
  return {
112
138
  height: maxHeight,
139
+ borderTopLeftRadius: 16,
140
+ borderTopRightRadius: 16,
141
+ backgroundColor: "white",
113
142
  ...containerStyle,
114
143
  };
115
- }, [maxHeight]);
144
+ }, [maxHeight, containerStyle]);
116
145
 
117
146
  const handleOnDismiss = React.useCallback(() => {
118
147
  setIsOpen(false);
@@ -129,19 +158,48 @@ export function BottomSheet(props: BottomSheetProps) {
129
158
  setLayout(event.nativeEvent.layout);
130
159
  }, []);
131
160
 
161
+ const handleDismissWithChanges = React.useCallback(() => {
162
+ onDismissPrevented?.();
163
+ }, [onDismissPrevented]);
164
+
165
+ const isAutosized = React.useMemo(
166
+ () => snapPoints.length === 0,
167
+ [snapPoints],
168
+ );
169
+
170
+ const pointerEvents = React.useMemo(() => {
171
+ return isOpen ? "box-none" : "none";
172
+ }, [isOpen]);
173
+
174
+ const computedIsOpen = React.useMemo(
175
+ () => isOpen && computedSnapPoints.length > 0,
176
+ [isOpen, computedSnapPoints],
177
+ );
178
+
179
+ const innerStyle = React.useMemo(
180
+ () => (isAutosized ? undefined : StyleSheet.absoluteFill),
181
+ [isAutosized],
182
+ );
183
+
132
184
  return (
133
- <NativeSheetsView
134
- isOpen={isOpen}
135
- openToIndex={openToIndex}
136
- onDismiss={handleOnDismiss}
137
- onStateChange={handleStateChange}
138
- snapPoints={computedSnapPoints}
139
- appearanceAndroid={appearanceAndroid}
140
- appearanceIOS={appearanceIOS}
141
- >
142
- <View style={style} onLayout={handleLayout}>
143
- {children}
144
- </View>
145
- </NativeSheetsView>
185
+ <View style={StyleSheet.absoluteFill} pointerEvents={pointerEvents}>
186
+ <NativeSheetsView
187
+ isOpen={computedIsOpen}
188
+ canDismiss={canDismiss}
189
+ initialIndex={initialIndex}
190
+ onDismiss={handleOnDismiss}
191
+ onStateChange={handleStateChange}
192
+ onDismissPrevented={handleDismissWithChanges}
193
+ snapPoints={computedSnapPoints}
194
+ appearanceAndroid={appearanceAndroid}
195
+ appearanceIOS={appearanceIOS}
196
+ >
197
+ <View style={style} collapsable={false}>
198
+ <View onLayout={handleLayout} style={innerStyle} collapsable={false}>
199
+ {children}
200
+ </View>
201
+ </View>
202
+ </NativeSheetsView>
203
+ </View>
146
204
  );
147
205
  }
@@ -1,11 +0,0 @@
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
-
@@ -1,43 +0,0 @@
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
-