@lodev09/react-native-true-sheet 4.0.0-beta.25 → 4.0.0-beta.26
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 +1 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +31 -10
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +3 -3
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +5 -0
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +8 -16
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +9 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +0 -8
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +29 -38
- package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +6 -2
- package/ios/TrueSheetContainerView.h +1 -0
- package/ios/TrueSheetContainerView.mm +6 -0
- package/ios/TrueSheetContentView.h +1 -0
- package/ios/TrueSheetContentView.mm +2 -0
- package/ios/TrueSheetFooterView.mm +5 -0
- package/ios/TrueSheetView.mm +9 -2
- package/ios/TrueSheetViewController.h +12 -0
- package/ios/TrueSheetViewController.mm +332 -29
- package/ios/core/TrueSheetDetentCalculator.h +11 -0
- package/ios/core/TrueSheetDetentCalculator.mm +17 -5
- package/ios/core/TrueSheetKeyboardObserver.mm +89 -9
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +6 -0
- package/lib/typescript/src/TrueSheet.types.d.ts +31 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -0
- 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/package.json +1 -1
- package/src/TrueSheet.types.ts +34 -0
- package/src/fabric/TrueSheetViewNativeComponent.ts +6 -0
- package/src/navigation/types.ts +1 -0
|
@@ -49,6 +49,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
49
49
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
50
50
|
- (NSArray *)accessibilityGrabberElements;
|
|
51
51
|
- (void)endInteractiveDismissState;
|
|
52
|
+
- (void)updateHingeState;
|
|
53
|
+
- (void)correctHingeOffsetForIndex:(NSInteger)index;
|
|
54
|
+
- (CGFloat)tabletopHingeEdge;
|
|
55
|
+
- (CGFloat)hingeOffsetForHeight:(CGFloat)height;
|
|
52
56
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
53
57
|
duration:(NSTimeInterval)duration
|
|
54
58
|
allowUserInteraction:(BOOL)allowUserInteraction
|
|
@@ -78,6 +82,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
78
82
|
NSInteger _pendingDetentIndex;
|
|
79
83
|
NSInteger _displayedBackgroundIndex;
|
|
80
84
|
BOOL _usesBackgroundEffects;
|
|
85
|
+
BOOL _refusedPanDismiss;
|
|
81
86
|
|
|
82
87
|
// Present/dismiss tracking: a display link scoped to the transition
|
|
83
88
|
// coordinator — started alongside the transition, stopped in its completion.
|
|
@@ -109,11 +114,23 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
109
114
|
UIView *_anchorView;
|
|
110
115
|
|
|
111
116
|
__weak UIWindow *_accessibilityWindow;
|
|
117
|
+
BOOL _appliedAccessibilityFooterElements;
|
|
112
118
|
|
|
113
119
|
TrueSheetBlurView *_blurView;
|
|
114
120
|
TrueSheetGrabberView *_grabberView;
|
|
115
121
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
116
122
|
TrueSheetDimView *_dimView;
|
|
123
|
+
|
|
124
|
+
// Foldable hinge (iOS 27.1+). Tabletop resolves on demand from the window's
|
|
125
|
+
// division region; these track the last applied posture to detect changes.
|
|
126
|
+
BOOL _hingeStateKnown;
|
|
127
|
+
CGFloat _hingeEdge;
|
|
128
|
+
NSInteger _hingeRestoreIndex;
|
|
129
|
+
NSInteger _hingeCorrections;
|
|
130
|
+
// Offset learned at the hinge, shared by every hinge-placed detent
|
|
131
|
+
CGFloat _hingeOffset;
|
|
132
|
+
// Offset each hinge-placed detent last resolved with, checked on settle
|
|
133
|
+
NSMutableDictionary<NSNumber *, NSNumber *> *_hingeOffsets;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
#pragma mark - Initialization
|
|
@@ -148,6 +165,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
148
165
|
_pendingDetentIndex = -1;
|
|
149
166
|
|
|
150
167
|
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
168
|
+
_hingeRestoreIndex = -1;
|
|
169
|
+
_hingeOffsets = [NSMutableDictionary dictionary];
|
|
151
170
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
152
171
|
_detentCalculator.delegate = self;
|
|
153
172
|
}
|
|
@@ -234,14 +253,22 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
234
253
|
return self.currentPosition + (presentationY - modelY) + containerTy;
|
|
235
254
|
}
|
|
236
255
|
|
|
256
|
+
// Some callers run before this controller's view is attached (e.g. mid-presentation
|
|
257
|
+
// detent resolution), so fall back to the presenter's window.
|
|
258
|
+
- (UIWindow *)hostWindow {
|
|
259
|
+
return self.view.window ?: self.presentingViewController.view.window;
|
|
260
|
+
}
|
|
261
|
+
|
|
237
262
|
- (CGFloat)screenHeight {
|
|
238
263
|
// In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
|
|
239
|
-
// UIScreen.mainScreen, so prefer the window's height.
|
|
240
|
-
|
|
241
|
-
UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
|
|
264
|
+
// UIScreen.mainScreen, so prefer the window's height.
|
|
265
|
+
UIWindow *window = self.hostWindow;
|
|
242
266
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
243
267
|
}
|
|
244
268
|
|
|
269
|
+
// Gap UIKit leaves between a floating sheet (iOS 26+) and the screen's bottom edge
|
|
270
|
+
static const CGFloat TrueSheetFloatingSheetInset = 8;
|
|
271
|
+
|
|
245
272
|
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
246
273
|
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
247
274
|
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
@@ -443,6 +470,20 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
443
470
|
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
444
471
|
_grabberView.hidden = YES;
|
|
445
472
|
[_grabberView addToView:self.view];
|
|
473
|
+
|
|
474
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_1)
|
|
475
|
+
if (@available(iOS 27.1, *)) {
|
|
476
|
+
__weak __typeof(self) weakSelf = self;
|
|
477
|
+
UIHingeInteraction *hingeInteraction =
|
|
478
|
+
[[UIHingeInteraction alloc] initWithUpdateHandler:^(UIHingeInteraction *_, UIHingeInteractionUpdate *__) {
|
|
479
|
+
// Next run loop, so the division region reflects the new posture
|
|
480
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
481
|
+
[weakSelf updateHingeState];
|
|
482
|
+
});
|
|
483
|
+
}];
|
|
484
|
+
[self.view addInteraction:hingeInteraction];
|
|
485
|
+
}
|
|
486
|
+
#endif
|
|
446
487
|
}
|
|
447
488
|
|
|
448
489
|
#pragma mark - Custom Dimming
|
|
@@ -521,6 +562,11 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
521
562
|
|
|
522
563
|
[self setupDimView];
|
|
523
564
|
|
|
565
|
+
// A new presentation records the posture it starts in
|
|
566
|
+
_hingeStateKnown = NO;
|
|
567
|
+
_hingeRestoreIndex = -1;
|
|
568
|
+
_hingeOffset = 0;
|
|
569
|
+
|
|
524
570
|
// UIKit hides the presenter's keyboard under a sheet dimmed at every detent and
|
|
525
571
|
// brings it back on dismiss, but custom dimming leaves the native sheet undimmed.
|
|
526
572
|
if ([self useCustomDimming] && [self isDimmedAtEveryDetent]) {
|
|
@@ -558,6 +604,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
558
604
|
|
|
559
605
|
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
|
|
560
606
|
[self settleAtDetentIndex:index debug:@"did present"];
|
|
607
|
+
|
|
608
|
+
// Apply a posture change that happened while presenting
|
|
609
|
+
[self updateHingeState];
|
|
561
610
|
});
|
|
562
611
|
|
|
563
612
|
[self setupGestureRecognizer];
|
|
@@ -606,6 +655,34 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
606
655
|
[self setAccessibilityContentElement:contentView];
|
|
607
656
|
}
|
|
608
657
|
|
|
658
|
+
// Only the topmost sheet may refresh, or it would steal the window
|
|
659
|
+
// accessibility override from the child presented over it.
|
|
660
|
+
- (BOOL)canRefreshAccessibilityContainer {
|
|
661
|
+
return _isPresented && !self.isBeingDismissed && self.isTopmostPresentedController;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
- (void)refreshAccessibilityContainer {
|
|
665
|
+
if (self.canRefreshAccessibilityContainer) {
|
|
666
|
+
[self setupAccessibilityContainer];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
- (void)refreshAccessibilityContainerIfFooterChanged {
|
|
671
|
+
// Only a dimmed sheet's boundary depends on the footer
|
|
672
|
+
if (!_dimmed || !self.canRefreshAccessibilityContainer) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
UIView *contentView = self.accessibilityContentView;
|
|
677
|
+
if (![contentView isKindOfClass:[TrueSheetContainerView class]] ||
|
|
678
|
+
[(TrueSheetContainerView *)contentView hasAccessibilityFooterElements] == _appliedAccessibilityFooterElements) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
[self setAccessibilityContentElement:contentView];
|
|
683
|
+
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
|
|
684
|
+
}
|
|
685
|
+
|
|
609
686
|
- (UIViewController *)accessibilityPresentingViewController {
|
|
610
687
|
UIViewController *presentingViewController = self.presentingViewController;
|
|
611
688
|
while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
|
|
@@ -679,6 +756,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
679
756
|
// Footer controls exposed as separate accessibility elements can be skipped
|
|
680
757
|
// by XCTest when the presented sheet is a hard modal accessibility boundary.
|
|
681
758
|
BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
|
|
759
|
+
_appliedAccessibilityFooterElements = hasAccessibilityFooterElements;
|
|
682
760
|
|
|
683
761
|
// At a hard modal boundary XCTest skips nested elements, so flatten the
|
|
684
762
|
// container's children into the array. Otherwise expose the container itself:
|
|
@@ -820,13 +898,20 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
820
898
|
}];
|
|
821
899
|
}
|
|
822
900
|
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
901
|
+
// A swipe can still be cancelled, so emit once it commits on release. Any
|
|
902
|
+
// other dismiss (programmatic, outside tap, navigation) is already committed.
|
|
903
|
+
id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
|
|
904
|
+
if (coordinator.isInteractive) {
|
|
905
|
+
__weak __typeof(self) weakSelf = self;
|
|
906
|
+
[coordinator
|
|
907
|
+
notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
908
|
+
if (!context.isCancelled) {
|
|
909
|
+
[weakSelf emitWillDismissEvents];
|
|
910
|
+
}
|
|
911
|
+
}];
|
|
912
|
+
} else {
|
|
913
|
+
[self emitWillDismissEvents];
|
|
914
|
+
}
|
|
830
915
|
|
|
831
916
|
[self setupTransitionTracker];
|
|
832
917
|
}
|
|
@@ -846,6 +931,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
846
931
|
// the footer out of the auto detent.
|
|
847
932
|
_keyboardSheetGrown = NO;
|
|
848
933
|
_keyboardHeight = 0;
|
|
934
|
+
_minimizedKeyboardOverlap = 0;
|
|
849
935
|
|
|
850
936
|
[self emitDidDismissEvents];
|
|
851
937
|
}
|
|
@@ -902,7 +988,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
902
988
|
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
903
989
|
[self.sheet invalidateDetents];
|
|
904
990
|
}
|
|
905
|
-
|
|
991
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
992
|
+
// Rotating a half-open foldable swaps between book and tabletop
|
|
993
|
+
[self updateHingeState];
|
|
994
|
+
}];
|
|
906
995
|
}
|
|
907
996
|
}
|
|
908
997
|
|
|
@@ -943,6 +1032,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
943
1032
|
}
|
|
944
1033
|
}
|
|
945
1034
|
|
|
1035
|
+
// The hinge callback can run before the division region updates; the
|
|
1036
|
+
// relayout UIKit does for the posture change always sees the new region.
|
|
1037
|
+
[self updateHingeState];
|
|
1038
|
+
|
|
946
1039
|
if (_pendingDetentIndex >= 0) {
|
|
947
1040
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
948
1041
|
_pendingDetentIndex = -1;
|
|
@@ -1118,13 +1211,6 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1118
1211
|
CGFloat position = self.livePosition;
|
|
1119
1212
|
|
|
1120
1213
|
if (self.isBeingDismissed) {
|
|
1121
|
-
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
1122
|
-
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
1123
|
-
// detent (isBeingDismissed stays YES during the rewind).
|
|
1124
|
-
if (self.currentPosition >= self.screenHeight) {
|
|
1125
|
-
[self emitWillDismissEvents];
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
1214
|
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
1129
1215
|
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
1130
1216
|
if (self.screenHeight - position < 1) {
|
|
@@ -1145,6 +1231,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1145
1231
|
// measured frame; the next unobstructed settle learns.
|
|
1146
1232
|
if (self.presentedViewController == nil) {
|
|
1147
1233
|
[self learnOffsetForDetentIndex:index];
|
|
1234
|
+
[self correctHingeOffsetForIndex:index];
|
|
1148
1235
|
}
|
|
1149
1236
|
|
|
1150
1237
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
@@ -1304,7 +1391,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1304
1391
|
}
|
|
1305
1392
|
|
|
1306
1393
|
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
1307
|
-
if (_keyboardSheetGrown) {
|
|
1394
|
+
if (_keyboardSheetGrown || _minimizedKeyboardOverlap > 0) {
|
|
1308
1395
|
return;
|
|
1309
1396
|
}
|
|
1310
1397
|
[_detentCalculator learnOffsetForDetentIndex:index];
|
|
@@ -1338,6 +1425,146 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1338
1425
|
return [_detentCalculator detentValueForIndex:index];
|
|
1339
1426
|
}
|
|
1340
1427
|
|
|
1428
|
+
#pragma mark - Hinge
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Bottom edge (window coordinates) of an active horizontal fold, or 0 when the
|
|
1432
|
+
* device isn't in tabletop. The division region is only active while the hinge
|
|
1433
|
+
* is partially open, so this also excludes flat and closed postures.
|
|
1434
|
+
*/
|
|
1435
|
+
- (CGFloat)tabletopHingeEdge {
|
|
1436
|
+
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(27_1)
|
|
1437
|
+
if (@available(iOS 27.1, *)) {
|
|
1438
|
+
UIWindow *window = self.hostWindow;
|
|
1439
|
+
for (UIViewReservedRegion *region in [window reservedRegionsOfKind:[UIViewReservedRegionKind divisionRegionKind]]) {
|
|
1440
|
+
CGRect frame = region.frame;
|
|
1441
|
+
if (region.isActive && frame.size.width > frame.size.height) {
|
|
1442
|
+
return CGRectGetMaxY(frame);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
#endif
|
|
1447
|
+
return 0;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Gap between a hinge-placed detent's resolved height and the sheet's actual top
|
|
1452
|
+
* edge: UIKit adds the bottom safe area, plus the floating sheet's inset. Every
|
|
1453
|
+
* hinge-placed detent rests at the same height, so they share one offset — the
|
|
1454
|
+
* first settle at the hinge learns it.
|
|
1455
|
+
*/
|
|
1456
|
+
- (CGFloat)hingeOffsetForHeight:(CGFloat)height {
|
|
1457
|
+
if (_hingeOffset > 0) {
|
|
1458
|
+
return _hingeOffset;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return [self bottomSafeAreaForHeight:height] + TrueSheetFloatingSheetInset;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// A hinge-placed detent resolved with an off estimate lands a few points off
|
|
1465
|
+
// the hinge — re-resolve once the settle has learned the real offset.
|
|
1466
|
+
- (void)correctHingeOffsetForIndex:(NSInteger)index {
|
|
1467
|
+
NSNumber *usedOffset = _hingeOffsets[@(index)];
|
|
1468
|
+
if (!usedOffset || _hingeCorrections >= 2) {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
CGFloat learnedOffset = [_detentCalculator learnedOffsetForIndex:index];
|
|
1473
|
+
if (learnedOffset == 0 || fabs(learnedOffset - usedOffset.doubleValue) <= 1) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
_hingeOffset = learnedOffset;
|
|
1478
|
+
_hingeCorrections++;
|
|
1479
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1480
|
+
[self invalidateDetentsAnimated];
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
- (void)invalidateDetentsAnimated {
|
|
1485
|
+
if (@available(iOS 16.0, *)) {
|
|
1486
|
+
[self.sheet animateChanges:^{
|
|
1487
|
+
self->_pendingContentSizeChange = YES;
|
|
1488
|
+
[self.sheet invalidateDetents];
|
|
1489
|
+
}];
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Re-resolves hinge-placed detents when the posture changes, and runs 'snap':
|
|
1495
|
+
* entering tabletop moves the sheet to the hinge detent, leaving it restores the
|
|
1496
|
+
* previous detent if the sheet is still on the hinge. The posture a presentation
|
|
1497
|
+
* starts in only records state, so presenting while already in tabletop doesn't
|
|
1498
|
+
* jump the sheet. A change while presenting, dismissing, or dragging stays
|
|
1499
|
+
* pending until the sheet is at rest again.
|
|
1500
|
+
*/
|
|
1501
|
+
- (void)updateHingeState {
|
|
1502
|
+
NSInteger hingeIndex = self.foldOptions.hingeDetentIndex;
|
|
1503
|
+
if (hingeIndex < 0 && self.foldOptions.hingeBehavior == TrueSheetViewHingeBehavior::None) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Covered by a full-screen presentation, the sheet has no window to read the posture from
|
|
1508
|
+
if (!self.viewIfLoaded.window) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
CGFloat edge = [self tabletopHingeEdge];
|
|
1513
|
+
if (!_hingeStateKnown) {
|
|
1514
|
+
_hingeStateKnown = YES;
|
|
1515
|
+
_hingeEdge = edge;
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (!self.isPresented || self.isBeingDismissed || _isDragging || fabs(edge - _hingeEdge) < 0.5) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
BOOL entered = edge > 0 && _hingeEdge == 0;
|
|
1524
|
+
BOOL left = edge == 0 && _hingeEdge > 0;
|
|
1525
|
+
_hingeEdge = edge;
|
|
1526
|
+
_hingeOffset = 0;
|
|
1527
|
+
_hingeCorrections = 0;
|
|
1528
|
+
|
|
1529
|
+
NSInteger targetIndex = -1;
|
|
1530
|
+
NSInteger detentCount = (NSInteger)self.detents.count;
|
|
1531
|
+
BOOL snaps =
|
|
1532
|
+
self.foldOptions.hingeBehavior == TrueSheetViewHingeBehavior::Snap && hingeIndex >= 0 && hingeIndex < detentCount;
|
|
1533
|
+
if (entered && snaps) {
|
|
1534
|
+
_hingeRestoreIndex = self.currentDetentIndex;
|
|
1535
|
+
targetIndex = hingeIndex;
|
|
1536
|
+
} else if (left && _hingeRestoreIndex >= 0) {
|
|
1537
|
+
// Keep a detent the user picked while folded
|
|
1538
|
+
if (snaps && self.currentDetentIndex == hingeIndex) {
|
|
1539
|
+
targetIndex = MIN(_hingeRestoreIndex, detentCount - 1);
|
|
1540
|
+
}
|
|
1541
|
+
_hingeRestoreIndex = -1;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// A resize settles through the pending detent change
|
|
1545
|
+
BOOL resizes = targetIndex >= 0 && targetIndex != self.activeDetentIndex;
|
|
1546
|
+
|
|
1547
|
+
if (@available(iOS 16.0, *)) {
|
|
1548
|
+
[self.sheet animateChanges:^{
|
|
1549
|
+
[self.sheet invalidateDetents];
|
|
1550
|
+
if (resizes) {
|
|
1551
|
+
[self resizeToDetentIndex:targetIndex];
|
|
1552
|
+
}
|
|
1553
|
+
}];
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// UIKit often re-resolves detents for the posture change on its own before
|
|
1557
|
+
// this runs, so the invalidation above triggers no layout pass to settle in.
|
|
1558
|
+
// Settle once the frame is final, same as a pending detent change.
|
|
1559
|
+
if (!resizes) {
|
|
1560
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
1561
|
+
if (self.isPresented && !self.isBeingDismissed && !self->_isDragging && !self->_isTransitioning) {
|
|
1562
|
+
[self settleAtDetentIndex:self.currentDetentIndex debug:@"hinge"];
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1341
1568
|
#pragma mark - Sheet Configuration
|
|
1342
1569
|
|
|
1343
1570
|
/**
|
|
@@ -1359,12 +1586,19 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1359
1586
|
_autoDetentHeight = autoHeight;
|
|
1360
1587
|
_peekDetentHeight = peekHeight;
|
|
1361
1588
|
|
|
1589
|
+
[self invalidateDetentsAnimated];
|
|
1590
|
+
}
|
|
1591
|
+
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Re-resolves the detents in place, e.g. when an input to the resolver
|
|
1595
|
+
// changes after UIKit has already laid out the sheet.
|
|
1596
|
+
- (void)invalidateDetents {
|
|
1597
|
+
if (@available(iOS 16.0, *)) {
|
|
1362
1598
|
[self.sheet animateChanges:^{
|
|
1363
|
-
self->_pendingContentSizeChange = YES;
|
|
1364
1599
|
[self.sheet invalidateDetents];
|
|
1365
1600
|
}];
|
|
1366
1601
|
}
|
|
1367
|
-
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
1368
1602
|
}
|
|
1369
1603
|
|
|
1370
1604
|
- (void)setupSheetDetentsForDetentsChange {
|
|
@@ -1381,6 +1615,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1381
1615
|
|
|
1382
1616
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
1383
1617
|
[_detentCalculator clearResolvedHeights];
|
|
1618
|
+
_hingeCorrections = 0;
|
|
1384
1619
|
|
|
1385
1620
|
_autoDetentHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight];
|
|
1386
1621
|
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
@@ -1503,24 +1738,66 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1503
1738
|
: maxDetentValue;
|
|
1504
1739
|
|
|
1505
1740
|
CGFloat adjustedHeight = height - bottomAdjustment;
|
|
1741
|
+
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1742
|
+
|
|
1743
|
+
// Tabletop: the hinge detent rests on the hinge, and 'avoid'/'snap'
|
|
1744
|
+
// cap every detent there.
|
|
1745
|
+
BOOL avoidsHinge = self.foldOptions.hingeBehavior != TrueSheetViewHingeBehavior::None;
|
|
1746
|
+
NSInteger hingeIndex = self.foldOptions.hingeDetentIndex;
|
|
1747
|
+
BOOL hasHingeDetent = hingeIndex >= 0 && hingeIndex <= lastIndex;
|
|
1748
|
+
CGFloat hingeEdge = (hasHingeDetent || avoidsHinge) ? [self tabletopHingeEdge] : 0;
|
|
1749
|
+
CGFloat hingeOffset = 0;
|
|
1750
|
+
CGFloat hingeHeight = 0;
|
|
1751
|
+
if (hingeEdge > 0) {
|
|
1752
|
+
CGFloat panelHeight = self.screenHeight - hingeEdge;
|
|
1753
|
+
hingeOffset = [self hingeOffsetForHeight:panelHeight];
|
|
1754
|
+
hingeHeight = panelHeight - hingeOffset;
|
|
1755
|
+
if (hasHingeDetent) {
|
|
1756
|
+
// Lower detents stay below the hinge detent and higher ones above
|
|
1757
|
+
// it, so the detents keep their order
|
|
1758
|
+
CGFloat hingeDetentHeight =
|
|
1759
|
+
avoidsHinge ? hingeHeight - (lastIndex - hingeIndex) : hingeHeight;
|
|
1760
|
+
if (index == hingeIndex) {
|
|
1761
|
+
adjustedHeight = hingeHeight;
|
|
1762
|
+
} else if (index < hingeIndex) {
|
|
1763
|
+
adjustedHeight = fmin(adjustedHeight, hingeDetentHeight - (hingeIndex - index));
|
|
1764
|
+
} else {
|
|
1765
|
+
adjustedHeight = fmax(adjustedHeight, hingeDetentHeight + (index - hingeIndex));
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
if (avoidsHinge) {
|
|
1769
|
+
maxValue = fmin(maxValue, hingeHeight);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1506
1772
|
|
|
1507
1773
|
// Only the last detent may occupy the full maximum. Two detents
|
|
1508
1774
|
// resolving to the same height (e.g. a capped auto and 1) make
|
|
1509
1775
|
// UIKit skip the stack's deck animation when presenting a child
|
|
1510
1776
|
// sheet and snap the sheet behind into place instead.
|
|
1511
|
-
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1512
1777
|
if (adjustedHeight >= maxValue && index < lastIndex) {
|
|
1513
1778
|
maxValue -= lastIndex - index;
|
|
1514
1779
|
}
|
|
1515
1780
|
|
|
1516
1781
|
CGFloat resolved = fmin(adjustedHeight, maxValue);
|
|
1517
1782
|
|
|
1783
|
+
// Detents within the collapse spacing of the hinge are hinge-placed.
|
|
1784
|
+
// They rest with the hinge offset, so position events map them right
|
|
1785
|
+
// before the first settle there.
|
|
1786
|
+
if (hingeHeight > 0 && fabs(resolved - hingeHeight) <= lastIndex + 0.5) {
|
|
1787
|
+
self->_hingeOffsets[@(index)] = @(hingeOffset);
|
|
1788
|
+
[self->_detentCalculator assumeOffset:hingeOffset forIndex:index];
|
|
1789
|
+
} else {
|
|
1790
|
+
[self->_hingeOffsets removeObjectForKey:@(index)];
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1518
1793
|
NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
|
|
1519
1794
|
if (heights && index >= 0 && index < (NSInteger)heights.count) {
|
|
1520
1795
|
heights[index] = @(resolved);
|
|
1521
1796
|
}
|
|
1522
1797
|
|
|
1523
|
-
|
|
1798
|
+
// Recorded uncompensated above: UIKit adds the overlap back on
|
|
1799
|
+
// top. A sheet shorter than the overlap still grows by the rest.
|
|
1800
|
+
return fmax(resolved - self.minimizedKeyboardOverlap, 0);
|
|
1524
1801
|
}];
|
|
1525
1802
|
}
|
|
1526
1803
|
|
|
@@ -1716,11 +1993,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1716
1993
|
_grabberView.onDecrement = nil;
|
|
1717
1994
|
}
|
|
1718
1995
|
|
|
1719
|
-
|
|
1720
|
-
// window accessibility override from the child presented over it.
|
|
1721
|
-
if (_isPresented && self.isTopmostPresentedController) {
|
|
1722
|
-
[self setupAccessibilityContainer];
|
|
1723
|
-
}
|
|
1996
|
+
[self refreshAccessibilityContainer];
|
|
1724
1997
|
}
|
|
1725
1998
|
|
|
1726
1999
|
- (void)handleGrabberTap {
|
|
@@ -1861,13 +2134,43 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1861
2134
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
1862
2135
|
|
|
1863
2136
|
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
|
|
1864
|
-
|
|
2137
|
+
// A sheet that can't be dragged can't be swiped away either, including by
|
|
2138
|
+
// pulling down its scroll view: refuse a dismissal while any pan is under
|
|
2139
|
+
// way, and keep the one from a tap outside the sheet. Reset on every call:
|
|
2140
|
+
// UIKit skips didAttemptToDismiss for a short pull.
|
|
2141
|
+
_refusedPanDismiss =
|
|
2142
|
+
self.dismissible && !self.draggable && [self hasActivePanInView:presentationController.containerView];
|
|
2143
|
+
return self.dismissible && !_refusedPanDismiss;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
- (BOOL)hasActivePanInView:(UIView *)view {
|
|
2147
|
+
if (!view) {
|
|
2148
|
+
return NO;
|
|
2149
|
+
}
|
|
2150
|
+
for (UIGestureRecognizer *recognizer in view.gestureRecognizers ?: @[]) {
|
|
2151
|
+
if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
|
|
2152
|
+
(recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged)) {
|
|
2153
|
+
return YES;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
for (UIView *subview in view.subviews) {
|
|
2157
|
+
if ([self hasActivePanInView:subview]) {
|
|
2158
|
+
return YES;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return NO;
|
|
1865
2162
|
}
|
|
1866
2163
|
|
|
1867
2164
|
// UIKit calls this after shouldDismiss returns NO — the sheet has already
|
|
1868
2165
|
// bounced back, so JS can confirm and dismiss programmatically (Apple's
|
|
1869
2166
|
// unsaved-changes pattern).
|
|
1870
2167
|
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
|
|
2168
|
+
// A swipe refused only because the sheet can't be dragged isn't an attempt
|
|
2169
|
+
// to dismiss a non-dismissible sheet.
|
|
2170
|
+
if (_refusedPanDismiss) {
|
|
2171
|
+
_refusedPanDismiss = NO;
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
1871
2174
|
[self.delegate viewControllerDidAttemptDismiss];
|
|
1872
2175
|
}
|
|
1873
2176
|
|
|
@@ -76,6 +76,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
76
76
|
*/
|
|
77
77
|
- (CGFloat)resolvedHeightForIndex:(NSInteger)index;
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
Returns the offset learned for this detent itself, or 0 when none is learned.
|
|
81
|
+
*/
|
|
82
|
+
- (CGFloat)learnedOffsetForIndex:(NSInteger)index;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
Sets the offset this detent is expected to rest with, unless its learned
|
|
86
|
+
offset is already within a point of it.
|
|
87
|
+
*/
|
|
88
|
+
- (void)assumeOffset:(CGFloat)offset forIndex:(NSInteger)index;
|
|
89
|
+
|
|
79
90
|
/**
|
|
80
91
|
Finds the segment between detents for a given position.
|
|
81
92
|
Returns YES if position is between two detents, NO if at edges.
|
|
@@ -80,13 +80,25 @@
|
|
|
80
80
|
return detentValue * self.delegate.screenHeight;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
- (CGFloat)
|
|
84
|
-
// Use this detent's own offset if available
|
|
83
|
+
- (CGFloat)learnedOffsetForIndex:(NSInteger)index {
|
|
85
84
|
if (index >= 0 && index < (NSInteger)_resolvedDetentOffsets.count) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
return [_resolvedDetentOffsets[index] doubleValue];
|
|
86
|
+
}
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
- (void)assumeOffset:(CGFloat)offset forIndex:(NSInteger)index {
|
|
91
|
+
if (index >= 0 && index < (NSInteger)_resolvedDetentOffsets.count &&
|
|
92
|
+
fabs([_resolvedDetentOffsets[index] doubleValue] - offset) > 1) {
|
|
93
|
+
_resolvedDetentOffsets[index] = @(offset);
|
|
89
94
|
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (CGFloat)offsetForIndex:(NSInteger)index {
|
|
98
|
+
// Use this detent's own offset if available
|
|
99
|
+
CGFloat learnedOffset = [self learnedOffsetForIndex:index];
|
|
100
|
+
if (learnedOffset != 0)
|
|
101
|
+
return learnedOffset;
|
|
90
102
|
|
|
91
103
|
// Fall back to any known offset
|
|
92
104
|
for (NSInteger i = 0; i < (NSInteger)_resolvedDetentOffsets.count; i++) {
|