@lodev09/react-native-true-sheet 4.0.0-beta.24 → 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 +2 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +13 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +12 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +120 -19
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +103 -27
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -10
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +31 -14
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +9 -0
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +3 -16
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +14 -1
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +11 -17
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +2 -4
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +29 -38
- package/android/src/main/java/com/lodev09/truesheet/utils/ColorUtils.kt +19 -0
- 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/TrueSheetModule.mm +11 -2
- package/ios/TrueSheetView.h +2 -0
- package/ios/TrueSheetView.mm +62 -9
- package/ios/TrueSheetViewController.h +14 -0
- package/ios/TrueSheetViewController.mm +490 -33
- package/ios/core/RNScreensEventObserver.mm +18 -1
- package/ios/core/TrueSheetDetentCalculator.h +11 -0
- package/ios/core/TrueSheetDetentCalculator.mm +17 -5
- package/ios/core/TrueSheetDimView.h +27 -0
- package/ios/core/TrueSheetDimView.mm +41 -0
- package/ios/core/TrueSheetKeyboardObserver.mm +98 -15
- package/lib/module/TrueSheet.js +88 -17
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +150 -37
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +13 -9
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -0
- package/lib/module/scrollable.js +28 -0
- package/lib/module/scrollable.js.map +1 -0
- package/lib/module/web/keyboard.js +69 -0
- package/lib/module/web/keyboard.js.map +1 -0
- package/lib/module/web/layout.js +1 -1
- package/lib/module/web/layout.js.map +1 -1
- package/lib/module/web/vaul/context.js.map +1 -1
- package/lib/module/web/vaul/index.js +18 -8
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/use-snap-points.js +15 -10
- package/lib/module/web/vaul/use-snap-points.js.map +1 -1
- package/lib/typescript/src/TrueSheet.d.ts +14 -1
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +53 -2
- 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/TrueSheetProvider.web.d.ts +5 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +7 -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/lib/typescript/src/scrollable.d.ts +2 -0
- package/lib/typescript/src/scrollable.d.ts.map +1 -0
- package/lib/typescript/src/web/keyboard.d.ts +15 -0
- package/lib/typescript/src/web/keyboard.d.ts.map +1 -0
- package/lib/typescript/src/web/layout.d.ts +1 -0
- package/lib/typescript/src/web/layout.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/context.d.ts +2 -1
- package/lib/typescript/src/web/vaul/context.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/lib/typescript/src/web/vaul/types.d.ts +9 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +3 -2
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TrueSheet.tsx +91 -14
- package/src/TrueSheet.types.ts +60 -2
- package/src/TrueSheet.web.tsx +170 -41
- package/src/TrueSheetProvider.web.tsx +22 -13
- package/src/fabric/TrueSheetViewNativeComponent.ts +8 -0
- package/src/navigation/types.ts +3 -0
- package/src/scrollable.ts +25 -0
- package/src/web/keyboard.ts +79 -0
- package/src/web/layout.ts +1 -1
- package/src/web/vaul/context.ts +2 -1
- package/src/web/vaul/index.tsx +20 -5
- package/src/web/vaul/types.ts +10 -0
- package/src/web/vaul/use-snap-points.ts +19 -9
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
#import "TrueSheetContentView.h"
|
|
12
12
|
#import "core/TrueSheetBlurView.h"
|
|
13
13
|
#import "core/TrueSheetDetentCalculator.h"
|
|
14
|
+
#import "core/TrueSheetDimView.h"
|
|
14
15
|
#import "core/TrueSheetGrabberView.h"
|
|
15
16
|
#import "utils/BlurUtil.h"
|
|
16
17
|
#import "utils/GestureUtil.h"
|
|
17
18
|
#import "utils/PlatformUtil.h"
|
|
19
|
+
#import "utils/UIView+FirstResponder.h"
|
|
18
20
|
#import "utils/WindowUtil.h"
|
|
19
21
|
|
|
20
22
|
#import <React/RCTLog.h>
|
|
@@ -47,6 +49,10 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
47
49
|
- (void)setAccessibilityContentElement:(UIView *)contentView;
|
|
48
50
|
- (NSArray *)accessibilityGrabberElements;
|
|
49
51
|
- (void)endInteractiveDismissState;
|
|
52
|
+
- (void)updateHingeState;
|
|
53
|
+
- (void)correctHingeOffsetForIndex:(NSInteger)index;
|
|
54
|
+
- (CGFloat)tabletopHingeEdge;
|
|
55
|
+
- (CGFloat)hingeOffsetForHeight:(CGFloat)height;
|
|
50
56
|
- (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
|
|
51
57
|
duration:(NSTimeInterval)duration
|
|
52
58
|
allowUserInteraction:(BOOL)allowUserInteraction
|
|
@@ -76,11 +82,14 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
76
82
|
NSInteger _pendingDetentIndex;
|
|
77
83
|
NSInteger _displayedBackgroundIndex;
|
|
78
84
|
BOOL _usesBackgroundEffects;
|
|
85
|
+
BOOL _refusedPanDismiss;
|
|
79
86
|
|
|
80
87
|
// Present/dismiss tracking: a display link scoped to the transition
|
|
81
88
|
// coordinator — started alongside the transition, stopped in its completion.
|
|
82
89
|
CADisplayLink *_transitionLink;
|
|
83
90
|
BOOL _isTransitioning;
|
|
91
|
+
// Where the sheet rests during the transition: the target on present, the start on dismiss
|
|
92
|
+
CGFloat _transitionRestPosition;
|
|
84
93
|
|
|
85
94
|
BOOL _pendingContentSizeChange;
|
|
86
95
|
BOOL _pendingDetentsChange;
|
|
@@ -91,6 +100,7 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
91
100
|
// for the next present (beats the float-threshold guess for re-presents).
|
|
92
101
|
BOOL _hasObservedBottomInset;
|
|
93
102
|
CGFloat _observedBottomInset;
|
|
103
|
+
BOOL _isRefreshingSafeArea;
|
|
94
104
|
|
|
95
105
|
BOOL _isInteractiveDismiss;
|
|
96
106
|
CGFloat _interactiveStartPosition;
|
|
@@ -99,14 +109,28 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
99
109
|
CADisplayLink *_interactivePositionLink;
|
|
100
110
|
|
|
101
111
|
__weak TrueSheetViewController *_parentSheetController;
|
|
112
|
+
__weak UIView *_presenterFirstResponder;
|
|
102
113
|
|
|
103
114
|
UIView *_anchorView;
|
|
104
115
|
|
|
105
116
|
__weak UIWindow *_accessibilityWindow;
|
|
117
|
+
BOOL _appliedAccessibilityFooterElements;
|
|
106
118
|
|
|
107
119
|
TrueSheetBlurView *_blurView;
|
|
108
120
|
TrueSheetGrabberView *_grabberView;
|
|
109
121
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
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;
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
#pragma mark - Initialization
|
|
@@ -126,6 +150,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
126
150
|
_footerAvoidsKeyboard = YES;
|
|
127
151
|
_dismissible = YES;
|
|
128
152
|
_dimmed = YES;
|
|
153
|
+
_dimColor = nil;
|
|
154
|
+
_dimOpacity = nil;
|
|
129
155
|
_dimmedDetentIndex = @(0);
|
|
130
156
|
_presentation = facebook::react::TrueSheetViewPresentation::Page;
|
|
131
157
|
_lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
|
|
@@ -139,6 +165,8 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
139
165
|
_pendingDetentIndex = -1;
|
|
140
166
|
|
|
141
167
|
_insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
|
|
168
|
+
_hingeRestoreIndex = -1;
|
|
169
|
+
_hingeOffsets = [NSMutableDictionary dictionary];
|
|
142
170
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
143
171
|
_detentCalculator.delegate = self;
|
|
144
172
|
}
|
|
@@ -225,14 +253,22 @@ static char TrueSheetAccessibilityWindowPreviousElementsKey;
|
|
|
225
253
|
return self.currentPosition + (presentationY - modelY) + containerTy;
|
|
226
254
|
}
|
|
227
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
|
+
|
|
228
262
|
- (CGFloat)screenHeight {
|
|
229
263
|
// In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
|
|
230
|
-
// UIScreen.mainScreen, so prefer the window's height.
|
|
231
|
-
|
|
232
|
-
UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
|
|
264
|
+
// UIScreen.mainScreen, so prefer the window's height.
|
|
265
|
+
UIWindow *window = self.hostWindow;
|
|
233
266
|
return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
// Gap UIKit leaves between a floating sheet (iOS 26+) and the screen's bottom edge
|
|
270
|
+
static const CGFloat TrueSheetFloatingSheetInset = 8;
|
|
271
|
+
|
|
236
272
|
// Only phone sheets absorb the bottom safe-area inset — non-phone idioms
|
|
237
273
|
// (iPad form sheets) are inset from the screen edge by UIKit.
|
|
238
274
|
static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
@@ -316,6 +352,12 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
316
352
|
// wrong leaves the safe-area region unfilled (gap) or double-padded.
|
|
317
353
|
UIView *view = self.viewIfLoaded;
|
|
318
354
|
if (view.window) {
|
|
355
|
+
// Mid-present, hold the expected inset over a stale zero so the footer
|
|
356
|
+
// doesn't shrink and regrow — viewDidAppear forces the real value.
|
|
357
|
+
if (!_isPresented && [self hasStaleBottomSafeArea]) {
|
|
358
|
+
return [self bottomSafeAreaForHeight:view.bounds.size.height];
|
|
359
|
+
}
|
|
360
|
+
|
|
319
361
|
_hasObservedBottomInset = YES;
|
|
320
362
|
_observedBottomInset = view.safeAreaInsets.bottom;
|
|
321
363
|
return _observedBottomInset;
|
|
@@ -428,6 +470,81 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
428
470
|
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
429
471
|
_grabberView.hidden = YES;
|
|
430
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
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#pragma mark - Custom Dimming
|
|
490
|
+
|
|
491
|
+
// UISheetPresentationController has no API for the dim's color or opacity, so a custom
|
|
492
|
+
// color/opacity turns the native dim off and fades in our own view behind the sheet.
|
|
493
|
+
- (BOOL)useCustomDimming {
|
|
494
|
+
return self.dimmed && (self.dimColor != nil || self.dimOpacity != nil);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
- (BOOL)isDimmedAtEveryDetent {
|
|
498
|
+
return self.dimmed && [self.dimmedDetentIndex integerValue] == 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Like the system dim, fades in between the detent below `dimmedDetentIndex` and the dimmed detent.
|
|
502
|
+
- (CGFloat)dimAlphaForIndex:(CGFloat)index {
|
|
503
|
+
if (![self useCustomDimming]) {
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
CGFloat progress = index - [self.dimmedDetentIndex integerValue] + 1;
|
|
508
|
+
return fmax(0, fmin(1, progress));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Present and dismiss fade the dim over the whole travel below the resting position.
|
|
512
|
+
- (CGFloat)dimAlphaForPosition:(CGFloat)position index:(CGFloat)index {
|
|
513
|
+
if (!_isTransitioning || position <= _transitionRestPosition) {
|
|
514
|
+
return [self dimAlphaForIndex:index];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
CGFloat travel = self.screenHeight - _transitionRestPosition;
|
|
518
|
+
CGFloat progress = travel > 0 ? fmax(0, (self.screenHeight - position) / travel) : 0;
|
|
519
|
+
return [self dimAlphaForIndex:[self interpolatedIndexForPosition:_transitionRestPosition]] * progress;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
- (void)setupDimView {
|
|
523
|
+
UIView *container = self.presentationController.containerView;
|
|
524
|
+
if (!container || (!_dimView && ![self useCustomDimming])) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!_dimView) {
|
|
529
|
+
_dimView = [[TrueSheetDimView alloc] init];
|
|
530
|
+
|
|
531
|
+
__weak __typeof(self) weakSelf = self;
|
|
532
|
+
_dimView.onTap = ^{
|
|
533
|
+
[weakSelf handleDimViewTap];
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Keep opacity in the color so a transparent dim still receives touches.
|
|
538
|
+
UIColor *color = self.dimColor ?: [UIColor blackColor];
|
|
539
|
+
CGFloat opacity = self.dimOpacity ? [self.dimOpacity doubleValue] : 0.5;
|
|
540
|
+
_dimView.backgroundColor = [color colorWithAlphaComponent:CGColorGetAlpha(color.CGColor) * opacity];
|
|
541
|
+
[_dimView addToView:container];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
- (void)handleDimViewTap {
|
|
545
|
+
if (self.dismissible) {
|
|
546
|
+
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
|
547
|
+
}
|
|
431
548
|
}
|
|
432
549
|
|
|
433
550
|
- (void)viewWillAppear:(BOOL)animated {
|
|
@@ -443,6 +560,22 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
443
560
|
[_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
|
|
444
561
|
}
|
|
445
562
|
|
|
563
|
+
[self setupDimView];
|
|
564
|
+
|
|
565
|
+
// A new presentation records the posture it starts in
|
|
566
|
+
_hingeStateKnown = NO;
|
|
567
|
+
_hingeRestoreIndex = -1;
|
|
568
|
+
_hingeOffset = 0;
|
|
569
|
+
|
|
570
|
+
// UIKit hides the presenter's keyboard under a sheet dimmed at every detent and
|
|
571
|
+
// brings it back on dismiss, but custom dimming leaves the native sheet undimmed.
|
|
572
|
+
if ([self useCustomDimming] && [self isDimmedAtEveryDetent]) {
|
|
573
|
+
UIView *firstResponder = [presenter.view findFirstResponder];
|
|
574
|
+
if ([firstResponder resignFirstResponder]) {
|
|
575
|
+
_presenterFirstResponder = firstResponder;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
446
579
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
447
580
|
NSInteger index = self.currentDetentIndex;
|
|
448
581
|
CGFloat position = self.currentPosition;
|
|
@@ -471,15 +604,48 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
471
604
|
|
|
472
605
|
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
|
|
473
606
|
[self settleAtDetentIndex:index debug:@"did present"];
|
|
607
|
+
|
|
608
|
+
// Apply a posture change that happened while presenting
|
|
609
|
+
[self updateHingeState];
|
|
474
610
|
});
|
|
475
611
|
|
|
476
612
|
[self setupGestureRecognizer];
|
|
477
613
|
_isPresented = YES;
|
|
614
|
+
[self refreshStaleBottomSafeArea];
|
|
478
615
|
}
|
|
479
616
|
|
|
480
617
|
[self setupAccessibilityContainer];
|
|
481
618
|
}
|
|
482
619
|
|
|
620
|
+
/**
|
|
621
|
+
* A present that overlaps another transition (e.g. re-presenting while a
|
|
622
|
+
* native stack pops) can leave the root view's bottom safe area at zero, and
|
|
623
|
+
* UIKit doesn't recompute it until the sheet's frame next changes. Suspect a
|
|
624
|
+
* zero the presented height says shouldn't be there — a small floating sheet
|
|
625
|
+
* legitimately has none.
|
|
626
|
+
*/
|
|
627
|
+
- (BOOL)hasStaleBottomSafeArea {
|
|
628
|
+
UIView *view = self.viewIfLoaded;
|
|
629
|
+
return view.window && view.safeAreaInsets.bottom <= 0 && [self bottomSafeAreaForHeight:view.bounds.size.height] > 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Toggling additionalSafeAreaInsets forces UIKit to recompute the safe area.
|
|
633
|
+
// The toggle's intermediate value isn't reported, so the footer only sees the
|
|
634
|
+
// settled one.
|
|
635
|
+
- (void)refreshStaleBottomSafeArea {
|
|
636
|
+
if (![self hasStaleBottomSafeArea]) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_isRefreshingSafeArea = YES;
|
|
641
|
+
UIEdgeInsets insets = self.additionalSafeAreaInsets;
|
|
642
|
+
self.additionalSafeAreaInsets = UIEdgeInsetsMake(insets.top, insets.left, insets.bottom + 1, insets.right);
|
|
643
|
+
self.additionalSafeAreaInsets = insets;
|
|
644
|
+
_isRefreshingSafeArea = NO;
|
|
645
|
+
|
|
646
|
+
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
647
|
+
}
|
|
648
|
+
|
|
483
649
|
- (void)setupAccessibilityContainer {
|
|
484
650
|
UIView *contentView = self.accessibilityContentView;
|
|
485
651
|
if (!contentView) {
|
|
@@ -489,6 +655,34 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
489
655
|
[self setAccessibilityContentElement:contentView];
|
|
490
656
|
}
|
|
491
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
|
+
|
|
492
686
|
- (UIViewController *)accessibilityPresentingViewController {
|
|
493
687
|
UIViewController *presentingViewController = self.presentingViewController;
|
|
494
688
|
while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
|
|
@@ -562,6 +756,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
562
756
|
// Footer controls exposed as separate accessibility elements can be skipped
|
|
563
757
|
// by XCTest when the presented sheet is a hard modal accessibility boundary.
|
|
564
758
|
BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
|
|
759
|
+
_appliedAccessibilityFooterElements = hasAccessibilityFooterElements;
|
|
565
760
|
|
|
566
761
|
// At a hard modal boundary XCTest skips nested elements, so flatten the
|
|
567
762
|
// container's children into the array. Otherwise expose the container itself:
|
|
@@ -656,6 +851,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
656
851
|
[self.delegate viewControllerWillBlur];
|
|
657
852
|
[self.delegate viewControllerWillDismiss];
|
|
658
853
|
[_parentSheetController.delegate viewControllerWillFocus];
|
|
854
|
+
|
|
855
|
+
// Restored once the dismiss commits, like UIKit's own keyboard preservation.
|
|
856
|
+
UIView *firstResponder = _presenterFirstResponder;
|
|
857
|
+
_presenterFirstResponder = nil;
|
|
858
|
+
if (!self.presentingViewController.isBeingDismissed) {
|
|
859
|
+
[firstResponder becomeFirstResponder];
|
|
860
|
+
}
|
|
659
861
|
}
|
|
660
862
|
}
|
|
661
863
|
|
|
@@ -666,6 +868,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
666
868
|
_isWillDismissEmitted = NO;
|
|
667
869
|
_hasObservedBottomInset = NO;
|
|
668
870
|
|
|
871
|
+
[_dimView removeFromSuperview];
|
|
872
|
+
_dimView = nil;
|
|
873
|
+
|
|
669
874
|
[_anchorView removeFromSuperview];
|
|
670
875
|
_anchorView = nil;
|
|
671
876
|
|
|
@@ -684,13 +889,29 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
684
889
|
[self restoreWindowAccessibilityElements];
|
|
685
890
|
[self setSheetAccessibilityElementsHidden:YES];
|
|
686
891
|
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
892
|
+
// Dismissing with a child on top only animates the child out — this sheet
|
|
893
|
+
// doesn't move and isn't part of that transition, so fade its dim over it.
|
|
894
|
+
if (self.isBeingDismissed && self.presentedViewController) {
|
|
895
|
+
[UIView animateWithDuration:self.transitionCoordinator.transitionDuration
|
|
896
|
+
animations:^{
|
|
897
|
+
self->_dimView.alpha = 0;
|
|
898
|
+
}];
|
|
899
|
+
}
|
|
900
|
+
|
|
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
|
+
}
|
|
694
915
|
|
|
695
916
|
[self setupTransitionTracker];
|
|
696
917
|
}
|
|
@@ -710,6 +931,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
710
931
|
// the footer out of the auto detent.
|
|
711
932
|
_keyboardSheetGrown = NO;
|
|
712
933
|
_keyboardHeight = 0;
|
|
934
|
+
_minimizedKeyboardOverlap = 0;
|
|
713
935
|
|
|
714
936
|
[self emitDidDismissEvents];
|
|
715
937
|
}
|
|
@@ -766,7 +988,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
766
988
|
animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
767
989
|
[self.sheet invalidateDetents];
|
|
768
990
|
}
|
|
769
|
-
|
|
991
|
+
completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
|
992
|
+
// Rotating a half-open foldable swaps between book and tabletop
|
|
993
|
+
[self updateHingeState];
|
|
994
|
+
}];
|
|
770
995
|
}
|
|
771
996
|
}
|
|
772
997
|
|
|
@@ -779,7 +1004,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
779
1004
|
|
|
780
1005
|
// Mid-dismiss the frame moves off-screen and reports transient insets —
|
|
781
1006
|
// don't let them pollute the observed value (matches viewDidLayoutSubviews).
|
|
782
|
-
if (!self.isBeingDismissed) {
|
|
1007
|
+
if (!self.isBeingDismissed && !_isRefreshingSafeArea) {
|
|
783
1008
|
[self.delegate viewControllerSafeAreaInsetsDidChange];
|
|
784
1009
|
}
|
|
785
1010
|
}
|
|
@@ -807,6 +1032,10 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
807
1032
|
}
|
|
808
1033
|
}
|
|
809
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
|
+
|
|
810
1039
|
if (_pendingDetentIndex >= 0) {
|
|
811
1040
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
812
1041
|
_pendingDetentIndex = -1;
|
|
@@ -928,6 +1157,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
928
1157
|
}
|
|
929
1158
|
|
|
930
1159
|
_isTransitioning = YES;
|
|
1160
|
+
_transitionRestPosition = self.isBeingDismissed ? self.livePosition : self.currentPosition;
|
|
931
1161
|
|
|
932
1162
|
// Learn the resolver-vs-actual offset before emitting transition positions so
|
|
933
1163
|
// the interpolated index lands exactly on the target detent. The presented
|
|
@@ -963,10 +1193,13 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
963
1193
|
// Settle after a present, cancelled dismiss, or detent-snap transition.
|
|
964
1194
|
// Delayed because the presentedView frame isn't final until UIKit
|
|
965
1195
|
// completes its layout pass after the transition animation — learning
|
|
966
|
-
// here absorbs any sub-pixel drift since the earlier learns.
|
|
1196
|
+
// here absorbs any sub-pixel drift since the earlier learns. A dismiss
|
|
1197
|
+
// cancelled mid-drag leaves the finger down, so the drag end settles instead.
|
|
967
1198
|
if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
|
|
968
1199
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
969
|
-
|
|
1200
|
+
if (!strongSelf->_isDragging) {
|
|
1201
|
+
[strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
|
|
1202
|
+
}
|
|
970
1203
|
});
|
|
971
1204
|
}
|
|
972
1205
|
}];
|
|
@@ -978,13 +1211,6 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
978
1211
|
CGFloat position = self.livePosition;
|
|
979
1212
|
|
|
980
1213
|
if (self.isBeingDismissed) {
|
|
981
|
-
// Emit only once the dismiss is committed: on release UIKit moves the
|
|
982
|
-
// model frame off-screen, while a cancelled drag rewinds it back to the
|
|
983
|
-
// detent (isBeingDismissed stays YES during the rewind).
|
|
984
|
-
if (self.currentPosition >= self.screenHeight) {
|
|
985
|
-
[self emitWillDismissEvents];
|
|
986
|
-
}
|
|
987
|
-
|
|
988
1214
|
// Hide blur at the end of dismiss to prevent UIVisualEffectView
|
|
989
1215
|
// from causing a flicker/flash at the bottom edge of the sheet.
|
|
990
1216
|
if (self.screenHeight - position < 1) {
|
|
@@ -1005,6 +1231,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1005
1231
|
// measured frame; the next unobstructed settle learns.
|
|
1006
1232
|
if (self.presentedViewController == nil) {
|
|
1007
1233
|
[self learnOffsetForDetentIndex:index];
|
|
1234
|
+
[self correctHingeOffsetForIndex:index];
|
|
1008
1235
|
}
|
|
1009
1236
|
|
|
1010
1237
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
|
|
@@ -1145,6 +1372,11 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1145
1372
|
.index = [self interpolatedIndexForPosition:position],
|
|
1146
1373
|
};
|
|
1147
1374
|
|
|
1375
|
+
// A child on top only pushes this sheet back, its detent stays.
|
|
1376
|
+
if (!presented) {
|
|
1377
|
+
_dimView.alpha = [self dimAlphaForPosition:position index:state.index];
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1148
1380
|
// Settle (non-realtime) emits are authoritative and bypass the dedupe — a
|
|
1149
1381
|
// learn right before them can correct the interpolated index by less than
|
|
1150
1382
|
// the tolerance, and the corrected value must still reach JS.
|
|
@@ -1159,7 +1391,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1159
1391
|
}
|
|
1160
1392
|
|
|
1161
1393
|
- (void)learnOffsetForDetentIndex:(NSInteger)index {
|
|
1162
|
-
if (_keyboardSheetGrown) {
|
|
1394
|
+
if (_keyboardSheetGrown || _minimizedKeyboardOverlap > 0) {
|
|
1163
1395
|
return;
|
|
1164
1396
|
}
|
|
1165
1397
|
[_detentCalculator learnOffsetForDetentIndex:index];
|
|
@@ -1193,6 +1425,146 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1193
1425
|
return [_detentCalculator detentValueForIndex:index];
|
|
1194
1426
|
}
|
|
1195
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
|
+
|
|
1196
1568
|
#pragma mark - Sheet Configuration
|
|
1197
1569
|
|
|
1198
1570
|
/**
|
|
@@ -1214,12 +1586,19 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1214
1586
|
_autoDetentHeight = autoHeight;
|
|
1215
1587
|
_peekDetentHeight = peekHeight;
|
|
1216
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, *)) {
|
|
1217
1598
|
[self.sheet animateChanges:^{
|
|
1218
|
-
self->_pendingContentSizeChange = YES;
|
|
1219
1599
|
[self.sheet invalidateDetents];
|
|
1220
1600
|
}];
|
|
1221
1601
|
}
|
|
1222
|
-
// iOS 15: detents are fixed medium/large — size changes can't affect them.
|
|
1223
1602
|
}
|
|
1224
1603
|
|
|
1225
1604
|
- (void)setupSheetDetentsForDetentsChange {
|
|
@@ -1236,6 +1615,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1236
1615
|
|
|
1237
1616
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
1238
1617
|
[_detentCalculator clearResolvedHeights];
|
|
1618
|
+
_hingeCorrections = 0;
|
|
1239
1619
|
|
|
1240
1620
|
_autoDetentHeight = [_detentCalculator autoHeightForKeyboardHeight:_keyboardHeight];
|
|
1241
1621
|
_peekDetentHeight = [_detentCalculator peekHeight];
|
|
@@ -1263,7 +1643,9 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1263
1643
|
|
|
1264
1644
|
sheet.detents = detents;
|
|
1265
1645
|
|
|
1266
|
-
if (
|
|
1646
|
+
if ([self useCustomDimming]) {
|
|
1647
|
+
sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
|
|
1648
|
+
} else if ([self isDimmedAtEveryDetent]) {
|
|
1267
1649
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
1268
1650
|
} else {
|
|
1269
1651
|
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
@@ -1356,24 +1738,66 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1356
1738
|
: maxDetentValue;
|
|
1357
1739
|
|
|
1358
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
|
+
}
|
|
1359
1772
|
|
|
1360
1773
|
// Only the last detent may occupy the full maximum. Two detents
|
|
1361
1774
|
// resolving to the same height (e.g. a capped auto and 1) make
|
|
1362
1775
|
// UIKit skip the stack's deck animation when presenting a child
|
|
1363
1776
|
// sheet and snap the sheet behind into place instead.
|
|
1364
|
-
NSInteger lastIndex = (NSInteger)self.detents.count - 1;
|
|
1365
1777
|
if (adjustedHeight >= maxValue && index < lastIndex) {
|
|
1366
1778
|
maxValue -= lastIndex - index;
|
|
1367
1779
|
}
|
|
1368
1780
|
|
|
1369
1781
|
CGFloat resolved = fmin(adjustedHeight, maxValue);
|
|
1370
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
|
+
|
|
1371
1793
|
NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
|
|
1372
1794
|
if (heights && index >= 0 && index < (NSInteger)heights.count) {
|
|
1373
1795
|
heights[index] = @(resolved);
|
|
1374
1796
|
}
|
|
1375
1797
|
|
|
1376
|
-
|
|
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);
|
|
1377
1801
|
}];
|
|
1378
1802
|
}
|
|
1379
1803
|
|
|
@@ -1569,11 +1993,7 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1569
1993
|
_grabberView.onDecrement = nil;
|
|
1570
1994
|
}
|
|
1571
1995
|
|
|
1572
|
-
|
|
1573
|
-
// window accessibility override from the child presented over it.
|
|
1574
|
-
if (_isPresented && self.isTopmostPresentedController) {
|
|
1575
|
-
[self setupAccessibilityContainer];
|
|
1576
|
-
}
|
|
1996
|
+
[self refreshAccessibilityContainer];
|
|
1577
1997
|
}
|
|
1578
1998
|
|
|
1579
1999
|
- (void)handleGrabberTap {
|
|
@@ -1702,18 +2122,55 @@ static BOOL TrueSheetIsPhoneIdiom(void) {
|
|
|
1702
2122
|
|
|
1703
2123
|
[self setupBackground];
|
|
1704
2124
|
[self setupGrabber];
|
|
2125
|
+
|
|
2126
|
+
// Dim props changed while presented. A hidden custom dim stays until dismiss;
|
|
2127
|
+
// at alpha 0 it no longer takes touches.
|
|
2128
|
+
if (_isPresented) {
|
|
2129
|
+
[self setupDimView];
|
|
2130
|
+
_dimView.alpha = [self dimAlphaForIndex:self.currentDetentIndex];
|
|
2131
|
+
}
|
|
1705
2132
|
}
|
|
1706
2133
|
|
|
1707
2134
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
1708
2135
|
|
|
1709
2136
|
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
|
|
1710
|
-
|
|
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;
|
|
1711
2162
|
}
|
|
1712
2163
|
|
|
1713
2164
|
// UIKit calls this after shouldDismiss returns NO — the sheet has already
|
|
1714
2165
|
// bounced back, so JS can confirm and dismiss programmatically (Apple's
|
|
1715
2166
|
// unsaved-changes pattern).
|
|
1716
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
|
+
}
|
|
1717
2174
|
[self.delegate viewControllerDidAttemptDismiss];
|
|
1718
2175
|
}
|
|
1719
2176
|
|