@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
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
#import "TrueSheetViewController.h"
|
|
10
|
+
#import "TrueSheetContentView.h"
|
|
10
11
|
#import "utils/ConversionUtil.h"
|
|
12
|
+
#import "utils/GestureUtil.h"
|
|
11
13
|
#import "utils/WindowUtil.h"
|
|
12
14
|
|
|
13
15
|
#import <React/RCTLog.h>
|
|
14
16
|
#import <React/RCTScrollViewComponentView.h>
|
|
15
|
-
#import "TrueSheetContentView.h"
|
|
16
|
-
#import "utils/GestureUtil.h"
|
|
17
17
|
|
|
18
18
|
@interface TrueSheetViewController ()
|
|
19
19
|
|
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
BOOL _isTransitioning;
|
|
26
26
|
BOOL _isDragging;
|
|
27
27
|
BOOL _isTrackingPositionFromLayout;
|
|
28
|
+
|
|
29
|
+
// Hidden view used to track position during native transition animations
|
|
28
30
|
UIView *_fakeTransitionView;
|
|
29
31
|
CADisplayLink *_displayLink;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
#pragma mark - Initialization
|
|
35
|
+
|
|
32
36
|
- (instancetype)init {
|
|
33
37
|
if (self = [super initWithNibName:nil bundle:nil]) {
|
|
34
38
|
_detents = @[ @0.5, @1 ];
|
|
35
39
|
_contentHeight = @(0);
|
|
40
|
+
_headerHeight = @(0);
|
|
36
41
|
_grabber = YES;
|
|
37
42
|
_dimmed = YES;
|
|
38
43
|
_dimmedDetentIndex = @(0);
|
|
@@ -46,7 +51,6 @@
|
|
|
46
51
|
_isPresented = NO;
|
|
47
52
|
_activeDetentIndex = -1;
|
|
48
53
|
|
|
49
|
-
// Initialize fake transition view for tracking position during animations
|
|
50
54
|
_fakeTransitionView = [[UIView alloc] init];
|
|
51
55
|
_fakeTransitionView.hidden = YES;
|
|
52
56
|
_fakeTransitionView.userInteractionEnabled = NO;
|
|
@@ -57,22 +61,18 @@
|
|
|
57
61
|
- (void)dealloc {
|
|
58
62
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
59
63
|
|
|
60
|
-
// Ensure display link is invalidated to prevent retain cycle
|
|
61
64
|
if (_displayLink) {
|
|
62
65
|
[_displayLink invalidate];
|
|
63
66
|
_displayLink = nil;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
#pragma mark -
|
|
70
|
+
#pragma mark - Computed Properties
|
|
68
71
|
|
|
69
72
|
- (BOOL)isTopmostPresentedController {
|
|
70
|
-
// Check if we're in the window hierarchy and visible
|
|
71
73
|
if (!self.isViewLoaded || self.view.window == nil) {
|
|
72
74
|
return NO;
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
// Check if another controller is presented on top of this sheet
|
|
76
76
|
return self.presentedViewController == nil;
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -84,22 +84,71 @@
|
|
|
84
84
|
return self.sheetPresentationController.presentedView;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
- (CGFloat)currentPosition {
|
|
88
|
+
UIView *presentedView = self.presentedView;
|
|
89
|
+
return presentedView ? presentedView.frame.origin.y : 0.0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
- (CGFloat)bottomInset {
|
|
93
|
+
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
UIWindow *window = [WindowUtil keyWindow];
|
|
97
|
+
return window ? window.safeAreaInsets.bottom : 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
- (CGFloat)currentHeight {
|
|
101
|
+
return self.containerHeight - self.currentPosition - self.bottomInset;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (CGFloat)containerHeight {
|
|
105
|
+
UIView *sheetContainerView = self.sheetPresentationController.containerView;
|
|
106
|
+
return sheetContainerView ? sheetContainerView.frame.size.height : 0.0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
- (NSInteger)currentDetentIndex {
|
|
110
|
+
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
111
|
+
if (!sheet)
|
|
112
|
+
return -1;
|
|
113
|
+
|
|
114
|
+
UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
|
|
115
|
+
if (!selectedIdentifier)
|
|
116
|
+
return -1;
|
|
117
|
+
|
|
118
|
+
NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
|
|
119
|
+
for (NSInteger i = 0; i < detents.count; i++) {
|
|
120
|
+
if (@available(iOS 16.0, *)) {
|
|
121
|
+
if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
|
|
122
|
+
return i;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// iOS 15 only supports medium/large system detents
|
|
126
|
+
if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
|
|
127
|
+
return 0;
|
|
128
|
+
} else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
|
|
129
|
+
return detents.count - 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return -1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#pragma mark - View Lifecycle
|
|
138
|
+
|
|
87
139
|
- (void)viewDidLoad {
|
|
88
140
|
[super viewDidLoad];
|
|
89
|
-
|
|
90
141
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
91
142
|
}
|
|
92
143
|
|
|
93
144
|
- (void)viewWillAppear:(BOOL)animated {
|
|
94
145
|
[super viewWillAppear:animated];
|
|
95
146
|
|
|
96
|
-
// Only trigger
|
|
147
|
+
// Only trigger on initial presentation, not repositioning
|
|
97
148
|
if (!_isPresented) {
|
|
98
149
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
|
|
99
150
|
[self.delegate viewControllerWillPresent];
|
|
100
151
|
}
|
|
101
|
-
|
|
102
|
-
// Setup transition position tracking
|
|
103
152
|
[self setupTransitionPositionTracking];
|
|
104
153
|
}
|
|
105
154
|
}
|
|
@@ -107,15 +156,11 @@
|
|
|
107
156
|
- (void)viewDidAppear:(BOOL)animated {
|
|
108
157
|
[super viewDidAppear:animated];
|
|
109
158
|
|
|
110
|
-
// Only trigger didPresent on the initial presentation, not on repositioning
|
|
111
159
|
if (!_isPresented) {
|
|
112
160
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
|
|
113
161
|
[self.delegate viewControllerDidPresent];
|
|
114
162
|
}
|
|
115
|
-
|
|
116
|
-
// Setup gesture recognizer after view appears and React content is mounted
|
|
117
163
|
[self setupGestureRecognizer];
|
|
118
|
-
|
|
119
164
|
_isPresented = YES;
|
|
120
165
|
}
|
|
121
166
|
}
|
|
@@ -123,7 +168,6 @@
|
|
|
123
168
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
124
169
|
[super viewWillDisappear:animated];
|
|
125
170
|
|
|
126
|
-
// Only dispatch willDismiss if the sheet is actually being dismissed
|
|
127
171
|
if (self.isBeingDismissed && [self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
|
|
128
172
|
[self.delegate viewControllerWillDismiss];
|
|
129
173
|
}
|
|
@@ -135,8 +179,7 @@
|
|
|
135
179
|
- (void)viewDidDisappear:(BOOL)animated {
|
|
136
180
|
[super viewDidDisappear:animated];
|
|
137
181
|
|
|
138
|
-
// Only dispatch didDismiss
|
|
139
|
-
// (not when another modal is presented on top)
|
|
182
|
+
// Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
|
|
140
183
|
BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
|
|
141
184
|
|
|
142
185
|
if (isActuallyDismissing && [self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
|
|
@@ -145,49 +188,28 @@
|
|
|
145
188
|
|
|
146
189
|
_isTrackingPositionFromLayout = NO;
|
|
147
190
|
|
|
148
|
-
// Only reset state if actually dismissing
|
|
149
191
|
if (isActuallyDismissing) {
|
|
150
192
|
_isPresented = NO;
|
|
151
193
|
_activeDetentIndex = -1;
|
|
152
194
|
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
|
-
- (void)viewWillTransitionToSize:(CGSize)size
|
|
156
|
-
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
|
157
|
-
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
|
158
|
-
|
|
159
|
-
// Handle rotation/size change
|
|
160
|
-
[coordinator
|
|
161
|
-
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
162
|
-
// Animation block - updates happen here
|
|
163
|
-
}
|
|
164
|
-
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
165
|
-
// After rotation completes
|
|
166
|
-
[self setupSheetDetents];
|
|
167
|
-
|
|
168
|
-
// Notify delegate of size change for state update
|
|
169
|
-
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
|
|
170
|
-
[self.delegate viewControllerDidChangeSize:size];
|
|
171
|
-
}
|
|
172
|
-
}];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
197
|
- (void)viewDidLayoutSubviews {
|
|
176
198
|
[super viewDidLayoutSubviews];
|
|
177
199
|
|
|
200
|
+
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
|
|
201
|
+
[self.delegate viewControllerDidChangeSize:self.view.frame.size];
|
|
202
|
+
}
|
|
203
|
+
|
|
178
204
|
if (!_isTransitioning && self.isActiveAndVisible) {
|
|
179
|
-
// Flag that we are tracking position from layout
|
|
180
205
|
_isTrackingPositionFromLayout = YES;
|
|
181
206
|
|
|
182
|
-
//
|
|
183
|
-
// This prevents incorrect position notifications when overlays adjust our size
|
|
207
|
+
// Treat position changes as transitioning when another controller is presented on top
|
|
184
208
|
[self emitChangePositionDelegateWithPosition:self.currentPosition
|
|
185
209
|
transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
|
|
186
210
|
|
|
187
|
-
// On
|
|
188
|
-
// Schedule flag reset after animation to avoid race condition
|
|
211
|
+
// On iOS 26, this is called twice when we have a ScrollView
|
|
189
212
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
190
|
-
// Reset layout transitioning after sending notification
|
|
191
213
|
self->_layoutTransitioning = NO;
|
|
192
214
|
});
|
|
193
215
|
}
|
|
@@ -195,19 +217,11 @@
|
|
|
195
217
|
|
|
196
218
|
#pragma mark - Gesture Handling
|
|
197
219
|
|
|
198
|
-
/**
|
|
199
|
-
* Finds the TrueSheetContentView in the hierarchy.
|
|
200
|
-
*
|
|
201
|
-
* @param view The presentedView to start searching from
|
|
202
|
-
* @return The TrueSheetContentView found, or nil
|
|
203
|
-
*/
|
|
204
220
|
- (TrueSheetContentView *)findContentView:(UIView *)view {
|
|
205
|
-
// Check if this view itself is TrueSheetContentView
|
|
206
221
|
if ([view isKindOfClass:[TrueSheetContentView class]]) {
|
|
207
222
|
return (TrueSheetContentView *)view;
|
|
208
223
|
}
|
|
209
224
|
|
|
210
|
-
// Recursively search all subviews
|
|
211
225
|
for (UIView *subview in view.subviews) {
|
|
212
226
|
TrueSheetContentView *found = [self findContentView:subview];
|
|
213
227
|
if (found) {
|
|
@@ -223,20 +237,17 @@
|
|
|
223
237
|
if (!presentedView)
|
|
224
238
|
return;
|
|
225
239
|
|
|
226
|
-
// Attach to presented view's pan
|
|
240
|
+
// Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
|
|
227
241
|
[GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
|
|
228
242
|
|
|
229
|
-
//
|
|
230
|
-
// This handles cases where the sheet content includes a ScrollView
|
|
243
|
+
// Also attach to ScrollView's pan gesture if present
|
|
231
244
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
232
245
|
if (contentView) {
|
|
233
246
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
|
|
234
|
-
if (scrollViewComponent) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
[GestureUtil attachPanGestureHandler:scrollView target:self selector:@selector(handlePanGesture:)];
|
|
239
|
-
}
|
|
247
|
+
if (scrollViewComponent && scrollViewComponent.scrollView) {
|
|
248
|
+
[GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
|
|
249
|
+
target:self
|
|
250
|
+
selector:@selector(handlePanGesture:)];
|
|
240
251
|
}
|
|
241
252
|
}
|
|
242
253
|
}
|
|
@@ -253,14 +264,14 @@
|
|
|
253
264
|
_isDragging = YES;
|
|
254
265
|
break;
|
|
255
266
|
case UIGestureRecognizerStateChanged:
|
|
256
|
-
if (!_isTrackingPositionFromLayout)
|
|
267
|
+
if (!_isTrackingPositionFromLayout) {
|
|
257
268
|
[self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
|
|
269
|
+
}
|
|
258
270
|
break;
|
|
259
271
|
case UIGestureRecognizerStateEnded:
|
|
260
272
|
case UIGestureRecognizerStateCancelled:
|
|
261
273
|
_isDragging = NO;
|
|
262
274
|
break;
|
|
263
|
-
|
|
264
275
|
default:
|
|
265
276
|
break;
|
|
266
277
|
}
|
|
@@ -268,17 +279,11 @@
|
|
|
268
279
|
|
|
269
280
|
#pragma mark - Position Tracking
|
|
270
281
|
|
|
271
|
-
/**
|
|
272
|
-
* Emits position change to the delegate if the position has changed.
|
|
273
|
-
* @param transitioning Whether the position change is part of a transition animation
|
|
274
|
-
*/
|
|
275
282
|
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
|
|
276
283
|
if (_lastPosition != position) {
|
|
277
284
|
_lastPosition = position;
|
|
278
285
|
|
|
279
|
-
// Emit position change delegate
|
|
280
286
|
NSInteger index = [self currentDetentIndex];
|
|
281
|
-
|
|
282
287
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
|
|
283
288
|
[self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
|
|
284
289
|
}
|
|
@@ -288,116 +293,79 @@
|
|
|
288
293
|
/**
|
|
289
294
|
* Sets up position tracking during view controller transitions using a fake view.
|
|
290
295
|
*
|
|
291
|
-
* This
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* animation without manually animating in JavaScript.
|
|
295
|
-
*
|
|
296
|
-
* The display link fires at screen refresh rate, allowing us to emit position updates
|
|
297
|
-
* that match the native animation curve, providing smooth synchronized updates to JS.
|
|
296
|
+
* This uses a hidden "fake" view added to the container that animates alongside
|
|
297
|
+
* the presented view. By observing the presentation layer, we can track smooth
|
|
298
|
+
* position changes during native transition animations.
|
|
298
299
|
*/
|
|
299
300
|
- (void)setupTransitionPositionTracking {
|
|
300
|
-
if (self.transitionCoordinator
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// Get the container view to add our fake transition view
|
|
304
|
-
UIView *containerView = self.sheetPresentationController.containerView;
|
|
305
|
-
UIView *presentedView = self.presentedView;
|
|
306
|
-
|
|
307
|
-
if (!containerView || !presentedView)
|
|
308
|
-
return;
|
|
301
|
+
if (self.transitionCoordinator == nil)
|
|
302
|
+
return;
|
|
309
303
|
|
|
310
|
-
|
|
304
|
+
_isTransitioning = YES;
|
|
311
305
|
|
|
312
|
-
|
|
313
|
-
|
|
306
|
+
UIView *containerView = self.sheetPresentationController.containerView;
|
|
307
|
+
UIView *presentedView = self.presentedView;
|
|
314
308
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
// - Dismissing: Start from current position and animate down to bottom
|
|
318
|
-
if (isPresenting) {
|
|
319
|
-
frame.origin.y = self.containerHeight;
|
|
320
|
-
} else {
|
|
321
|
-
frame.origin.y = presentedView.frame.origin.y;
|
|
322
|
-
}
|
|
309
|
+
if (!containerView || !presentedView)
|
|
310
|
+
return;
|
|
323
311
|
|
|
324
|
-
|
|
325
|
-
|
|
312
|
+
CGRect frame = presentedView.frame;
|
|
313
|
+
BOOL isPresenting = self.isBeingPresented;
|
|
326
314
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
315
|
+
// Set initial position: presenting starts from bottom, dismissing from current
|
|
316
|
+
frame.origin.y = isPresenting ? self.containerHeight : presentedView.frame.origin.y;
|
|
317
|
+
_fakeTransitionView.frame = frame;
|
|
330
318
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
CGRect finalFrame = presentedView.frame;
|
|
334
|
-
finalFrame.origin.y = presentedView.frame.origin.y;
|
|
335
|
-
self->_fakeTransitionView.frame = finalFrame;
|
|
319
|
+
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
320
|
+
[[context containerView] addSubview:self->_fakeTransitionView];
|
|
336
321
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// during a transition after a drag
|
|
341
|
-
self->_lastTransitionPosition = finalFrame.origin.y;
|
|
322
|
+
CGRect finalFrame = presentedView.frame;
|
|
323
|
+
finalFrame.origin.y = presentedView.frame.origin.y;
|
|
324
|
+
self->_fakeTransitionView.frame = finalFrame;
|
|
342
325
|
|
|
343
|
-
|
|
344
|
-
// This fires at 60-120Hz and reads from the presentation layer
|
|
345
|
-
self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
|
|
346
|
-
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
347
|
-
};
|
|
326
|
+
self->_lastTransitionPosition = finalFrame.origin.y;
|
|
348
327
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
[self->_displayLink invalidate];
|
|
354
|
-
self->_displayLink = nil;
|
|
328
|
+
// Track position at screen refresh rate via display link
|
|
329
|
+
self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
|
|
330
|
+
[self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
|
331
|
+
};
|
|
355
332
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
333
|
+
[self.transitionCoordinator
|
|
334
|
+
animateAlongsideTransition:animation
|
|
335
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
336
|
+
[self->_displayLink invalidate];
|
|
337
|
+
self->_displayLink = nil;
|
|
338
|
+
[self->_fakeTransitionView removeFromSuperview];
|
|
339
|
+
self->_isTransitioning = NO;
|
|
340
|
+
}];
|
|
361
341
|
}
|
|
362
342
|
|
|
363
343
|
- (void)trackTransitionPosition:(CADisplayLink *)displayLink {
|
|
364
344
|
UIView *presentedView = self.presentedView;
|
|
365
345
|
|
|
366
|
-
if (_isDragging || !_fakeTransitionView || !presentedView)
|
|
346
|
+
if (_isDragging || !_fakeTransitionView || !presentedView)
|
|
367
347
|
return;
|
|
368
|
-
}
|
|
369
348
|
|
|
370
|
-
//
|
|
371
|
-
// Unlike the model layer (which has the final/target value), the presentation
|
|
372
|
-
// layer reflects the current state during animation
|
|
349
|
+
// Presentation layer contains in-flight animated values (not final/target values)
|
|
373
350
|
CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
|
|
374
351
|
|
|
375
352
|
if (presentationLayer) {
|
|
376
353
|
BOOL transitioning = NO;
|
|
377
354
|
CGFloat position = presentationLayer.frame.origin.y;
|
|
378
355
|
|
|
379
|
-
//
|
|
380
|
-
// Sheet must've been repositioning after dragging at lowest detent
|
|
381
|
-
// Use 0.5 points as epsilon to account for floating-point precision in layout calculations
|
|
356
|
+
// If position matches last transition position (within epsilon), sheet is repositioning after drag
|
|
382
357
|
if (fabs(_lastTransitionPosition - position) < 0.5) {
|
|
383
|
-
// Let's just flag it as transitioning to let JS manually animate
|
|
384
358
|
transitioning = YES;
|
|
385
|
-
|
|
386
|
-
// Use the target presented view's frame position to animate
|
|
387
359
|
position = presentedView.frame.origin.y;
|
|
388
360
|
} else {
|
|
389
|
-
// We are actually getting changes to our fake view's layer position
|
|
390
|
-
// Just update our last transition position
|
|
391
361
|
_lastTransitionPosition = position;
|
|
392
362
|
}
|
|
393
363
|
|
|
394
|
-
// Emit the current animated Y position to JS
|
|
395
|
-
// This provides smooth position updates that match the native animation curve
|
|
396
364
|
[self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
|
|
397
365
|
}
|
|
398
366
|
}
|
|
399
367
|
|
|
400
|
-
#pragma mark - Sheet Configuration
|
|
368
|
+
#pragma mark - Sheet Configuration
|
|
401
369
|
|
|
402
370
|
- (void)setupSheetDetents {
|
|
403
371
|
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
@@ -406,9 +374,8 @@
|
|
|
406
374
|
|
|
407
375
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
408
376
|
|
|
409
|
-
// Subtract bottomInset
|
|
410
|
-
|
|
411
|
-
CGFloat totalHeight = [self.contentHeight floatValue] - self.bottomInset;
|
|
377
|
+
// Subtract bottomInset to prevent iOS from adding extra bottom insets
|
|
378
|
+
CGFloat totalHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
|
|
412
379
|
|
|
413
380
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
414
381
|
id detent = self.detents[index];
|
|
@@ -448,7 +415,7 @@
|
|
|
448
415
|
|
|
449
416
|
CGFloat value = [detent floatValue];
|
|
450
417
|
|
|
451
|
-
// -1 represents "auto"
|
|
418
|
+
// -1 represents "auto" (fit content height)
|
|
452
419
|
if (value == -1) {
|
|
453
420
|
if (@available(iOS 16.0, *)) {
|
|
454
421
|
NSString *detentId = @"custom-auto";
|
|
@@ -458,38 +425,32 @@
|
|
|
458
425
|
CGFloat maxDetentValue = context.maximumDetentValue;
|
|
459
426
|
CGFloat maxValue =
|
|
460
427
|
self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
|
|
461
|
-
|
|
462
|
-
return resolvedValue;
|
|
428
|
+
return fmin(height, maxValue);
|
|
463
429
|
}];
|
|
464
430
|
} else {
|
|
465
431
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
466
432
|
}
|
|
467
433
|
}
|
|
468
434
|
|
|
469
|
-
// Handle fraction (0-1)
|
|
470
|
-
// Fraction should only be > 0 and <= 1
|
|
471
435
|
if (value <= 0 || value > 1) {
|
|
472
436
|
RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
|
|
473
437
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
474
438
|
}
|
|
475
439
|
|
|
476
|
-
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
477
|
-
|
|
478
440
|
if (@available(iOS 16.0, *)) {
|
|
479
|
-
// Use exact comparison for common values
|
|
480
441
|
if (value == 1.0) {
|
|
481
442
|
return [UISheetPresentationControllerDetent largeDetent];
|
|
482
443
|
} else if (value == 0.5) {
|
|
483
444
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
484
445
|
} else {
|
|
446
|
+
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
485
447
|
return [UISheetPresentationControllerDetent
|
|
486
448
|
customDetentWithIdentifier:detentId
|
|
487
449
|
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
|
|
488
450
|
CGFloat maxDetentValue = context.maximumDetentValue;
|
|
489
451
|
CGFloat maxValue =
|
|
490
452
|
self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
|
|
491
|
-
|
|
492
|
-
return resolvedValue;
|
|
453
|
+
return fmin(value * maxDetentValue, maxValue);
|
|
493
454
|
}];
|
|
494
455
|
}
|
|
495
456
|
} else if (value >= 0.5) {
|
|
@@ -524,11 +485,9 @@
|
|
|
524
485
|
if (!sheet)
|
|
525
486
|
return;
|
|
526
487
|
|
|
527
|
-
// Validate and clamp activeDetentIndex to detents bounds
|
|
528
488
|
NSInteger detentCount = _detents.count;
|
|
529
|
-
if (detentCount == 0)
|
|
489
|
+
if (detentCount == 0)
|
|
530
490
|
return;
|
|
531
|
-
}
|
|
532
491
|
|
|
533
492
|
// Clamp index to valid range
|
|
534
493
|
NSInteger clampedIndex = _activeDetentIndex;
|
|
@@ -538,7 +497,6 @@
|
|
|
538
497
|
clampedIndex = detentCount - 1;
|
|
539
498
|
}
|
|
540
499
|
|
|
541
|
-
// Update the stored index if it was clamped
|
|
542
500
|
if (clampedIndex != _activeDetentIndex) {
|
|
543
501
|
_activeDetentIndex = clampedIndex;
|
|
544
502
|
}
|
|
@@ -554,83 +512,23 @@
|
|
|
554
512
|
[self applyActiveDetent];
|
|
555
513
|
}
|
|
556
514
|
|
|
557
|
-
- (NSInteger)currentDetentIndex {
|
|
558
|
-
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
559
|
-
if (!sheet)
|
|
560
|
-
return -1;
|
|
561
|
-
|
|
562
|
-
UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
|
|
563
|
-
if (!selectedIdentifier)
|
|
564
|
-
return -1;
|
|
565
|
-
|
|
566
|
-
// Find the index by matching the identifier in the detents array
|
|
567
|
-
NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
|
|
568
|
-
for (NSInteger i = 0; i < detents.count; i++) {
|
|
569
|
-
if (@available(iOS 16.0, *)) {
|
|
570
|
-
if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
|
|
571
|
-
return i;
|
|
572
|
-
}
|
|
573
|
-
} else {
|
|
574
|
-
// For iOS 15, we only support system detents (medium/large)
|
|
575
|
-
// Return the index based on the selected identifier
|
|
576
|
-
if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
|
|
577
|
-
return 0;
|
|
578
|
-
} else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
|
|
579
|
-
return detents.count - 1;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return -1;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
- (CGFloat)currentPosition {
|
|
588
|
-
UIView *presentedView = self.presentedView;
|
|
589
|
-
if (!presentedView)
|
|
590
|
-
return 0.0;
|
|
591
|
-
|
|
592
|
-
return presentedView.frame.origin.y;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
- (CGFloat)bottomInset {
|
|
596
|
-
// No bottom inset for iPad
|
|
597
|
-
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
|
598
|
-
return 0;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Get bottom safe area inset from the window's safe area
|
|
602
|
-
UIWindow *window = [WindowUtil keyWindow];
|
|
603
|
-
return window ? window.safeAreaInsets.bottom : 0;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
- (CGFloat)currentHeight {
|
|
607
|
-
return self.containerHeight - self.currentPosition - self.bottomInset;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
- (CGFloat)containerHeight {
|
|
611
|
-
UIView *containerView = self.sheetPresentationController.containerView;
|
|
612
|
-
if (!containerView)
|
|
613
|
-
return 0.0;
|
|
614
|
-
|
|
615
|
-
return containerView.frame.size.height;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
515
|
- (void)setupSheetProps {
|
|
619
516
|
UISheetPresentationController *sheet = self.sheetPresentationController;
|
|
620
517
|
if (!sheet) {
|
|
518
|
+
RCTLogWarn(
|
|
519
|
+
@"TrueSheet: No sheet presentation controller available. Ensure the view controller is presented modally.");
|
|
621
520
|
return;
|
|
622
521
|
}
|
|
623
522
|
|
|
624
523
|
sheet.delegate = self;
|
|
625
524
|
|
|
626
|
-
// Configure page sizing behavior (iOS 17+)
|
|
627
525
|
if (@available(iOS 17.0, *)) {
|
|
628
526
|
sheet.prefersPageSizing = self.pageSizing;
|
|
629
527
|
}
|
|
630
528
|
|
|
631
529
|
sheet.prefersEdgeAttachedInCompactHeight = YES;
|
|
632
530
|
sheet.prefersGrabberVisible = self.grabber;
|
|
633
|
-
|
|
531
|
+
|
|
634
532
|
if (self.cornerRadius) {
|
|
635
533
|
sheet.preferredCornerRadius = [self.cornerRadius floatValue];
|
|
636
534
|
} else {
|
|
@@ -639,19 +537,14 @@
|
|
|
639
537
|
|
|
640
538
|
self.view.backgroundColor = self.backgroundColor;
|
|
641
539
|
|
|
642
|
-
// Setup blur effect
|
|
540
|
+
// Setup or remove blur effect
|
|
643
541
|
if (self.blurTint && self.blurTint.length > 0) {
|
|
644
542
|
UIBlurEffectStyle style = [ConversionUtil blurEffectStyleFromString:self.blurTint];
|
|
645
|
-
|
|
646
|
-
// Create a blur effect view and set it as the background
|
|
647
543
|
UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:style]];
|
|
648
544
|
blurView.frame = self.view.bounds;
|
|
649
545
|
blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
650
|
-
|
|
651
|
-
// Insert blur view at the bottom
|
|
652
546
|
[self.view insertSubview:blurView atIndex:0];
|
|
653
547
|
} else {
|
|
654
|
-
// Remove any blur views
|
|
655
548
|
for (UIView *subview in self.view.subviews) {
|
|
656
549
|
if ([subview isKindOfClass:[UIVisualEffectView class]]) {
|
|
657
550
|
[subview removeFromSuperview];
|
|
@@ -674,15 +567,12 @@
|
|
|
674
567
|
|
|
675
568
|
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
676
569
|
- (BOOL)isDismissible {
|
|
677
|
-
//
|
|
678
|
-
// when presenting a React Navigation modal
|
|
570
|
+
// Prevent react-native-screens from dismissing this sheet when presenting a modal
|
|
679
571
|
return NO;
|
|
680
572
|
}
|
|
681
573
|
|
|
682
574
|
- (UIViewController *)newPresentingViewController {
|
|
683
|
-
//
|
|
684
|
-
// This allows react-native-screens to present modals on top of the sheet's content
|
|
685
|
-
// instead of trying to present on top of the sheet itself
|
|
575
|
+
// Allow react-native-screens to present modals on top of the sheet's content
|
|
686
576
|
return self;
|
|
687
577
|
}
|
|
688
578
|
#endif
|
package/ios/utils/LayoutUtil.h
CHANGED
|
@@ -43,8 +43,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
43
43
|
/**
|
|
44
44
|
* Unpins a view by removing its constraints and re-enabling autoresizing mask translation
|
|
45
45
|
* @param view The view to unpin
|
|
46
|
+
* @param parentView The parent view that holds the constraints (optional, will use superview if nil)
|
|
46
47
|
*/
|
|
47
|
-
+ (void)unpinView:(UIView *)view;
|
|
48
|
+
+ (void)unpinView:(UIView *)view fromParentView:(nullable UIView *)parentView;
|
|
48
49
|
|
|
49
50
|
@end
|
|
50
51
|
|