@lodev09/react-native-true-sheet 3.11.10 → 4.0.0-beta.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/README.md +6 -5
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +23 -2
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +116 -26
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +48 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +7 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +45 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +28 -12
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +99 -34
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +9 -5
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +12 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +33 -4
- package/android/src/main/jni/TrueSheetSpec.h +2 -0
- package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +43 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +37 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +56 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
- package/ios/TrueSheetContainerView.h +12 -10
- package/ios/TrueSheetContainerView.mm +11 -29
- package/ios/TrueSheetContentView.h +22 -7
- package/ios/TrueSheetContentView.mm +143 -41
- package/ios/TrueSheetFooterView.h +12 -2
- package/ios/TrueSheetFooterView.mm +74 -71
- package/ios/TrueSheetHeaderView.mm +0 -1
- package/ios/TrueSheetView.mm +60 -27
- package/ios/TrueSheetViewController.h +9 -0
- package/ios/TrueSheetViewController.mm +288 -122
- package/ios/core/TrueSheetDetentCalculator.h +9 -0
- package/ios/core/TrueSheetDetentCalculator.mm +13 -4
- package/ios/tvos/TrueSheetStubs.mm +2 -0
- package/lib/module/TrueSheet.js +32 -13
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +262 -38
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/lib/module/web/vaul/index.js +17 -6
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +35 -11
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -1
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/package.json +10 -10
- package/react-native.config.js +5 -1
- package/src/TrueSheet.tsx +45 -14
- package/src/TrueSheet.types.ts +38 -12
- package/src/TrueSheet.web.tsx +314 -44
- package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
- package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
- package/src/navigation/types.ts +3 -1
- package/src/web/vaul/index.tsx +23 -5
- package/ios/utils/LayoutUtil.h +0 -77
- package/ios/utils/LayoutUtil.mm +0 -106
|
@@ -40,11 +40,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
40
40
|
@interface TrueSheetViewController ()
|
|
41
41
|
|
|
42
42
|
- (UIViewController *)accessibilityPresentingViewController;
|
|
43
|
+
- (void)setupTransitionTracker;
|
|
44
|
+
- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug;
|
|
43
45
|
- (void)restoreWindowAccessibilityElements;
|
|
44
46
|
- (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
|
|
45
47
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
46
48
|
- (void)endInteractiveDismissState;
|
|
47
|
-
- (void)emitInteractivePosition;
|
|
48
49
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
49
50
|
duration:(NSTimeInterval)duration
|
|
50
51
|
allowUserInteraction:(BOOL)allowUserInteraction
|
|
@@ -54,24 +55,26 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
54
55
|
|
|
55
56
|
@implementation TrueSheetViewController {
|
|
56
57
|
TrueSheetPositionState _lastEmittedPositionState;
|
|
57
|
-
|
|
58
|
+
CGSize _lastReportedSize;
|
|
59
|
+
CGFloat _autoDetentHeight;
|
|
60
|
+
CGFloat _peekDetentHeight;
|
|
58
61
|
NSInteger _pendingDetentIndex;
|
|
62
|
+
|
|
63
|
+
// Present/dismiss tracking: a display link scoped to the transition
|
|
64
|
+
// coordinator — started alongside the transition, stopped in its completion.
|
|
65
|
+
CADisplayLink *_transitionLink;
|
|
66
|
+
BOOL _isTransitioning;
|
|
67
|
+
|
|
59
68
|
BOOL _pendingContentSizeChange;
|
|
60
69
|
BOOL _pendingDetentsChange;
|
|
61
|
-
|
|
62
|
-
CADisplayLink *_transitioningTimer;
|
|
63
|
-
UIView *_transitionFakeView;
|
|
64
70
|
BOOL _isDragging;
|
|
65
|
-
BOOL _isTransitioning;
|
|
66
|
-
BOOL _isTransitionSnapping;
|
|
67
|
-
BOOL _isTrackingPositionFromLayout;
|
|
68
71
|
BOOL _isWillDismissEmitted;
|
|
69
72
|
|
|
70
73
|
BOOL _isInteractiveDismiss;
|
|
71
74
|
CGFloat _interactiveStartPosition;
|
|
72
75
|
UIView *_interactiveContainerView;
|
|
73
|
-
CADisplayLink *_interactivePositionLink;
|
|
74
76
|
NSUInteger _interactiveGeneration;
|
|
77
|
+
CADisplayLink *_interactivePositionLink;
|
|
75
78
|
|
|
76
79
|
__weak TrueSheetViewController *_parentSheetController;
|
|
77
80
|
|
|
@@ -103,15 +106,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
103
106
|
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
104
107
|
_isDragging = NO;
|
|
105
108
|
_isPresented = NO;
|
|
106
|
-
_isTransitioning = NO;
|
|
107
109
|
_isWillDismissEmitted = NO;
|
|
110
|
+
_isTransitioning = NO;
|
|
108
111
|
_pendingContentSizeChange = NO;
|
|
112
|
+
_pendingDetentsChange = NO;
|
|
109
113
|
_activeDetentIndex = -1;
|
|
110
114
|
_pendingDetentIndex = -1;
|
|
111
115
|
|
|
112
|
-
_transitionFakeView = [UIView new];
|
|
113
|
-
_isTrackingPositionFromLayout = NO;
|
|
114
|
-
|
|
115
116
|
_blurInteraction = YES;
|
|
116
117
|
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
117
118
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
@@ -122,8 +123,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
122
123
|
|
|
123
124
|
- (void)dealloc {
|
|
124
125
|
[self restoreWindowAccessibilityElements];
|
|
125
|
-
[
|
|
126
|
-
|
|
126
|
+
[_transitionLink invalidate];
|
|
127
|
+
_transitionLink = nil;
|
|
127
128
|
[_interactivePositionLink invalidate];
|
|
128
129
|
_interactivePositionLink = nil;
|
|
129
130
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
@@ -142,6 +143,13 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
142
143
|
return self.presentedViewController == nil;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// YES while a child controller is stacked on top (e.g. a nested sheet) —
|
|
147
|
+
// UIKit's push-back scaling mutates the frame while the sheet is backgrounded.
|
|
148
|
+
- (BOOL)isStackedBehindChild {
|
|
149
|
+
UIViewController *presented = self.presentedViewController;
|
|
150
|
+
return presented != nil && !presented.isBeingDismissed;
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
- (UIView *)presentedView {
|
|
146
154
|
return self.sheet.presentedView;
|
|
147
155
|
}
|
|
@@ -151,6 +159,48 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
151
159
|
return presentedView ? presentedView.frame.origin.y : 0.0;
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
// YES when any layer from the presented view up to the window has in-flight
|
|
163
|
+
// animations (detent snap, present/dismiss transition, container settle).
|
|
164
|
+
- (BOOL)isPresentedViewAnimating {
|
|
165
|
+
for (CALayer *layer = self.presentedView.layer; layer; layer = layer.superlayer) {
|
|
166
|
+
if (layer.animationKeys.count > 0) {
|
|
167
|
+
return YES;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return NO;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The sheet's live on-screen position. At rest and during drags (direct frame
|
|
175
|
+
* sets) this is the model position; during animations it adds the
|
|
176
|
+
* presentation-vs-model delta measured in window space, so ancestor animations
|
|
177
|
+
* compose and persistent transforms (e.g. floating-sheet inset) cancel out.
|
|
178
|
+
*/
|
|
179
|
+
- (CGFloat)livePosition {
|
|
180
|
+
UIView *presentedView = self.presentedView;
|
|
181
|
+
UIWindow *window = presentedView.window;
|
|
182
|
+
UIView *containerView = self.sheet.containerView;
|
|
183
|
+
|
|
184
|
+
// Direct (non-animated) translation applied during an interactive nav dismiss.
|
|
185
|
+
CGFloat containerTy = containerView ? containerView.transform.ty : 0;
|
|
186
|
+
|
|
187
|
+
if (!presentedView || !window || !self.isPresentedViewAnimating) {
|
|
188
|
+
return self.currentPosition + containerTy;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
CALayer *modelLayer = presentedView.layer;
|
|
192
|
+
CALayer *presentationLayer = modelLayer.presentationLayer;
|
|
193
|
+
CALayer *rootModel = window.layer;
|
|
194
|
+
CALayer *rootPresentation = rootModel.presentationLayer;
|
|
195
|
+
if (!presentationLayer || !rootPresentation) {
|
|
196
|
+
return self.currentPosition + containerTy;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
CGFloat modelY = [modelLayer convertPoint:CGPointZero toLayer:rootModel].y;
|
|
200
|
+
CGFloat presentationY = [presentationLayer convertPoint:CGPointZero toLayer:rootPresentation].y;
|
|
201
|
+
return self.currentPosition + (presentationY - modelY) + containerTy;
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
- (CGFloat)screenHeight {
|
|
155
205
|
UIWindow *window = self.view.window;
|
|
156
206
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
@@ -161,6 +211,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
161
211
|
return 0;
|
|
162
212
|
}
|
|
163
213
|
|
|
214
|
+
return [self bottomSafeAreaForHeight:height];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
- (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
|
|
164
218
|
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone) {
|
|
165
219
|
return 0;
|
|
166
220
|
}
|
|
@@ -177,6 +231,52 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
177
231
|
return window ? window.safeAreaInsets.bottom : 0;
|
|
178
232
|
}
|
|
179
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Bottom inset excluded from the auto detent. A relative footer owns the
|
|
236
|
+
* sheet's bottom edge, so it absorbs the inset — UIKit lays custom detents out
|
|
237
|
+
* above the safe area, which would otherwise gap the content from the footer.
|
|
238
|
+
*/
|
|
239
|
+
- (CGFloat)autoDetentBottomInsetForHeight:(CGFloat)height {
|
|
240
|
+
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
241
|
+
// 'none' already excludes the inset for every detent
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return [self bottomSafeAreaForHeight:height];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The inset the footer absorbs as padding. Uses the auto detent height when
|
|
253
|
+
// available so it matches the auto detent's inset exclusion above.
|
|
254
|
+
- (CGFloat)footerBottomInset {
|
|
255
|
+
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return [self bottomSafeAreaForHeight:_autoDetentHeight > 0 ? _autoDetentHeight : self.screenHeight];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Bottom inset excluded from the peek detent. An absolute footer counts
|
|
264
|
+
* toward the peek height and absorbs the inset as padding, but UIKit already
|
|
265
|
+
* lays custom detents out above the safe area — subtract it so the inset
|
|
266
|
+
* isn't doubled.
|
|
267
|
+
*/
|
|
268
|
+
- (CGFloat)peekDetentBottomInsetForHeight:(CGFloat)height {
|
|
269
|
+
if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [self bottomSafeAreaForHeight:height];
|
|
278
|
+
}
|
|
279
|
+
|
|
180
280
|
- (BOOL)isDesignCompatibilityMode {
|
|
181
281
|
if (@available(iOS 26.0, *)) {
|
|
182
282
|
NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
|
|
@@ -439,8 +539,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
439
539
|
[self restoreWindowAccessibilityElements];
|
|
440
540
|
[self setSheetAccessibilityElementsHidden:YES];
|
|
441
541
|
|
|
442
|
-
// Dispatch to allow pan gesture to set _isDragging before checking
|
|
443
|
-
//
|
|
542
|
+
// Dispatch to allow pan gesture to set _isDragging before checking;
|
|
543
|
+
// the transition tracker emits when the sheet is transitioning to dismiss
|
|
444
544
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
445
545
|
if (!self->_isDragging) {
|
|
446
546
|
[self emitWillDismissEvents];
|
|
@@ -470,54 +570,63 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
470
570
|
- (void)viewWillLayoutSubviews {
|
|
471
571
|
[super viewWillLayoutSubviews];
|
|
472
572
|
|
|
473
|
-
// Skip during
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
573
|
+
// Skip during transitions and nav swipes — their trackers own position then.
|
|
574
|
+
if (_isTransitioning || _isInteractiveDismiss) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
477
577
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
578
|
+
if (_pendingContentSizeChange || _pendingDetentsChange) {
|
|
579
|
+
_pendingContentSizeChange = NO;
|
|
580
|
+
_pendingDetentsChange = NO;
|
|
581
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
|
|
582
|
+
} else if (!_isDragging) {
|
|
583
|
+
// Drags emit from the pan handler. A child on top moves this sheet two
|
|
584
|
+
// ways: an animated collapse/restore step (presenting/dismissing) — emit
|
|
585
|
+
// the target non-realtime so JS animates to it — or direct per-frame
|
|
586
|
+
// re-layouts while the child drags, which stay realtime.
|
|
587
|
+
BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
|
|
588
|
+
|
|
589
|
+
// A layout pass at rest can nudge the frame by a sub-pixel amount
|
|
590
|
+
// (pixel-grid snapping after present), leaving the interpolated index
|
|
591
|
+
// slightly off the whole number. Re-learn when the frame is effectively
|
|
592
|
+
// at the current detent so the drift is absorbed instead of emitted.
|
|
593
|
+
// The tight threshold keeps real movement (e.g. scroll-driven expansion)
|
|
594
|
+
// from being absorbed.
|
|
595
|
+
if (self.presentedViewController == nil && !self.isPresentedViewAnimating) {
|
|
596
|
+
NSInteger index = self.currentDetentIndex;
|
|
597
|
+
CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
|
|
598
|
+
CGFloat actualHeight = self.screenHeight - self.currentPosition;
|
|
599
|
+
if (expectedHeight > 0 && fabs(actualHeight - expectedHeight) < 2) {
|
|
600
|
+
[self learnOffsetForDetentIndex:index];
|
|
498
601
|
}
|
|
499
|
-
|
|
500
|
-
[self emitChangePositionDelegateWithPosition:self.currentPosition
|
|
501
|
-
realtime:!hasPresentedController
|
|
502
|
-
debug:@"layout"];
|
|
503
602
|
}
|
|
603
|
+
|
|
604
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
|
|
504
605
|
}
|
|
505
606
|
}
|
|
506
607
|
|
|
507
608
|
- (void)viewDidLayoutSubviews {
|
|
508
609
|
[super viewDidLayoutSubviews];
|
|
509
610
|
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
611
|
+
// Skip size reports while backgrounded behind a stacked child — the push-back
|
|
612
|
+
// scaling changes the frame and would needlessly resize content.
|
|
613
|
+
// _lastReportedSize stays stale so the restore pass re-reports any real change.
|
|
614
|
+
if (!self.isStackedBehindChild) {
|
|
615
|
+
// Report any size change (detent resize, keyboard, rotation) so Yoga
|
|
616
|
+
// relayouts the container synchronously with the sheet.
|
|
617
|
+
CGSize size = self.view.frame.size;
|
|
618
|
+
if (!CGSizeEqualToSize(_lastReportedSize, size)) {
|
|
619
|
+
_lastReportedSize = size;
|
|
620
|
+
[self.delegate viewControllerDidChangeSize:size];
|
|
621
|
+
}
|
|
515
622
|
}
|
|
516
623
|
|
|
517
624
|
if (_pendingDetentIndex >= 0) {
|
|
518
625
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
519
626
|
_pendingDetentIndex = -1;
|
|
520
627
|
|
|
628
|
+
// The presentedView frame isn't final until UIKit finishes the resize
|
|
629
|
+
// animation — no completion hook exists for it, hence the delay.
|
|
521
630
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
522
631
|
CGFloat detent = [self detentValueForIndex:pendingIndex];
|
|
523
632
|
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
|
|
@@ -525,8 +634,6 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
525
634
|
[self settleAtDetentIndex:pendingIndex debug:@"pending detent change"];
|
|
526
635
|
});
|
|
527
636
|
}
|
|
528
|
-
|
|
529
|
-
_isTrackingPositionFromLayout = NO;
|
|
530
637
|
}
|
|
531
638
|
|
|
532
639
|
#pragma mark - Position & Gesture Handling
|
|
@@ -588,17 +695,17 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
588
695
|
_isDragging = YES;
|
|
589
696
|
break;
|
|
590
697
|
case UIGestureRecognizerStateChanged:
|
|
591
|
-
|
|
592
|
-
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
593
|
-
}
|
|
698
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
594
699
|
break;
|
|
595
700
|
case UIGestureRecognizerStateEnded:
|
|
596
701
|
case UIGestureRecognizerStateCancelled: {
|
|
597
702
|
if (!_isTransitioning) {
|
|
703
|
+
// Dispatch so UIKit picks the target detent first; the settle emit is
|
|
704
|
+
// non-realtime and JS animates alongside UIKit's snap spring.
|
|
598
705
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
599
|
-
NSInteger
|
|
600
|
-
[self->_grabberView updateAccessibilityValueWithIndex:
|
|
601
|
-
[self settleAtDetentIndex:
|
|
706
|
+
NSInteger endIndex = self.currentDetentIndex;
|
|
707
|
+
[self->_grabberView updateAccessibilityValueWithIndex:endIndex detentCount:self->_detents.count];
|
|
708
|
+
[self settleAtDetentIndex:endIndex debug:@"drag end"];
|
|
602
709
|
});
|
|
603
710
|
}
|
|
604
711
|
|
|
@@ -610,9 +717,22 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
610
717
|
}
|
|
611
718
|
}
|
|
612
719
|
|
|
720
|
+
#pragma mark - Position Tracking
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Tracks the sheet's live position through a present/dismiss transition. The
|
|
724
|
+
* link's lifetime is scoped to the transition coordinator: started alongside
|
|
725
|
+
* the transition, stopped in its completion — which fires for finished and
|
|
726
|
+
* cancelled (interactive) transitions alike.
|
|
727
|
+
*/
|
|
613
728
|
- (void)setupTransitionTracker {
|
|
614
|
-
|
|
729
|
+
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
|
|
730
|
+
|
|
731
|
+
// A cancelled dismiss re-enters viewWillAppear with the same coordinator —
|
|
732
|
+
// the tracker from viewWillDisappear still owns it.
|
|
733
|
+
if (!coordinator || _isTransitioning) {
|
|
615
734
|
return;
|
|
735
|
+
}
|
|
616
736
|
|
|
617
737
|
_isTransitioning = YES;
|
|
618
738
|
|
|
@@ -623,43 +743,35 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
623
743
|
[self learnOffsetForDetentIndex:self.currentDetentIndex];
|
|
624
744
|
}
|
|
625
745
|
|
|
626
|
-
CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
|
|
627
|
-
CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
|
|
628
|
-
|
|
629
|
-
_transitionFakeView.frame = self.isBeingDismissed ? presentedFrame : dismissedFrame;
|
|
630
|
-
|
|
631
746
|
__weak __typeof(self) weakSelf = self;
|
|
632
|
-
|
|
633
|
-
[self.transitionCoordinator
|
|
747
|
+
[coordinator
|
|
634
748
|
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
635
749
|
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
636
750
|
if (!strongSelf)
|
|
637
751
|
return;
|
|
638
752
|
|
|
639
|
-
[
|
|
640
|
-
strongSelf->
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
753
|
+
[strongSelf->_transitionLink invalidate];
|
|
754
|
+
strongSelf->_transitionLink = [CADisplayLink displayLinkWithTarget:strongSelf
|
|
755
|
+
selector:@selector(handleTransitionTick)];
|
|
756
|
+
// Track at the display's native refresh rate — the default caps at 60Hz
|
|
757
|
+
// on ProMotion, dropping every other frame of a 120Hz animation.
|
|
758
|
+
strongSelf->_transitionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
|
|
759
|
+
[strongSelf->_transitionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
645
760
|
}
|
|
646
761
|
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
647
762
|
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
|
648
763
|
if (!strongSelf)
|
|
649
764
|
return;
|
|
650
765
|
|
|
651
|
-
[strongSelf->
|
|
652
|
-
|
|
653
|
-
strongSelf->_transitioningTimer = nil;
|
|
654
|
-
[strongSelf->_transitionFakeView removeFromSuperview];
|
|
766
|
+
[strongSelf->_transitionLink invalidate];
|
|
767
|
+
strongSelf->_transitionLink = nil;
|
|
655
768
|
strongSelf->_isTransitioning = NO;
|
|
656
|
-
strongSelf->_isTransitionSnapping = NO;
|
|
657
769
|
|
|
658
|
-
// Settle after a present or detent-snap transition.
|
|
659
|
-
//
|
|
770
|
+
// Settle after a present, cancelled dismiss, or detent-snap transition.
|
|
771
|
+
// Delayed because the presentedView frame isn't final until UIKit
|
|
660
772
|
// completes its layout pass after the transition animation — learning
|
|
661
773
|
// here absorbs any sub-pixel drift since the earlier learns.
|
|
662
|
-
if (strongSelf->_isPresented) {
|
|
774
|
+
if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
|
|
663
775
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
664
776
|
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
665
777
|
});
|
|
@@ -667,34 +779,43 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
667
779
|
}];
|
|
668
780
|
}
|
|
669
781
|
|
|
670
|
-
- (void)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
CGFloat
|
|
782
|
+
- (void)handleTransitionTick {
|
|
783
|
+
// Interactive dismiss phase (finger down) — the pan handler emits.
|
|
784
|
+
if (!_isDragging) {
|
|
785
|
+
CGFloat position = self.livePosition;
|
|
674
786
|
|
|
675
|
-
if (self.
|
|
676
|
-
|
|
787
|
+
if (self.isBeingDismissed) {
|
|
788
|
+
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
789
|
+
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
790
|
+
// detent (isBeingDismissed stays YES during the rewind).
|
|
791
|
+
if (self.currentPosition >= self.screenHeight) {
|
|
792
|
+
[self emitWillDismissEvents];
|
|
793
|
+
}
|
|
677
794
|
|
|
678
795
|
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
679
796
|
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
680
797
|
if (self.screenHeight - position < 1) {
|
|
681
798
|
_blurView.alpha = 0;
|
|
682
799
|
}
|
|
800
|
+
}
|
|
683
801
|
|
|
684
|
-
|
|
685
|
-
|
|
802
|
+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
|
|
803
|
+
}
|
|
804
|
+
}
|
|
686
805
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
806
|
+
/**
|
|
807
|
+
* The sheet is at rest at a detent: learn the resolver-vs-actual offset and
|
|
808
|
+
* emit the settled position. Only valid when the presentedView frame is final
|
|
809
|
+
* — never mid-drag or mid-animation, where learning would store a bogus offset.
|
|
810
|
+
*/
|
|
811
|
+
- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug {
|
|
812
|
+
// Don't learn while a child sits on top — the deck transform skews the
|
|
813
|
+
// measured frame; the next unobstructed settle learns.
|
|
814
|
+
if (self.presentedViewController == nil) {
|
|
815
|
+
[self learnOffsetForDetentIndex:index];
|
|
697
816
|
}
|
|
817
|
+
|
|
818
|
+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
698
819
|
}
|
|
699
820
|
|
|
700
821
|
#pragma mark - Interactive Navigation Dismiss
|
|
@@ -716,6 +837,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
716
837
|
// Emit position from the container's presentation layer for the whole gesture, so
|
|
717
838
|
// both the direct-set drag and the settle animation report a live, smooth position.
|
|
718
839
|
_interactivePositionLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(emitInteractivePosition)];
|
|
840
|
+
_interactivePositionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
|
|
719
841
|
[_interactivePositionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
720
842
|
}
|
|
721
843
|
|
|
@@ -849,16 +971,6 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
849
971
|
}
|
|
850
972
|
}
|
|
851
973
|
|
|
852
|
-
/**
|
|
853
|
-
* Learn the resolver-vs-actual offset at a settled detent, then emit the corrected
|
|
854
|
-
* position. Only valid when presentedView's frame is final — never mid-drag or
|
|
855
|
-
* mid-animation, where learning would store a bogus offset.
|
|
856
|
-
*/
|
|
857
|
-
- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug {
|
|
858
|
-
[self learnOffsetForDetentIndex:index];
|
|
859
|
-
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
860
|
-
}
|
|
861
|
-
|
|
862
974
|
- (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
|
|
863
975
|
return [_detentCalculator findSegmentForPosition:position outIndex:outIndex outProgress:outProgress];
|
|
864
976
|
}
|
|
@@ -877,11 +989,31 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
877
989
|
|
|
878
990
|
#pragma mark - Sheet Configuration
|
|
879
991
|
|
|
992
|
+
/**
|
|
993
|
+
* Applies a content/header/footer/peek size change to the already-built
|
|
994
|
+
* detents. Auto and peek detents resolve lazily from snapshots, so this only
|
|
995
|
+
* refreshes the snapshots and invalidates — never reassigns `sheet.detents`,
|
|
996
|
+
* which would force UIKit to re-layout the whole presentation stack (visibly
|
|
997
|
+
* perturbing a sheet behind this one).
|
|
998
|
+
*/
|
|
880
999
|
- (void)setupSheetDetentsForSizeChange {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
[
|
|
884
|
-
|
|
1000
|
+
if (@available(iOS 16.0, *)) {
|
|
1001
|
+
CGFloat autoHeight = [_detentCalculator autoHeight];
|
|
1002
|
+
CGFloat peekHeight = [_detentCalculator peekHeight];
|
|
1003
|
+
|
|
1004
|
+
if (fabs(autoHeight - _autoDetentHeight) < 0.5 && fabs(peekHeight - _peekDetentHeight) < 0.5) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
_autoDetentHeight = autoHeight;
|
|
1009
|
+
_peekDetentHeight = peekHeight;
|
|
1010
|
+
|
|
1011
|
+
[self.sheet animateChanges:^{
|
|
1012
|
+
self->_pendingContentSizeChange = YES;
|
|
1013
|
+
[self.sheet invalidateDetents];
|
|
1014
|
+
}];
|
|
1015
|
+
}
|
|
1016
|
+
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
885
1017
|
}
|
|
886
1018
|
|
|
887
1019
|
- (void)setupSheetDetentsForDetentsChange {
|
|
@@ -899,13 +1031,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
899
1031
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
900
1032
|
[_detentCalculator clearResolvedHeights];
|
|
901
1033
|
|
|
902
|
-
|
|
1034
|
+
_autoDetentHeight = [_detentCalculator autoHeight];
|
|
1035
|
+
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
903
1036
|
|
|
904
1037
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
905
1038
|
id detent = self.detents[index];
|
|
906
|
-
UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
|
|
907
|
-
withAutoHeight:autoHeight
|
|
908
|
-
atIndex:index];
|
|
1039
|
+
UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent atIndex:index];
|
|
909
1040
|
[detents addObject:sheetDetent];
|
|
910
1041
|
}
|
|
911
1042
|
|
|
@@ -932,18 +1063,25 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
932
1063
|
}
|
|
933
1064
|
}
|
|
934
1065
|
|
|
935
|
-
- (UISheetPresentationControllerDetent *)detentForValue:(id)detent
|
|
936
|
-
withAutoHeight:(CGFloat)autoHeight
|
|
937
|
-
atIndex:(NSInteger)index {
|
|
1066
|
+
- (UISheetPresentationControllerDetent *)detentForValue:(id)detent atIndex:(NSInteger)index {
|
|
938
1067
|
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
939
1068
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
940
1069
|
}
|
|
941
1070
|
|
|
942
1071
|
CGFloat value = [detent doubleValue];
|
|
943
1072
|
|
|
1073
|
+
// Auto/peek resolve from height snapshots — refreshed only in
|
|
1074
|
+
// setupSheetDetents and setupSheetDetentsForSizeChange — so size changes are
|
|
1075
|
+
// picked up on invalidation, while UIKit's spontaneous re-resolutions (e.g.
|
|
1076
|
+
// while a child sheet dismisses) read a stable value.
|
|
944
1077
|
if (value == -1) {
|
|
945
1078
|
if (@available(iOS 16.0, *)) {
|
|
946
|
-
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1079
|
+
return [self customDetentWithIdentifier:@"custom-auto"
|
|
1080
|
+
atIndex:index
|
|
1081
|
+
heightBlock:^CGFloat {
|
|
1082
|
+
CGFloat height = self->_autoDetentHeight;
|
|
1083
|
+
return height - [self autoDetentBottomInsetForHeight:height];
|
|
1084
|
+
}];
|
|
947
1085
|
} else {
|
|
948
1086
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
949
1087
|
}
|
|
@@ -951,7 +1089,12 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
951
1089
|
|
|
952
1090
|
if (value == -2) {
|
|
953
1091
|
if (@available(iOS 16.0, *)) {
|
|
954
|
-
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1092
|
+
return [self customDetentWithIdentifier:@"custom-peek"
|
|
1093
|
+
atIndex:index
|
|
1094
|
+
heightBlock:^CGFloat {
|
|
1095
|
+
CGFloat height = self->_peekDetentHeight;
|
|
1096
|
+
return height - [self peekDetentBottomInsetForHeight:height];
|
|
1097
|
+
}];
|
|
955
1098
|
} else {
|
|
956
1099
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
957
1100
|
}
|
|
@@ -976,17 +1119,40 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
976
1119
|
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
977
1120
|
height:(CGFloat)height
|
|
978
1121
|
atIndex:(NSInteger)index API_AVAILABLE(ios(16.0)) {
|
|
979
|
-
|
|
1122
|
+
return [self customDetentWithIdentifier:identifier
|
|
1123
|
+
atIndex:index
|
|
1124
|
+
heightBlock:^CGFloat {
|
|
1125
|
+
return height;
|
|
1126
|
+
}];
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
1130
|
+
atIndex:(NSInteger)index
|
|
1131
|
+
heightBlock:(CGFloat (^)(void))heightBlock
|
|
1132
|
+
API_AVAILABLE(ios(16.0)) {
|
|
980
1133
|
return [UISheetPresentationControllerDetent
|
|
981
1134
|
customDetentWithIdentifier:identifier
|
|
982
1135
|
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
|
|
983
1136
|
CGFloat maxDetentValue = context.maximumDetentValue;
|
|
984
1137
|
self->_detentCalculator.maxDetentHeight = maxDetentValue;
|
|
985
1138
|
|
|
1139
|
+
CGFloat height = heightBlock();
|
|
1140
|
+
CGFloat bottomAdjustment = [self detentBottomAdjustmentForHeight:height];
|
|
986
1141
|
CGFloat maxValue = self.maxContentHeight
|
|
987
1142
|
? fmin(maxDetentValue, [self.maxContentHeight floatValue])
|
|
988
1143
|
: maxDetentValue;
|
|
1144
|
+
|
|
989
1145
|
CGFloat adjustedHeight = height - bottomAdjustment;
|
|
1146
|
+
|
|
1147
|
+
// Only the last detent may occupy the full maximum. Two detents
|
|
1148
|
+
// resolving to the same height (e.g. a capped auto and 1) make
|
|
1149
|
+
// UIKit skip the stack's deck animation when presenting a child
|
|
1150
|
+
// sheet and snap the sheet behind into place instead.
|
|
1151
|
+
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1152
|
+
if (adjustedHeight >= maxValue && index < lastIndex) {
|
|
1153
|
+
maxValue -= lastIndex - index;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
990
1156
|
CGFloat resolved = fmin(adjustedHeight, maxValue);
|
|
991
1157
|
|
|
992
1158
|
NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
|