@lodev09/react-native-true-sheet 3.0.0-beta.7 → 3.0.0-beta.8
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/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +51 -49
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +10 -18
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +76 -20
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +21 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +81 -147
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +303 -409
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +116 -0
- package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +33 -5
- package/ios/TrueSheetContainerView.h +20 -2
- package/ios/TrueSheetContainerView.mm +61 -14
- package/ios/TrueSheetContentView.h +4 -2
- package/ios/TrueSheetContentView.mm +29 -72
- package/ios/TrueSheetFooterView.mm +2 -2
- package/ios/TrueSheetHeaderView.h +29 -0
- package/ios/TrueSheetHeaderView.mm +60 -0
- package/ios/TrueSheetView.mm +178 -232
- package/ios/TrueSheetViewController.h +1 -2
- package/ios/TrueSheetViewController.mm +126 -236
- package/ios/utils/LayoutUtil.h +2 -1
- package/ios/utils/LayoutUtil.mm +14 -1
- package/lib/module/TrueSheet.js +10 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +9 -9
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
- package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
- package/package.json +4 -1
- package/src/TrueSheet.tsx +10 -0
- package/src/TrueSheet.types.ts +10 -11
- package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
|
@@ -37,7 +37,7 @@ class TrueSheetViewManager :
|
|
|
37
37
|
|
|
38
38
|
override fun onAfterUpdateTransaction(view: TrueSheetView) {
|
|
39
39
|
super.onAfterUpdateTransaction(view)
|
|
40
|
-
view.
|
|
40
|
+
view.finalizeUpdates()
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
override fun addEventEmitters(reactContext: ThemedReactContext, view: TrueSheetView) {
|
|
@@ -97,9 +97,7 @@ class TrueSheetViewManager :
|
|
|
97
97
|
|
|
98
98
|
@ReactProp(name = "cornerRadius", defaultDouble = -1.0)
|
|
99
99
|
override fun setCornerRadius(view: TrueSheetView, radius: Double) {
|
|
100
|
-
|
|
101
|
-
view.setCornerRadius(radius.dpToPx())
|
|
102
|
-
}
|
|
100
|
+
view.setCornerRadius(radius.dpToPx())
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
@ReactProp(name = "grabber", defaultBoolean = true)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
4
|
+
import androidx.fragment.app.Fragment
|
|
5
|
+
import androidx.fragment.app.FragmentManager
|
|
6
|
+
import com.facebook.react.bridge.ReactContext
|
|
7
|
+
|
|
8
|
+
private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Observes fragment lifecycle to detect react-native-screens modal presentation.
|
|
12
|
+
* Automatically notifies when modals are presented/dismissed.
|
|
13
|
+
*/
|
|
14
|
+
class RNScreensFragmentObserver(
|
|
15
|
+
private val reactContext: ReactContext,
|
|
16
|
+
private val onModalPresented: () -> Unit,
|
|
17
|
+
private val onModalDismissed: () -> Unit
|
|
18
|
+
) {
|
|
19
|
+
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
|
|
20
|
+
private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if there are active modal fragments being tracked.
|
|
24
|
+
*/
|
|
25
|
+
fun hasActiveModals(): Boolean = activeModalFragments.isNotEmpty()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start observing fragment lifecycle events.
|
|
29
|
+
*/
|
|
30
|
+
fun start() {
|
|
31
|
+
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
|
|
32
|
+
val fragmentManager = activity.supportFragmentManager
|
|
33
|
+
|
|
34
|
+
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
35
|
+
override fun onFragmentAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
|
|
36
|
+
super.onFragmentAttached(fm, fragment, context)
|
|
37
|
+
|
|
38
|
+
if (isModalFragment(fragment)) {
|
|
39
|
+
activeModalFragments.add(fragment)
|
|
40
|
+
|
|
41
|
+
// Notify when the first modal is attached
|
|
42
|
+
if (activeModalFragments.size == 1) {
|
|
43
|
+
onModalPresented()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override fun onFragmentStopped(fm: FragmentManager, fragment: Fragment) {
|
|
49
|
+
super.onFragmentStopped(fm, fragment)
|
|
50
|
+
|
|
51
|
+
if (activeModalFragments.contains(fragment)) {
|
|
52
|
+
activeModalFragments.remove(fragment)
|
|
53
|
+
|
|
54
|
+
// Notify when all modals are dismissed
|
|
55
|
+
if (activeModalFragments.isEmpty()) {
|
|
56
|
+
onModalDismissed()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback!!, true)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Stop observing and cleanup.
|
|
67
|
+
*/
|
|
68
|
+
fun stop() {
|
|
69
|
+
fragmentLifecycleCallback?.let { callback ->
|
|
70
|
+
val activity = reactContext.currentActivity as? AppCompatActivity
|
|
71
|
+
activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
|
|
72
|
+
}
|
|
73
|
+
fragmentLifecycleCallback = null
|
|
74
|
+
activeModalFragments.clear()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
companion object {
|
|
78
|
+
/**
|
|
79
|
+
* Check if fragment is from react-native-screens.
|
|
80
|
+
*/
|
|
81
|
+
private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
|
|
85
|
+
* Uses reflection to check the fragment's screen.stackPresentation property.
|
|
86
|
+
*/
|
|
87
|
+
private fun isModalFragment(fragment: Fragment): Boolean {
|
|
88
|
+
val className = fragment.javaClass.name
|
|
89
|
+
|
|
90
|
+
if (!isScreensFragment(fragment)) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ScreenModalFragment is always a modal (used for formSheet with BottomSheetDialog)
|
|
95
|
+
if (className.contains("ScreenModalFragment")) {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For ScreenStackFragment, check the screen's stackPresentation via reflection
|
|
100
|
+
try {
|
|
101
|
+
val getScreenMethod = fragment.javaClass.getMethod("getScreen")
|
|
102
|
+
val screen = getScreenMethod.invoke(fragment) ?: return false
|
|
103
|
+
|
|
104
|
+
val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
|
|
105
|
+
val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
|
|
106
|
+
|
|
107
|
+
val presentationName = stackPresentation.toString()
|
|
108
|
+
return presentationName == "MODAL" ||
|
|
109
|
+
presentationName == "TRANSPARENT_MODAL" ||
|
|
110
|
+
presentationName == "FORM_SHEET"
|
|
111
|
+
} catch (e: Exception) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -89,14 +89,42 @@ object ScreenUtils {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Get the
|
|
92
|
+
* Get the screen width
|
|
93
93
|
*
|
|
94
|
-
* @param
|
|
95
|
-
* @return
|
|
94
|
+
* @param context React context
|
|
95
|
+
* @return Screen width in pixels
|
|
96
96
|
*/
|
|
97
|
-
fun
|
|
97
|
+
fun getScreenWidth(context: ReactContext): Int {
|
|
98
|
+
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
99
|
+
|
|
100
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
101
|
+
val windowMetrics = windowManager.currentWindowMetrics
|
|
102
|
+
windowMetrics.bounds.width()
|
|
103
|
+
} else {
|
|
104
|
+
val displayMetrics = DisplayMetrics()
|
|
105
|
+
@Suppress("DEPRECATION")
|
|
106
|
+
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
107
|
+
displayMetrics.widthPixels
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the location of a view in screen coordinates
|
|
113
|
+
*
|
|
114
|
+
* @param view The view to get screen location for
|
|
115
|
+
* @return IntArray with [x, y] coordinates in screen space
|
|
116
|
+
*/
|
|
117
|
+
fun getScreenLocation(view: View): IntArray {
|
|
98
118
|
val location = IntArray(2)
|
|
99
119
|
view.getLocationOnScreen(location)
|
|
100
|
-
return location
|
|
120
|
+
return location
|
|
101
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get the Y coordinate of a view in screen coordinates
|
|
125
|
+
*
|
|
126
|
+
* @param view The view to get screen Y coordinate for
|
|
127
|
+
* @return Y coordinate in screen space
|
|
128
|
+
*/
|
|
129
|
+
fun getScreenY(view: View): Int = getScreenLocation(view)[1]
|
|
102
130
|
}
|
|
@@ -21,6 +21,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
21
21
|
*/
|
|
22
22
|
- (void)containerViewContentDidChangeSize:(CGSize)newSize;
|
|
23
23
|
|
|
24
|
+
@optional
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Called when the header size changes
|
|
28
|
+
* @param newSize The new size of the header
|
|
29
|
+
*/
|
|
30
|
+
- (void)containerViewHeaderDidChangeSize:(CGSize)newSize;
|
|
31
|
+
|
|
24
32
|
@end
|
|
25
33
|
|
|
26
34
|
@interface TrueSheetContainerView : RCTViewComponentView
|
|
@@ -30,20 +38,30 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
30
38
|
*/
|
|
31
39
|
@property (nonatomic, weak, nullable) id<TrueSheetContainerViewDelegate> delegate;
|
|
32
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Enable ScrollView pinning
|
|
43
|
+
*/
|
|
44
|
+
@property (nonatomic, assign) BOOL scrollViewPinningEnabled;
|
|
45
|
+
|
|
33
46
|
/**
|
|
34
47
|
* Returns the current content height
|
|
35
48
|
*/
|
|
36
49
|
- (CGFloat)contentHeight;
|
|
37
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Returns the current header height
|
|
53
|
+
*/
|
|
54
|
+
- (CGFloat)headerHeight;
|
|
55
|
+
|
|
38
56
|
/**
|
|
39
57
|
* Updates footer layout constraints if needed
|
|
40
58
|
*/
|
|
41
59
|
- (void)layoutFooter;
|
|
42
60
|
|
|
43
61
|
/**
|
|
44
|
-
*
|
|
62
|
+
* Setup ScrollView pinning
|
|
45
63
|
*/
|
|
46
|
-
- (void)setupContentScrollViewPinning
|
|
64
|
+
- (void)setupContentScrollViewPinning;
|
|
47
65
|
|
|
48
66
|
@end
|
|
49
67
|
|
|
@@ -9,26 +9,32 @@
|
|
|
9
9
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
10
10
|
|
|
11
11
|
#import "TrueSheetContainerView.h"
|
|
12
|
+
#import "TrueSheetContentView.h"
|
|
13
|
+
#import "TrueSheetFooterView.h"
|
|
14
|
+
#import "TrueSheetHeaderView.h"
|
|
15
|
+
|
|
12
16
|
#import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
|
|
13
17
|
#import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
14
18
|
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
15
19
|
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
16
|
-
#import "TrueSheetContentView.h"
|
|
17
|
-
#import "TrueSheetFooterView.h"
|
|
18
20
|
|
|
19
21
|
#import <React/RCTConversions.h>
|
|
20
22
|
#import <React/RCTLog.h>
|
|
21
23
|
|
|
22
24
|
using namespace facebook::react;
|
|
23
25
|
|
|
24
|
-
@interface TrueSheetContainerView () <TrueSheetContentViewDelegate>
|
|
26
|
+
@interface TrueSheetContainerView () <TrueSheetContentViewDelegate, TrueSheetHeaderViewDelegate>
|
|
25
27
|
@end
|
|
26
28
|
|
|
27
29
|
@implementation TrueSheetContainerView {
|
|
28
30
|
TrueSheetContentView *_contentView;
|
|
31
|
+
TrueSheetHeaderView *_headerView;
|
|
29
32
|
TrueSheetFooterView *_footerView;
|
|
33
|
+
BOOL _scrollViewPinningSet;
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
#pragma mark - Initialization
|
|
37
|
+
|
|
32
38
|
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
33
39
|
return concreteComponentDescriptorProvider<TrueSheetContainerViewComponentDescriptor>();
|
|
34
40
|
}
|
|
@@ -39,17 +45,20 @@ using namespace facebook::react;
|
|
|
39
45
|
_props = defaultProps;
|
|
40
46
|
|
|
41
47
|
self.backgroundColor = [UIColor clearColor];
|
|
42
|
-
|
|
43
48
|
_contentView = nil;
|
|
49
|
+
_headerView = nil;
|
|
44
50
|
_footerView = nil;
|
|
51
|
+
_scrollViewPinningSet = NO;
|
|
45
52
|
}
|
|
46
53
|
return self;
|
|
47
54
|
}
|
|
48
55
|
|
|
56
|
+
#pragma mark - Layout
|
|
57
|
+
|
|
49
58
|
- (void)layoutSubviews {
|
|
50
59
|
[super layoutSubviews];
|
|
51
60
|
|
|
52
|
-
// Override Yoga layout
|
|
61
|
+
// Override Yoga layout to fill parent (controller's view)
|
|
53
62
|
if (self.superview) {
|
|
54
63
|
CGRect parentBounds = self.superview.bounds;
|
|
55
64
|
if (!CGRectEqualToRect(self.frame, parentBounds)) {
|
|
@@ -62,9 +71,12 @@ using namespace facebook::react;
|
|
|
62
71
|
return _contentView ? _contentView.frame.size.height : 0;
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
- (CGFloat)headerHeight {
|
|
75
|
+
return _headerView ? _headerView.frame.size.height : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
- (void)layoutFooter {
|
|
66
79
|
if (_footerView) {
|
|
67
|
-
// Force footer to reapply constraints on size change
|
|
68
80
|
CGFloat height = _footerView.frame.size.height;
|
|
69
81
|
if (height > 0) {
|
|
70
82
|
[_footerView setupConstraintsWithHeight:height];
|
|
@@ -72,42 +84,70 @@ using namespace facebook::react;
|
|
|
72
84
|
}
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
- (void)
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
- (void)setScrollViewPinningEnabled:(BOOL)scrollViewPinningEnabled {
|
|
88
|
+
_scrollViewPinningEnabled = scrollViewPinningEnabled;
|
|
89
|
+
_scrollViewPinningSet = YES;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
- (void)setupContentScrollViewPinning {
|
|
93
|
+
if (_scrollViewPinningSet && _contentView) {
|
|
94
|
+
[_contentView setupScrollViewPinning:_scrollViewPinningEnabled withHeaderView:_headerView];
|
|
78
95
|
}
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
#pragma mark - Child Component Mounting
|
|
99
|
+
|
|
81
100
|
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
82
101
|
[super mountChildComponentView:childComponentView index:index];
|
|
83
102
|
|
|
84
|
-
// Handle content view mounting
|
|
85
103
|
if ([childComponentView isKindOfClass:[TrueSheetContentView class]]) {
|
|
86
104
|
if (_contentView != nil) {
|
|
87
105
|
RCTLogWarn(@"TrueSheet: Container can only have one content component.");
|
|
88
106
|
return;
|
|
89
107
|
}
|
|
90
|
-
|
|
91
108
|
_contentView = (TrueSheetContentView *)childComponentView;
|
|
92
109
|
_contentView.delegate = self;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
if ([childComponentView isKindOfClass:[TrueSheetHeaderView class]]) {
|
|
113
|
+
if (_headerView != nil) {
|
|
114
|
+
RCTLogWarn(@"TrueSheet: Container can only have one header component.");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
_headerView = (TrueSheetHeaderView *)childComponentView;
|
|
118
|
+
_headerView.delegate = self;
|
|
119
|
+
|
|
120
|
+
if (_contentView) {
|
|
121
|
+
[self setupContentScrollViewPinning];
|
|
122
|
+
}
|
|
123
|
+
[self headerViewDidChangeSize:_headerView.frame.size];
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
if ([childComponentView isKindOfClass:[TrueSheetFooterView class]]) {
|
|
97
127
|
if (_footerView != nil) {
|
|
98
128
|
RCTLogWarn(@"TrueSheet: Container can only have one footer component.");
|
|
99
129
|
return;
|
|
100
130
|
}
|
|
101
|
-
|
|
102
131
|
_footerView = (TrueSheetFooterView *)childComponentView;
|
|
103
132
|
}
|
|
104
133
|
}
|
|
105
134
|
|
|
106
135
|
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
|
|
107
136
|
if ([childComponentView isKindOfClass:[TrueSheetContentView class]]) {
|
|
137
|
+
_contentView.delegate = nil;
|
|
108
138
|
_contentView = nil;
|
|
109
139
|
}
|
|
110
140
|
|
|
141
|
+
if ([childComponentView isKindOfClass:[TrueSheetHeaderView class]]) {
|
|
142
|
+
_headerView.delegate = nil;
|
|
143
|
+
_headerView = nil;
|
|
144
|
+
|
|
145
|
+
if (_contentView) {
|
|
146
|
+
[self setupContentScrollViewPinning];
|
|
147
|
+
}
|
|
148
|
+
[self headerViewDidChangeSize:CGSizeZero];
|
|
149
|
+
}
|
|
150
|
+
|
|
111
151
|
if ([childComponentView isKindOfClass:[TrueSheetFooterView class]]) {
|
|
112
152
|
_footerView = nil;
|
|
113
153
|
}
|
|
@@ -122,12 +162,19 @@ using namespace facebook::react;
|
|
|
122
162
|
#pragma mark - TrueSheetContentViewDelegate
|
|
123
163
|
|
|
124
164
|
- (void)contentViewDidChangeSize:(CGSize)newSize {
|
|
125
|
-
// Forward content size changes to host view for sheet resizing
|
|
126
165
|
if ([self.delegate respondsToSelector:@selector(containerViewContentDidChangeSize:)]) {
|
|
127
166
|
[self.delegate containerViewContentDidChangeSize:newSize];
|
|
128
167
|
}
|
|
129
168
|
}
|
|
130
169
|
|
|
170
|
+
#pragma mark - TrueSheetHeaderViewDelegate
|
|
171
|
+
|
|
172
|
+
- (void)headerViewDidChangeSize:(CGSize)newSize {
|
|
173
|
+
if ([self.delegate respondsToSelector:@selector(containerViewHeaderDidChangeSize:)]) {
|
|
174
|
+
[self.delegate containerViewHeaderDidChangeSize:newSize];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
131
178
|
@end
|
|
132
179
|
|
|
133
180
|
#endif
|
|
@@ -31,9 +31,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
31
31
|
- (RCTScrollViewComponentView *_Nullable)findScrollView;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Setup ScrollView pinning
|
|
35
|
+
* @param pinned Whether to pin the scroll view
|
|
36
|
+
* @param headerView Optional header view to pin below (can be nil)
|
|
35
37
|
*/
|
|
36
|
-
- (void)setupScrollViewPinning:(BOOL)pinned;
|
|
38
|
+
- (void)setupScrollViewPinning:(BOOL)pinned withHeaderView:(nullable UIView *)headerView;
|
|
37
39
|
|
|
38
40
|
@end
|
|
39
41
|
|
|
@@ -53,98 +53,55 @@ using namespace facebook::react;
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
- (void)
|
|
56
|
+
- (void)unpinScrollViewFromParentView:(UIView *)parentView {
|
|
57
57
|
// Unpin previous scroll view if exists
|
|
58
58
|
if (_pinnedScrollView) {
|
|
59
|
-
[LayoutUtil unpinView:_pinnedScrollView];
|
|
59
|
+
[LayoutUtil unpinView:_pinnedScrollView fromParentView:parentView];
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
- (void)setupScrollViewPinning:(BOOL)pinned {
|
|
63
|
+
- (void)setupScrollViewPinning:(BOOL)pinned withHeaderView:(UIView *)headerView {
|
|
64
|
+
// Pin to container view (parent of content view)
|
|
65
|
+
UIView *containerView = self.superview;
|
|
66
|
+
|
|
64
67
|
if (!pinned) {
|
|
65
|
-
[self
|
|
68
|
+
[self unpinScrollViewFromParentView:containerView];
|
|
69
|
+
_pinnedScrollView = nil;
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// Auto-detect and pin scroll views for proper sheet scrolling behavior
|
|
70
|
-
// Pinning ensures ScrollView fills the
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
[self
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
// No top sibling, pin to all edges of container (original behavior)
|
|
88
|
-
[LayoutUtil pinView:scrollView toParentView:containerView edges:UIRectEdgeAll];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
_pinnedScrollView = scrollView;
|
|
74
|
+
// Pinning ensures ScrollView fills the available area and scrolls correctly with the sheet
|
|
75
|
+
RCTScrollViewComponentView *scrollView = [self findScrollView];
|
|
76
|
+
|
|
77
|
+
if (scrollView && containerView) {
|
|
78
|
+
// Always unpin first to remove old constraints
|
|
79
|
+
[self unpinScrollViewFromParentView:containerView];
|
|
80
|
+
|
|
81
|
+
if (headerView) {
|
|
82
|
+
// Pin ScrollView below the header view
|
|
83
|
+
[LayoutUtil pinView:scrollView
|
|
84
|
+
toParentView:containerView
|
|
85
|
+
withTopView:headerView
|
|
86
|
+
edges:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom];
|
|
87
|
+
} else {
|
|
88
|
+
// No header, pin to all edges of container
|
|
89
|
+
[LayoutUtil pinView:scrollView toParentView:containerView edges:UIRectEdgeAll];
|
|
92
90
|
}
|
|
91
|
+
|
|
92
|
+
_pinnedScrollView = scrollView;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
- (RCTScrollViewComponentView *)findScrollView {
|
|
97
|
-
return [self findScrollView:nil];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
- (RCTScrollViewComponentView *)findScrollView:(UIView **)outTopSibling {
|
|
101
|
-
if (self.subviews.count == 0) {
|
|
102
|
-
return nil;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
RCTScrollViewComponentView *scrollView = nil;
|
|
106
|
-
UIView *topSibling = nil;
|
|
107
|
-
|
|
108
97
|
// Check first-level children for scroll views (ScrollView or FlatList)
|
|
109
98
|
for (UIView *subview in self.subviews) {
|
|
110
99
|
if ([subview isKindOfClass:RCTScrollViewComponentView.class]) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Find the view positioned directly above this ScrollView by frame position
|
|
114
|
-
if (self.subviews.count > 1) {
|
|
115
|
-
CGFloat scrollViewTop = CGRectGetMinY(scrollView.frame);
|
|
116
|
-
CGFloat closestDistance = CGFLOAT_MAX;
|
|
117
|
-
|
|
118
|
-
for (UIView *sibling in self.subviews) {
|
|
119
|
-
// Skip the ScrollView itself
|
|
120
|
-
if (sibling == scrollView) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
CGFloat siblingBottom = CGRectGetMaxY(sibling.frame);
|
|
125
|
-
|
|
126
|
-
// Check if this sibling is positioned above the ScrollView
|
|
127
|
-
if (siblingBottom <= scrollViewTop) {
|
|
128
|
-
CGFloat distance = scrollViewTop - siblingBottom;
|
|
129
|
-
|
|
130
|
-
// Find the closest view above (smallest distance)
|
|
131
|
-
if (distance < closestDistance) {
|
|
132
|
-
closestDistance = distance;
|
|
133
|
-
topSibling = sibling;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
break; // Found ScrollView, no need to continue
|
|
100
|
+
return (RCTScrollViewComponentView *)subview;
|
|
140
101
|
}
|
|
141
102
|
}
|
|
142
103
|
|
|
143
|
-
|
|
144
|
-
*outTopSibling = topSibling;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return scrollView;
|
|
104
|
+
return nil;
|
|
148
105
|
}
|
|
149
106
|
|
|
150
107
|
- (void)prepareForRecycle {
|
|
@@ -152,7 +109,7 @@ using namespace facebook::react;
|
|
|
152
109
|
|
|
153
110
|
// Remove scroll view constraints
|
|
154
111
|
if (_pinnedScrollView) {
|
|
155
|
-
[LayoutUtil unpinView:_pinnedScrollView];
|
|
112
|
+
[LayoutUtil unpinView:_pinnedScrollView fromParentView:self.superview];
|
|
156
113
|
_pinnedScrollView = nil;
|
|
157
114
|
}
|
|
158
115
|
}
|
|
@@ -48,7 +48,7 @@ using namespace facebook::react;
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Remove existing constraints before applying new ones
|
|
51
|
-
[LayoutUtil unpinView:self];
|
|
51
|
+
[LayoutUtil unpinView:self fromParentView:parentView];
|
|
52
52
|
|
|
53
53
|
// Pin footer to bottom and sides of container with specific height
|
|
54
54
|
[LayoutUtil pinView:self
|
|
@@ -90,7 +90,7 @@ using namespace facebook::react;
|
|
|
90
90
|
[super prepareForRecycle];
|
|
91
91
|
|
|
92
92
|
// Remove footer constraints
|
|
93
|
-
[LayoutUtil unpinView:self];
|
|
93
|
+
[LayoutUtil unpinView:self fromParentView:self.superview];
|
|
94
94
|
|
|
95
95
|
_lastHeight = 0;
|
|
96
96
|
_didInitialLayout = NO;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
10
|
+
|
|
11
|
+
#import <React/RCTViewComponentView.h>
|
|
12
|
+
#import <UIKit/UIKit.h>
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
15
|
+
|
|
16
|
+
@protocol TrueSheetHeaderViewDelegate <NSObject>
|
|
17
|
+
@optional
|
|
18
|
+
- (void)headerViewDidChangeSize:(CGSize)size;
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
@interface TrueSheetHeaderView : RCTViewComponentView
|
|
22
|
+
|
|
23
|
+
@property (nonatomic, weak, nullable) id<TrueSheetHeaderViewDelegate> delegate;
|
|
24
|
+
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
NS_ASSUME_NONNULL_END
|
|
28
|
+
|
|
29
|
+
#endif
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Created by Jovanni Lo (@lodev09)
|
|
3
|
+
// Copyright (c) 2024-present. All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This source code is licensed under the MIT license found in the
|
|
6
|
+
// LICENSE file in the root directory of this source tree.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
10
|
+
|
|
11
|
+
#import "TrueSheetHeaderView.h"
|
|
12
|
+
#import <react/renderer/components/TrueSheetSpec/ComponentDescriptors.h>
|
|
13
|
+
#import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
|
|
14
|
+
#import <react/renderer/components/TrueSheetSpec/Props.h>
|
|
15
|
+
#import <react/renderer/components/TrueSheetSpec/RCTComponentViewHelpers.h>
|
|
16
|
+
#import "utils/LayoutUtil.h"
|
|
17
|
+
|
|
18
|
+
using namespace facebook::react;
|
|
19
|
+
|
|
20
|
+
@implementation TrueSheetHeaderView {
|
|
21
|
+
CGSize _lastSize;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
25
|
+
return concreteComponentDescriptorProvider<TrueSheetHeaderViewComponentDescriptor>();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
29
|
+
if (self = [super initWithFrame:frame]) {
|
|
30
|
+
static const auto defaultProps = std::make_shared<const TrueSheetHeaderViewProps>();
|
|
31
|
+
_props = defaultProps;
|
|
32
|
+
|
|
33
|
+
_lastSize = CGSizeZero;
|
|
34
|
+
}
|
|
35
|
+
return self;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
|
|
39
|
+
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics {
|
|
40
|
+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
|
|
41
|
+
|
|
42
|
+
CGSize newSize = CGSizeMake(layoutMetrics.frame.size.width, layoutMetrics.frame.size.height);
|
|
43
|
+
|
|
44
|
+
// Notify delegate when header size changes
|
|
45
|
+
if (!CGSizeEqualToSize(newSize, _lastSize)) {
|
|
46
|
+
_lastSize = newSize;
|
|
47
|
+
if ([self.delegate respondsToSelector:@selector(headerViewDidChangeSize:)]) {
|
|
48
|
+
[self.delegate headerViewDidChangeSize:newSize];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)prepareForRecycle {
|
|
54
|
+
[super prepareForRecycle];
|
|
55
|
+
_lastSize = CGSizeZero;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@end
|
|
59
|
+
|
|
60
|
+
#endif
|