819 lines
26 KiB
Plaintext
819 lines
26 KiB
Plaintext
//
|
|
// Created by Jovanni Lo (@lodev09)
|
|
// Copyright (c) 2024-present. All rights reserved.
|
|
//
|
|
// This source code is licensed under the MIT license found in the
|
|
// LICENSE file in the root directory of this source tree.
|
|
//
|
|
|
|
#import "TrueSheetViewController.h"
|
|
#import "TrueSheetContentView.h"
|
|
#import "core/TrueSheetBlurView.h"
|
|
#import "core/TrueSheetDetentCalculator.h"
|
|
#import "core/TrueSheetGrabberView.h"
|
|
#import "utils/BlurUtil.h"
|
|
#import "utils/GestureUtil.h"
|
|
#import "utils/PlatformUtil.h"
|
|
#import "utils/WindowUtil.h"
|
|
|
|
#import <React/RCTLog.h>
|
|
#import <React/RCTScrollViewComponentView.h>
|
|
|
|
@interface TrueSheetViewController ()
|
|
|
|
@end
|
|
|
|
@implementation TrueSheetViewController {
|
|
CGFloat _lastPosition;
|
|
NSInteger _pendingDetentIndex;
|
|
BOOL _pendingContentSizeChange;
|
|
BOOL _pendingDetentsChange;
|
|
|
|
CADisplayLink *_transitioningTimer;
|
|
UIView *_transitionFakeView;
|
|
BOOL _isDragging;
|
|
BOOL _isTransitioning;
|
|
BOOL _isTrackingPositionFromLayout;
|
|
|
|
__weak TrueSheetViewController *_parentSheetController;
|
|
|
|
TrueSheetBlurView *_blurView;
|
|
TrueSheetGrabberView *_grabberView;
|
|
TrueSheetDetentCalculator *_detentCalculator;
|
|
}
|
|
|
|
#pragma mark - Initialization
|
|
|
|
- (instancetype)init {
|
|
if (self = [super initWithNibName:nil bundle:nil]) {
|
|
_detents = @[ @0.5, @1 ];
|
|
_contentHeight = @(0);
|
|
_headerHeight = @(0);
|
|
_grabber = YES;
|
|
_draggable = YES;
|
|
_dimmed = YES;
|
|
_dimmedDetentIndex = @(0);
|
|
_pageSizing = YES;
|
|
_lastPosition = 0;
|
|
_isDragging = NO;
|
|
_isPresented = NO;
|
|
_pendingContentSizeChange = NO;
|
|
_activeDetentIndex = -1;
|
|
_pendingDetentIndex = -1;
|
|
|
|
_isTransitioning = NO;
|
|
_transitionFakeView = [UIView new];
|
|
_isTrackingPositionFromLayout = NO;
|
|
|
|
_blurInteraction = YES;
|
|
_insetAdjustment = @"automatic";
|
|
_detentCalculator = [[TrueSheetDetentCalculator alloc] init];
|
|
_detentCalculator.delegate = self;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
#pragma mark - Computed Properties
|
|
|
|
- (UISheetPresentationController *)sheet {
|
|
return self.sheetPresentationController;
|
|
}
|
|
|
|
- (BOOL)isTopmostPresentedController {
|
|
if (!self.isViewLoaded || self.view.window == nil) {
|
|
return NO;
|
|
}
|
|
return self.presentedViewController == nil;
|
|
}
|
|
|
|
- (UIView *)presentedView {
|
|
return self.sheet.presentedView;
|
|
}
|
|
|
|
- (CGFloat)currentPosition {
|
|
UIView *presentedView = self.presentedView;
|
|
return presentedView ? presentedView.frame.origin.y : 0.0;
|
|
}
|
|
|
|
- (CGFloat)screenHeight {
|
|
return UIScreen.mainScreen.bounds.size.height;
|
|
}
|
|
|
|
- (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
|
|
if ([_insetAdjustment isEqualToString:@"automatic"]) {
|
|
return 0;
|
|
}
|
|
|
|
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone) {
|
|
return 0;
|
|
}
|
|
|
|
// On iOS 26+, returns 0 for small detents (height <= 150)
|
|
// Floating sheets don't need adjustment
|
|
if (@available(iOS 26.0, *)) {
|
|
if (height <= 150) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
UIWindow *window = [WindowUtil keyWindow];
|
|
return window ? window.safeAreaInsets.bottom : 0;
|
|
}
|
|
|
|
- (BOOL)isDesignCompatibilityMode {
|
|
if (@available(iOS 26.0, *)) {
|
|
NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
|
|
return value.boolValue;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (NSInteger)currentDetentIndex {
|
|
UISheetPresentationController *sheet = self.sheet;
|
|
if (!sheet)
|
|
return -1;
|
|
|
|
UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
|
|
if (!selectedIdentifier)
|
|
return -1;
|
|
|
|
NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
|
|
for (NSInteger i = 0; i < detents.count; i++) {
|
|
if (@available(iOS 16.0, *)) {
|
|
if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
|
|
return i;
|
|
}
|
|
} else {
|
|
if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
|
|
return 0;
|
|
} else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
|
|
return detents.count - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
#pragma mark - View Lifecycle
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
|
|
|
|
_grabberView = [[TrueSheetGrabberView alloc] init];
|
|
_grabberView.hidden = YES;
|
|
[_grabberView addToView:self.view];
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
[super viewWillAppear:animated];
|
|
|
|
if (!_isPresented) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
});
|
|
|
|
UIViewController *presenter = self.presentingViewController;
|
|
if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
|
|
_parentSheetController = (TrueSheetViewController *)presenter;
|
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
|
|
[_parentSheetController.delegate viewControllerWillBlur];
|
|
}
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillPresentAtIndex:position:detent:)]) {
|
|
NSInteger index = self.currentDetentIndex;
|
|
CGFloat position = self.currentPosition;
|
|
CGFloat detent = [self detentValueForIndex:index];
|
|
|
|
[self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
|
|
[self.delegate viewControllerWillFocus];
|
|
}
|
|
});
|
|
}
|
|
|
|
[self setupTransitionTracker];
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated {
|
|
[super viewDidAppear:animated];
|
|
|
|
if (!_isPresented) {
|
|
if (_parentSheetController) {
|
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
|
|
[_parentSheetController.delegate viewControllerDidBlur];
|
|
}
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidPresentAtIndex:position:detent:)]) {
|
|
NSInteger index = [self currentDetentIndex];
|
|
CGFloat detent = [self detentValueForIndex:index];
|
|
[self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
|
|
[self.delegate viewControllerDidFocus];
|
|
}
|
|
|
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
|
|
});
|
|
|
|
[self setupGestureRecognizer];
|
|
_isPresented = YES;
|
|
}
|
|
}
|
|
|
|
- (BOOL)isDismissing {
|
|
return self.presentingViewController == nil || self.isBeingDismissed;
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
[super viewWillDisappear:animated];
|
|
|
|
if (self.isDismissing) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
|
|
[self.delegate viewControllerWillBlur];
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
|
|
[self.delegate viewControllerWillDismiss];
|
|
}
|
|
});
|
|
|
|
if (_parentSheetController) {
|
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
|
|
[_parentSheetController.delegate viewControllerWillFocus];
|
|
}
|
|
}
|
|
}
|
|
|
|
[self setupTransitionTracker];
|
|
}
|
|
|
|
- (void)viewDidDisappear:(BOOL)animated {
|
|
[super viewDidDisappear:animated];
|
|
|
|
if (self.isDismissing) {
|
|
_isPresented = NO;
|
|
_activeDetentIndex = -1;
|
|
|
|
if (_parentSheetController) {
|
|
if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
|
|
[_parentSheetController.delegate viewControllerDidFocus];
|
|
}
|
|
_parentSheetController = nil;
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
|
|
[self.delegate viewControllerDidBlur];
|
|
}
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
|
|
[self.delegate viewControllerDidDismiss];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)viewWillLayoutSubviews {
|
|
[super viewWillLayoutSubviews];
|
|
|
|
if (!_isTransitioning) {
|
|
_isTrackingPositionFromLayout = YES;
|
|
|
|
UIViewController *presented = self.presentedViewController;
|
|
BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
|
|
BOOL realtime = !hasPresentedController;
|
|
|
|
if (_pendingContentSizeChange || _pendingDetentsChange) {
|
|
_pendingContentSizeChange = NO;
|
|
_pendingDetentsChange = NO;
|
|
realtime = NO;
|
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
}
|
|
|
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
|
|
}
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews {
|
|
[super viewDidLayoutSubviews];
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
|
|
[self.delegate viewControllerDidChangeSize:self.view.frame.size];
|
|
}
|
|
|
|
if (_pendingDetentIndex >= 0) {
|
|
NSInteger pendingIndex = _pendingDetentIndex;
|
|
_pendingDetentIndex = -1;
|
|
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
|
|
CGFloat detent = [self detentValueForIndex:pendingIndex];
|
|
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
|
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
|
|
}
|
|
});
|
|
}
|
|
|
|
_isTrackingPositionFromLayout = NO;
|
|
}
|
|
|
|
#pragma mark - Position & Gesture Handling
|
|
|
|
- (TrueSheetContentView *)findContentView:(UIView *)view {
|
|
if ([view isKindOfClass:[TrueSheetContentView class]]) {
|
|
return (TrueSheetContentView *)view;
|
|
}
|
|
|
|
for (UIView *subview in view.subviews) {
|
|
TrueSheetContentView *found = [self findContentView:subview];
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)setupGestureRecognizer {
|
|
UIView *presentedView = self.presentedView;
|
|
if (!presentedView)
|
|
return;
|
|
|
|
if (!self.draggable) {
|
|
[GestureUtil setPanGesturesEnabled:NO forView:presentedView];
|
|
return;
|
|
}
|
|
|
|
[GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
|
|
|
|
TrueSheetContentView *contentView = [self findContentView:presentedView];
|
|
if (contentView) {
|
|
RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
|
|
if (scrollViewComponent && scrollViewComponent.scrollView) {
|
|
[GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
|
|
target:self
|
|
selector:@selector(handlePanGesture:)];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setupDraggable {
|
|
UIView *presentedView = self.presentedView;
|
|
if (!presentedView)
|
|
return;
|
|
|
|
[GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
|
|
}
|
|
|
|
- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
|
|
NSInteger index = self.currentDetentIndex;
|
|
CGFloat detent = [self detentValueForIndex:index];
|
|
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:detent:)]) {
|
|
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
|
|
}
|
|
|
|
switch (gesture.state) {
|
|
case UIGestureRecognizerStateBegan:
|
|
_isDragging = YES;
|
|
break;
|
|
case UIGestureRecognizerStateChanged:
|
|
if (!_isTrackingPositionFromLayout) {
|
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
|
|
}
|
|
break;
|
|
case UIGestureRecognizerStateEnded:
|
|
case UIGestureRecognizerStateCancelled: {
|
|
if (!_isTransitioning) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
|
|
});
|
|
}
|
|
|
|
_isDragging = NO;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)setupTransitionTracker {
|
|
if (!self.transitionCoordinator)
|
|
return;
|
|
|
|
_isTransitioning = YES;
|
|
|
|
CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
|
|
CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
|
|
|
|
_transitionFakeView.frame = self.isDismissing ? presentedFrame : dismissedFrame;
|
|
[self storeResolvedPositionForIndex:self.currentDetentIndex];
|
|
|
|
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
[[context containerView] addSubview:self->_transitionFakeView];
|
|
self->_transitionFakeView.frame = self.isDismissing ? dismissedFrame : presentedFrame;
|
|
|
|
self->_transitioningTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTransitionTracker)];
|
|
[self->_transitioningTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
|
|
};
|
|
|
|
[self.transitionCoordinator
|
|
animateAlongsideTransition:animation
|
|
completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
|
[self->_transitioningTimer setPaused:YES];
|
|
[self->_transitioningTimer invalidate];
|
|
[self->_transitionFakeView removeFromSuperview];
|
|
self->_isTransitioning = NO;
|
|
}];
|
|
}
|
|
|
|
- (void)handleTransitionTracker {
|
|
if (!_isDragging && _transitionFakeView.layer) {
|
|
CALayer *layer = _transitionFakeView.layer;
|
|
CGFloat layerPosition = layer.presentationLayer.frame.origin.y;
|
|
|
|
if (self.currentPosition >= self.screenHeight) {
|
|
CGFloat position = fmax(_lastPosition, layerPosition);
|
|
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition out"];
|
|
} else {
|
|
CGFloat position = fmax(self.currentPosition, layerPosition);
|
|
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition in"];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
|
|
UIViewController *presented = self.presentedViewController;
|
|
if (presented) {
|
|
UIModalPresentationStyle style = presented.modalPresentationStyle;
|
|
if (style == UIModalPresentationFullScreen || style == UIModalPresentationOverFullScreen ||
|
|
style == UIModalPresentationCurrentContext || style == UIModalPresentationOverCurrentContext) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (fabs(_lastPosition - position) > 0.01) {
|
|
_lastPosition = position;
|
|
|
|
CGFloat index = [self interpolatedIndexForPosition:position];
|
|
CGFloat detent = [self interpolatedDetentForPosition:position];
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
|
|
[self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)storeResolvedPositionForIndex:(NSInteger)index {
|
|
[_detentCalculator storeResolvedPositionForIndex:index];
|
|
}
|
|
|
|
- (CGFloat)estimatedPositionForIndex:(NSInteger)index {
|
|
return [_detentCalculator estimatedPositionForIndex:index];
|
|
}
|
|
|
|
- (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
|
|
return [_detentCalculator findSegmentForPosition:position outIndex:outIndex outProgress:outProgress];
|
|
}
|
|
|
|
- (CGFloat)interpolatedIndexForPosition:(CGFloat)position {
|
|
return [_detentCalculator interpolatedIndexForPosition:position];
|
|
}
|
|
|
|
- (CGFloat)interpolatedDetentForPosition:(CGFloat)position {
|
|
return [_detentCalculator interpolatedDetentForPosition:position];
|
|
}
|
|
|
|
- (CGFloat)detentValueForIndex:(NSInteger)index {
|
|
return [_detentCalculator detentValueForIndex:index];
|
|
}
|
|
|
|
#pragma mark - Sheet Configuration
|
|
|
|
- (void)setupSheetDetentsForSizeChange {
|
|
[self.sheet animateChanges:^{
|
|
_pendingContentSizeChange = YES;
|
|
[self setupSheetDetents];
|
|
}];
|
|
}
|
|
|
|
- (void)setupSheetDetentsForDetentsChange {
|
|
_pendingDetentsChange = YES;
|
|
[self setupSheetDetents];
|
|
}
|
|
|
|
- (void)setupSheetDetents {
|
|
UISheetPresentationController *sheet = self.sheet;
|
|
if (!sheet) {
|
|
RCTLogError(@"TrueSheet: sheetPresentationController is nil in setupSheetDetents");
|
|
return;
|
|
}
|
|
|
|
NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
|
|
[_detentCalculator clearResolvedPositions];
|
|
|
|
CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
|
|
|
|
for (NSInteger index = 0; index < self.detents.count; index++) {
|
|
id detent = self.detents[index];
|
|
UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
|
|
withAutoHeight:autoHeight
|
|
atIndex:index];
|
|
[detents addObject:sheetDetent];
|
|
}
|
|
|
|
[_detentCalculator setDetentCount:self.detents.count];
|
|
sheet.detents = detents;
|
|
|
|
if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
|
|
sheet.largestUndimmedDetentIdentifier = nil;
|
|
} else {
|
|
sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
|
|
if (@available(iOS 16.0, *)) {
|
|
if (self.dimmed && self.dimmedDetentIndex) {
|
|
NSInteger dimmedIdx = [self.dimmedDetentIndex integerValue];
|
|
if (dimmedIdx > 0 && dimmedIdx - 1 < sheet.detents.count) {
|
|
sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIdx - 1].identifier;
|
|
} else if (sheet.detents.lastObject) {
|
|
sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
|
|
}
|
|
} else if (sheet.detents.lastObject) {
|
|
sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (UISheetPresentationControllerDetent *)detentForValue:(id)detent
|
|
withAutoHeight:(CGFloat)autoHeight
|
|
atIndex:(NSInteger)index {
|
|
if (![detent isKindOfClass:[NSNumber class]]) {
|
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
}
|
|
|
|
CGFloat value = [detent doubleValue];
|
|
|
|
if (value == -1) {
|
|
if (@available(iOS 16.0, *)) {
|
|
return [self customDetentWithIdentifier:@"custom-auto" height:autoHeight];
|
|
} else {
|
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
}
|
|
}
|
|
|
|
if (value <= 0 || value > 1) {
|
|
RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
|
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
}
|
|
|
|
if (@available(iOS 16.0, *)) {
|
|
NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
|
|
CGFloat sheetHeight = value * self.screenHeight;
|
|
return [self customDetentWithIdentifier:detentId height:sheetHeight];
|
|
} else if (value >= 0.5) {
|
|
return [UISheetPresentationControllerDetent largeDetent];
|
|
} else {
|
|
return [UISheetPresentationControllerDetent mediumDetent];
|
|
}
|
|
}
|
|
|
|
- (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
|
|
height:(CGFloat)height API_AVAILABLE(ios(16.0)) {
|
|
CGFloat bottomAdjustment = [self detentBottomAdjustmentForHeight:height];
|
|
return [UISheetPresentationControllerDetent
|
|
customDetentWithIdentifier:identifier
|
|
resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
|
|
CGFloat maxDetentValue = context.maximumDetentValue;
|
|
CGFloat maxValue =
|
|
self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
|
|
CGFloat adjustedHeight = height - bottomAdjustment;
|
|
return fmin(adjustedHeight, maxValue);
|
|
}];
|
|
}
|
|
|
|
- (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
|
|
UISheetPresentationController *sheet = self.sheet;
|
|
if (!sheet) {
|
|
RCTLogError(@"TrueSheet: sheetPresentationController is nil in detentIdentifierForIndex");
|
|
return UISheetPresentationControllerDetentIdentifierMedium;
|
|
}
|
|
|
|
UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
|
|
if (index >= 0 && index < (NSInteger)sheet.detents.count) {
|
|
UISheetPresentationControllerDetent *detent = sheet.detents[index];
|
|
if (@available(iOS 16.0, *)) {
|
|
identifier = detent.identifier;
|
|
} else {
|
|
if (detent == [UISheetPresentationControllerDetent largeDetent]) {
|
|
identifier = UISheetPresentationControllerDetentIdentifierLarge;
|
|
}
|
|
}
|
|
}
|
|
|
|
return identifier;
|
|
}
|
|
|
|
- (void)applyActiveDetent {
|
|
if (!self.sheet) {
|
|
RCTLogError(@"TrueSheet: sheetPresentationController is nil in applyActiveDetent");
|
|
return;
|
|
}
|
|
|
|
NSInteger detentCount = _detents.count;
|
|
if (detentCount == 0)
|
|
return;
|
|
|
|
NSInteger clampedIndex = _activeDetentIndex;
|
|
if (clampedIndex < 0) {
|
|
clampedIndex = 0;
|
|
} else if (clampedIndex >= detentCount) {
|
|
clampedIndex = detentCount - 1;
|
|
}
|
|
|
|
if (clampedIndex != _activeDetentIndex) {
|
|
_activeDetentIndex = clampedIndex;
|
|
}
|
|
|
|
UISheetPresentationControllerDetentIdentifier identifier = [self detentIdentifierForIndex:clampedIndex];
|
|
if (identifier) {
|
|
self.sheet.selectedDetentIdentifier = identifier;
|
|
}
|
|
}
|
|
|
|
- (void)setupActiveDetentWithIndex:(NSInteger)index {
|
|
_activeDetentIndex = index;
|
|
[self applyActiveDetent];
|
|
}
|
|
|
|
- (void)resizeToDetentIndex:(NSInteger)index {
|
|
if (index == _activeDetentIndex) {
|
|
return;
|
|
}
|
|
|
|
_pendingDetentIndex = index;
|
|
_activeDetentIndex = index;
|
|
[self applyActiveDetent];
|
|
}
|
|
|
|
- (void)setupBackground {
|
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1)
|
|
BOOL useBackgroundEffect = NO;
|
|
if (@available(iOS 26.1, *)) {
|
|
useBackgroundEffect = !self.isDesignCompatibilityMode;
|
|
}
|
|
|
|
// iOS 26.1+: use native backgroundEffect when only backgroundBlur is set (no backgroundColor)
|
|
// Fall back to TrueSheetBlurView when blur intensity is set (not 100%) since
|
|
// sheet.backgroundEffect doesn't support intensity control
|
|
if (@available(iOS 26.1, *)) {
|
|
if (useBackgroundEffect) {
|
|
BOOL hasCustomIntensity = self.blurIntensity && [self.blurIntensity floatValue] < 100;
|
|
if (!self.backgroundColor && self.backgroundBlur && self.backgroundBlur.length > 0) {
|
|
if (hasCustomIntensity) {
|
|
// Clear native effect to allow custom blur view with intensity
|
|
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
|
|
} else {
|
|
UIBlurEffectStyle style = [BlurUtil blurEffectStyleFromString:self.backgroundBlur];
|
|
self.sheet.backgroundEffect = [UIBlurEffect effectWithStyle:style];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
NSString *effectiveBackgroundBlur = self.backgroundBlur;
|
|
if (@available(iOS 26.0, *)) {
|
|
// iOS 26+ has default liquid glass effect
|
|
} else if ((!effectiveBackgroundBlur || effectiveBackgroundBlur.length == 0) && !self.backgroundColor) {
|
|
effectiveBackgroundBlur = @"system-material";
|
|
}
|
|
|
|
BOOL blurChanged = ![_blurView.backgroundBlur isEqualToString:effectiveBackgroundBlur];
|
|
|
|
if (_blurView && blurChanged) {
|
|
[_blurView removeFromSuperview];
|
|
_blurView = nil;
|
|
}
|
|
|
|
if (effectiveBackgroundBlur && effectiveBackgroundBlur.length > 0) {
|
|
if (!_blurView) {
|
|
_blurView = [[TrueSheetBlurView alloc] init];
|
|
[_blurView addToView:self.view];
|
|
}
|
|
_blurView.backgroundBlur = effectiveBackgroundBlur;
|
|
_blurView.blurIntensity = self.blurIntensity;
|
|
_blurView.blurInteraction = self.blurInteraction;
|
|
[_blurView applyBlurEffect];
|
|
}
|
|
|
|
#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1)
|
|
if (@available(iOS 26.1, *)) {
|
|
if (useBackgroundEffect && self.backgroundColor) {
|
|
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:self.backgroundColor];
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
self.view.backgroundColor = self.backgroundColor;
|
|
}
|
|
|
|
- (void)setupGrabber {
|
|
BOOL showGrabber = self.grabber && self.draggable;
|
|
|
|
if (self.grabberOptions) {
|
|
self.sheet.prefersGrabberVisible = NO;
|
|
|
|
NSDictionary *options = self.grabberOptions;
|
|
_grabberView.grabberWidth = options[@"width"];
|
|
_grabberView.grabberHeight = options[@"height"];
|
|
_grabberView.topMargin = options[@"topMargin"];
|
|
_grabberView.cornerRadius = options[@"cornerRadius"];
|
|
_grabberView.color = options[@"color"];
|
|
_grabberView.adaptive = options[@"adaptive"];
|
|
[_grabberView applyConfiguration];
|
|
_grabberView.hidden = !showGrabber;
|
|
|
|
[self.view bringSubviewToFront:_grabberView];
|
|
} else {
|
|
self.sheet.prefersGrabberVisible = showGrabber;
|
|
_grabberView.hidden = YES;
|
|
}
|
|
}
|
|
|
|
- (void)setupSheetProps {
|
|
UISheetPresentationController *sheet = self.sheet;
|
|
if (!sheet) {
|
|
RCTLogWarn(
|
|
@"TrueSheet: No sheet presentation controller available. Ensure the view controller is presented modally.");
|
|
return;
|
|
}
|
|
|
|
sheet.delegate = self;
|
|
|
|
if (@available(iOS 17.0, *)) {
|
|
sheet.prefersPageSizing = self.pageSizing;
|
|
}
|
|
|
|
sheet.prefersEdgeAttachedInCompactHeight = YES;
|
|
sheet.prefersScrollingExpandsWhenScrolledToEdge = self.draggable;
|
|
|
|
if (self.cornerRadius) {
|
|
sheet.preferredCornerRadius = [self.cornerRadius floatValue];
|
|
} else {
|
|
sheet.preferredCornerRadius = UISheetPresentationControllerAutomaticDimension;
|
|
}
|
|
|
|
[self setupBackground];
|
|
[self setupGrabber];
|
|
}
|
|
|
|
#pragma mark - UISheetPresentationControllerDelegate
|
|
|
|
- (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
|
|
(UISheetPresentationController *)sheetPresentationController {
|
|
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSInteger index = self.currentDetentIndex;
|
|
if (index >= 0) {
|
|
CGFloat detent = [self detentValueForIndex:index];
|
|
[self.delegate viewControllerDidChangeDetent:index position:self.currentPosition detent:detent];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#pragma mark - RNSDismissibleModalProtocol
|
|
|
|
#if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
|
|
- (BOOL)isDismissible {
|
|
return NO;
|
|
}
|
|
|
|
- (UIViewController *)newPresentingViewController {
|
|
UIViewController *topmost = self;
|
|
while (topmost.presentedViewController != nil && !topmost.presentedViewController.isBeingDismissed &&
|
|
[topmost.presentedViewController isKindOfClass:[TrueSheetViewController class]]) {
|
|
topmost = topmost.presentedViewController;
|
|
}
|
|
return topmost;
|
|
}
|
|
#endif
|
|
|
|
@end
|