From 2b24b655ae5904e0b407f4ab85e1400884760168 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 9 Jul 2020 13:51:45 +0200 Subject: [PATCH 01/17] Implement onRequestClose for iOS, fixes #29319 --- React/Views/RCTModalHostView.h | 5 +++-- React/Views/RCTModalHostView.m | 30 ++++++++++++++++++++++++++- React/Views/RCTModalHostViewManager.m | 3 --- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h index 4e61886dba54..bbfdc3c9c05d 100644 --- a/React/Views/RCTModalHostView.h +++ b/React/Views/RCTModalHostView.h @@ -17,7 +17,7 @@ @protocol RCTModalHostViewInteractor; -@interface RCTModalHostView : UIView +@interface RCTModalHostView : UIView @property (nonatomic, copy) NSString *animationType; @property (nonatomic, assign) UIModalPresentationStyle presentationStyle; @@ -32,8 +32,9 @@ @property (nonatomic, copy) NSArray *supportedOrientations; @property (nonatomic, copy) RCTDirectEventBlock onOrientationChange; -#if TARGET_OS_TV @property (nonatomic, copy) RCTDirectEventBlock onRequestClose; + +#if TARGET_OS_TV @property (nonatomic, strong) RCTTVRemoteHandler *tvRemoteHandler; #endif diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 6a1533010388..d7f716a61df5 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -41,6 +41,9 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge if ((self = [super initWithFrame:CGRectZero])) { _bridge = bridge; _modalViewController = [RCTModalHostViewController new]; + if (@available(iOS 13.0, *)) { + _modalViewController.presentationController.delegate = self; + } UIView *containerView = [UIView new]; containerView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; _modalViewController.view = containerView; @@ -62,6 +65,22 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge return self; } +- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection +{ + if (self.presentationStyle == UIModalPresentationFullScreen && self.isTransparent) { + return UIModalPresentationOverFullScreen; + } + return self.presentationStyle; +} + +- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller +{ + if (self.presentationStyle == UIModalPresentationFullScreen && self.isTransparent) { + return UIModalPresentationOverFullScreen; + } + return self.presentationStyle; +} + #if TARGET_OS_TV - (void)menuButtonPressed:(__unused UIGestureRecognizer *)gestureRecognizer { @@ -69,10 +88,12 @@ - (void)menuButtonPressed:(__unused UIGestureRecognizer *)gestureRecognizer _onRequestClose(nil); } } +#endif - (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose { _onRequestClose = onRequestClose; + #if TARGET_OS_TV if (_reactSubview) { if (_onRequestClose && _menuButtonGestureRecognizer) { [_reactSubview addGestureRecognizer:_menuButtonGestureRecognizer]; @@ -80,8 +101,8 @@ - (void)setOnRequestClose:(RCTDirectEventBlock)onRequestClose [_reactSubview removeGestureRecognizer:_menuButtonGestureRecognizer]; } } + #endif } -#endif - (void)notifyForBoundsChange:(CGRect)newBounds { @@ -155,6 +176,13 @@ - (void)didUpdateReactSubviews // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` } +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController +{ + if (_onRequestClose) { + _onRequestClose(nil); + } +} + - (void)dismissModalViewController { if (_isPresented) { diff --git a/React/Views/RCTModalHostViewManager.m b/React/Views/RCTModalHostViewManager.m index bafab9dff9f3..d56bb52693ea 100644 --- a/React/Views/RCTModalHostViewManager.m +++ b/React/Views/RCTModalHostViewManager.m @@ -116,9 +116,6 @@ - (void)invalidate RCT_EXPORT_VIEW_PROPERTY(identifier, NSNumber) RCT_EXPORT_VIEW_PROPERTY(supportedOrientations, NSArray) RCT_EXPORT_VIEW_PROPERTY(onOrientationChange, RCTDirectEventBlock) - -#if TARGET_OS_TV RCT_EXPORT_VIEW_PROPERTY(onRequestClose, RCTDirectEventBlock) -#endif @end From abcd899a98f402334827e2299d8a776655a6773e Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 9 Jul 2020 14:45:07 +0200 Subject: [PATCH 02/17] Set modalInPresentation if pageSheet or formSheet Sets the [isModalInPresentation](https://developer.apple.com/documentation/uikit/uiviewcontroller/3229894-ismodalinpresentation) property to `NO` if the Presentation Style is formSheet or pageSheet, otherwise `YES` --- React/Views/RCTModalHostView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index d7f716a61df5..ba86e01c43a8 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -215,6 +215,7 @@ - (void)didMoveToWindow if (self.presentationStyle != UIModalPresentationNone) { _modalViewController.modalPresentationStyle = self.presentationStyle; } + _modalViewController.modalInPresentation = self.presentationStyle != UIModalPresentationFormSheet && self.presentationStyle != UIModalPresentationPageSheet; [_delegate presentModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]]; _isPresented = YES; } From 51d9a17d77da11976fe63f9a8164ff0fc414efce Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:03:10 +0200 Subject: [PATCH 03/17] Implement `automaticallyAdjustKeyboardInsets` Controls whether the ScrollView should automatically adjust it's contentInset and scrollViewInsets when the Keyboard changes it's size. The default value is false. --- Libraries/Components/ScrollView/ScrollView.js | 6 ++ .../ScrollViewNativeComponentType.js | 1 + .../ScrollView/ScrollViewViewConfig.js | 1 + Libraries/polyfills/console.js | 16 +++--- React/Views/ScrollView/RCTScrollView.h | 1 + React/Views/ScrollView/RCTScrollView.m | 57 +++++++++++++++++++ React/Views/ScrollView/RCTScrollViewManager.m | 1 + 7 files changed, 75 insertions(+), 8 deletions(-) diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 907949b6c258..4e4eb7b0563e 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -94,6 +94,12 @@ type IOSProps = $ReadOnly<{| * @platform ios */ automaticallyAdjustContentInsets?: ?boolean, + /** + * Controls whether the ScrollView should automatically adjust it's contentInset + * and scrollViewInsets when the Keyboard changes it's size. The default value is false. + * @platform ios + */ + automaticallyAdjustKeyboardInsets?: ?boolean, /** * The amount by which the scroll view content is inset from the edges * of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`. diff --git a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index a6366e8b62b3..f6c485b5f591 100644 --- a/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -26,6 +26,7 @@ export type ScrollViewNativeProps = $ReadOnly<{ alwaysBounceHorizontal?: ?boolean, alwaysBounceVertical?: ?boolean, automaticallyAdjustContentInsets?: ?boolean, + automaticallyAdjustKeyboardInsets?: ?boolean, bounces?: ?boolean, bouncesZoom?: ?boolean, canCancelContentTouches?: ?boolean, diff --git a/Libraries/Components/ScrollView/ScrollViewViewConfig.js b/Libraries/Components/ScrollView/ScrollViewViewConfig.js index b2a3a9246b34..c22c97b80564 100644 --- a/Libraries/Components/ScrollView/ScrollViewViewConfig.js +++ b/Libraries/Components/ScrollView/ScrollViewViewConfig.js @@ -24,6 +24,7 @@ const ScrollViewViewConfig = { alwaysBounceHorizontal: true, alwaysBounceVertical: true, automaticallyAdjustContentInsets: true, + automaticallyAdjustKeyboardInsets: false, bounces: true, bouncesZoom: true, canCancelContentTouches: true, diff --git a/Libraries/polyfills/console.js b/Libraries/polyfills/console.js index f82fb4d05bc1..82d2a031efef 100644 --- a/Libraries/polyfills/console.js +++ b/Libraries/polyfills/console.js @@ -427,14 +427,14 @@ function getNativeLogFunction(level) { // (Note: Logic duplicated in ExceptionsManager.js.) logLevel = LOG_LEVELS.warn; } - if (global.__inspectorLog) { - global.__inspectorLog( - INSPECTOR_LEVELS[logLevel], - str, - [].slice.call(arguments), - INSPECTOR_FRAMES_TO_SKIP, - ); - } + // if (global.__inspectorLog) { + // global.__inspectorLog( + // INSPECTOR_LEVELS[logLevel], + // str, + // [].slice.call(arguments), + // INSPECTOR_FRAMES_TO_SKIP, + // ); + // } if (groupStack.length) { str = groupFormat('', str); } diff --git a/React/Views/ScrollView/RCTScrollView.h b/React/Views/ScrollView/RCTScrollView.h index d5ac000284e4..1a7850b22437 100644 --- a/React/Views/ScrollView/RCTScrollView.h +++ b/React/Views/ScrollView/RCTScrollView.h @@ -40,6 +40,7 @@ @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, assign) BOOL automaticallyAdjustKeyboardInsets; @property (nonatomic, assign) BOOL DEPRECATED_sendUpdatedChildFrames; @property (nonatomic, assign) NSTimeInterval scrollEventThrottle; @property (nonatomic, assign) BOOL centerContent; diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index acef7bc88538..87be89051a6b 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -274,11 +274,67 @@ @implementation RCTScrollView { NSHashTable *_scrollListeners; } +- (void)registerKeyboardListener +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; +} + +- (void)unregisterKeyboardListener +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIKeyboardWillChangeFrameNotification + object:nil]; +} + +- (void)keyboardWillChangeFrame:(NSNotification*)notification +{ + if (![self automaticallyAdjustKeyboardInsets]) { + return; + } + if ([self isHorizontal:_scrollView]) { + return; + } + + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; + CGRect location = [self convertRect:self.bounds toView:nil]; + CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; + + CGFloat keyboardOverlap = (location.origin.y + location.size.height) - (screenHeight - keyboardSize.height); + + if (keyboardOverlap <= 0) { + // Keyboard does not overlap the ScrollView. + return; + } + + UIEdgeInsets contentInsets = _scrollView.contentInset; + if (self.inverted) { + contentInsets.top = keyboardOverlap; + } else { + contentInsets.bottom = keyboardOverlap; + } + _scrollView.contentInset = contentInsets; + + UIEdgeInsets scrollIndicatorInsets = _scrollView.scrollIndicatorInsets; + if (self.inverted) { + scrollIndicatorInsets.top = keyboardOverlap; + } else { + scrollIndicatorInsets.bottom = keyboardOverlap; + } + _scrollView.scrollIndicatorInsets = scrollIndicatorInsets; + + CGFloat bottom = self.inverted ? -keyboardOverlap : (location.size.height + keyboardOverlap); + [_scrollView setContentOffset:CGPointMake(0, bottom) animated:YES]; +} + - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { RCTAssertParam(eventDispatcher); if ((self = [super initWithFrame:CGRectZero])) { + [self registerKeyboardListener]; _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; @@ -410,6 +466,7 @@ - (void)dealloc { _scrollView.delegate = nil; [_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self]; + [self unregisterKeyboardListener]; } - (void)layoutSubviews diff --git a/React/Views/ScrollView/RCTScrollViewManager.m b/React/Views/ScrollView/RCTScrollViewManager.m index a176d1068609..46c229c105e1 100644 --- a/React/Views/ScrollView/RCTScrollViewManager.m +++ b/React/Views/ScrollView/RCTScrollViewManager.m @@ -71,6 +71,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(centerContent, BOOL) RCT_EXPORT_VIEW_PROPERTY(maintainVisibleContentPosition, NSDictionary) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) +RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustKeyboardInsets, BOOL) RCT_EXPORT_VIEW_PROPERTY(decelerationRate, CGFloat) RCT_EXPORT_VIEW_PROPERTY(directionalLockEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(indicatorStyle, UIScrollViewIndicatorStyle) From b9b7d422cfd641c9b2aa169c75831ee75a1de182 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 9 Jul 2020 14:45:07 +0200 Subject: [PATCH 04/17] Set modalInPresentation if pageSheet or formSheet Sets the [isModalInPresentation](https://developer.apple.com/documentation/uikit/uiviewcontroller/3229894-ismodalinpresentation) property to `NO` if the Presentation Style is formSheet or pageSheet, otherwise `YES` --- React/Views/RCTModalHostView.m | 1 + 1 file changed, 1 insertion(+) diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 0cfa69a99aab..6bcd7bb7f660 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -132,6 +132,7 @@ - (void)didMoveToWindow if (self.presentationStyle != UIModalPresentationNone) { _modalViewController.modalPresentationStyle = self.presentationStyle; } + _modalViewController.modalInPresentation = self.presentationStyle != UIModalPresentationFormSheet && self.presentationStyle != UIModalPresentationPageSheet; [_delegate presentModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]]; _isPresented = YES; } From 8d4d37851a916ab309303e79842158a976c9d4fb Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:14:54 +0200 Subject: [PATCH 05/17] Revert "Set modalInPresentation if pageSheet or formSheet" This reverts commit b9b7d422cfd641c9b2aa169c75831ee75a1de182. --- React/Views/RCTModalHostView.m | 1 - 1 file changed, 1 deletion(-) diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index 6bcd7bb7f660..0cfa69a99aab 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -132,7 +132,6 @@ - (void)didMoveToWindow if (self.presentationStyle != UIModalPresentationNone) { _modalViewController.modalPresentationStyle = self.presentationStyle; } - _modalViewController.modalInPresentation = self.presentationStyle != UIModalPresentationFormSheet && self.presentationStyle != UIModalPresentationPageSheet; [_delegate presentModalHostView:self withViewController:_modalViewController animated:[self hasAnimationType]]; _isPresented = YES; } From 9fe597e4dca8fb5ace8891bb5006d6553dd54688 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:16:30 +0200 Subject: [PATCH 06/17] Update console.js --- packages/polyfills/console.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index 82d2a031efef..f82fb4d05bc1 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -427,14 +427,14 @@ function getNativeLogFunction(level) { // (Note: Logic duplicated in ExceptionsManager.js.) logLevel = LOG_LEVELS.warn; } - // if (global.__inspectorLog) { - // global.__inspectorLog( - // INSPECTOR_LEVELS[logLevel], - // str, - // [].slice.call(arguments), - // INSPECTOR_FRAMES_TO_SKIP, - // ); - // } + if (global.__inspectorLog) { + global.__inspectorLog( + INSPECTOR_LEVELS[logLevel], + str, + [].slice.call(arguments), + INSPECTOR_FRAMES_TO_SKIP, + ); + } if (groupStack.length) { str = groupFormat('', str); } From 3bbabe558752b20970fe08b73908526becfaf9dd Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:16:49 +0200 Subject: [PATCH 07/17] Update RCTModalHostView.h --- React/Views/RCTModalHostView.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h index e78e02e53867..c54c1c69c944 100644 --- a/React/Views/RCTModalHostView.h +++ b/React/Views/RCTModalHostView.h @@ -16,7 +16,7 @@ @protocol RCTModalHostViewInteractor; -@interface RCTModalHostView : UIView +@interface RCTModalHostView : UIView @property (nonatomic, copy) NSString *animationType; @property (nonatomic, assign) UIModalPresentationStyle presentationStyle; From 20c11c25769deab10cd358f1b5bb4cea539d271f Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:18:09 +0200 Subject: [PATCH 08/17] Update RCTScrollView.m --- React/Views/ScrollView/RCTScrollView.m | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 77f1c62b5a1b..18197d02e35d 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -275,11 +275,75 @@ @implementation RCTScrollView { NSHashTable *_scrollListeners; } +- (void)registerKeyboardListener +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification + object:nil]; +} + +- (void)unregisterKeyboardListener +{ + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIKeyboardWillChangeFrameNotification + object:nil]; +} + +- (void)keyboardWillChangeFrame:(NSNotification*)notification +{ + if (![self automaticallyAdjustKeyboardInsets]) { + return; + } + if ([self isHorizontal:_scrollView]) { + return; + } + + CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; + CGRect location = [self convertRect:self.bounds toView:nil]; + CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; + + CGFloat keyboardOverlap = (location.origin.y + location.size.height) - (screenHeight - keyboardSize.height); + + if (keyboardOverlap <= 0) { + // Keyboard does not overlap the ScrollView. + return; + } + + UIEdgeInsets contentInsets = _scrollView.contentInset; + if (self.inverted) { + contentInsets.top = keyboardOverlap; + } else { + contentInsets.bottom = keyboardOverlap; + } + _scrollView.contentInset = contentInsets; + + UIEdgeInsets scrollIndicatorInsets = _scrollView.scrollIndicatorInsets; + if (self.inverted) { + scrollIndicatorInsets.top = keyboardOverlap; + } else { + scrollIndicatorInsets.bottom = keyboardOverlap; + } + _scrollView.scrollIndicatorInsets = scrollIndicatorInsets; + + CGFloat bottom = self.inverted ? -keyboardOverlap : (location.size.height + keyboardOverlap); + + + UIViewAnimationCurve curve = (UIViewAnimationCurve)[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; + double duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + [UIView animateWithDuration:duration delay:0 options:curve animations:^{ + [self->_scrollView setContentOffset:CGPointMake(0, bottom)]; + } completion:^(BOOL finished) { + // .. + }]; +} + - (instancetype)initWithEventDispatcher:(id)eventDispatcher { RCTAssertParam(eventDispatcher); if ((self = [super initWithFrame:CGRectZero])) { + [self registerKeyboardListener]; _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; @@ -404,6 +468,7 @@ - (void)dealloc { _scrollView.delegate = nil; [_eventDispatcher.bridge.uiManager.observerCoordinator removeObserver:self]; + [self unregisterKeyboardListener]; } - (void)layoutSubviews From 45a6c7e88dd1e4bffee44224ce279e68eb17d1de Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:23:08 +0200 Subject: [PATCH 09/17] Use Keyboard animation curve (`UIKeyboardAnimationCurveUserInfoKey`) --- React/Views/ScrollView/RCTScrollView.m | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 18197d02e35d..f0416c161fe5 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -331,11 +331,22 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification UIViewAnimationCurve curve = (UIViewAnimationCurve)[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; double duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - [UIView animateWithDuration:duration delay:0 options:curve animations:^{ - [self->_scrollView setContentOffset:CGPointMake(0, bottom)]; - } completion:^(BOOL finished) { - // .. - }]; + + UIViewAnimationOptions options; + switch (curve) { + case UIViewAnimationCurveEaseInOut: + options = UIViewAnimationOptionCurveEaseInOut; + case UIViewAnimationCurveEaseIn: + options = UIViewAnimationOptionCurveEaseIn; + case UIViewAnimationCurveEaseOut: + options = UIViewAnimationOptionCurveEaseOut; + case UIViewAnimationCurveLinear: + options =UIViewAnimationOptionCurveLinear; + } + + [UIView animateWithDuration:duration delay:0 options:options animations:^{ + [self->_scrollView setContentOffset:CGPointMake(0, bottom) animated:false]; + } completion:nil]; } - (instancetype)initWithEventDispatcher:(id)eventDispatcher From c66b4b6b01b882d05afe45c670e0378d593bf42b Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 01:33:23 +0200 Subject: [PATCH 10/17] Only scroll down if in `autoscrollThreshold` range --- React/Views/ScrollView/RCTScrollView.m | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index f0416c161fe5..c560d39dc7d1 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -326,27 +326,31 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification } _scrollView.scrollIndicatorInsets = scrollIndicatorInsets; - CGFloat bottom = self.inverted ? -keyboardOverlap : (location.size.height + keyboardOverlap); - UIViewAnimationCurve curve = (UIViewAnimationCurve)[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; - double duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + CGFloat bottom = self.inverted ? -keyboardOverlap : (location.size.height + keyboardOverlap); + NSNumber *autoscrollThreshold = self->_maintainVisibleContentPosition[@"autoscrollToTopThreshold"]; + BOOL shouldScrollToEnd = autoscrollThreshold == nil ? true : _scrollView.contentOffset.y <= [autoscrollThreshold intValue]; + if (shouldScrollToEnd) { + UIViewAnimationCurve curve = (UIViewAnimationCurve)[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; + double duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + UIViewAnimationOptions options; + switch (curve) { + case UIViewAnimationCurveEaseInOut: + options = UIViewAnimationOptionCurveEaseInOut; + case UIViewAnimationCurveEaseIn: + options = UIViewAnimationOptionCurveEaseIn; + case UIViewAnimationCurveEaseOut: + options = UIViewAnimationOptionCurveEaseOut; + case UIViewAnimationCurveLinear: + options =UIViewAnimationOptionCurveLinear; + } - UIViewAnimationOptions options; - switch (curve) { - case UIViewAnimationCurveEaseInOut: - options = UIViewAnimationOptionCurveEaseInOut; - case UIViewAnimationCurveEaseIn: - options = UIViewAnimationOptionCurveEaseIn; - case UIViewAnimationCurveEaseOut: - options = UIViewAnimationOptionCurveEaseOut; - case UIViewAnimationCurveLinear: - options =UIViewAnimationOptionCurveLinear; + [UIView animateWithDuration:duration delay:0 options:options animations:^{ + [self->_scrollView setContentOffset:CGPointMake(0, bottom) animated:false]; + } completion:nil]; } - - [UIView animateWithDuration:duration delay:0 options:options animations:^{ - [self->_scrollView setContentOffset:CGPointMake(0, bottom) animated:false]; - } completion:nil]; } - (instancetype)initWithEventDispatcher:(id)eventDispatcher From 4d7fc41b1a912dd248b5305fbde8899e1812a01a Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 13:22:48 +0200 Subject: [PATCH 11/17] Fix interactive dismissal --- React/Views/ScrollView/RCTScrollView.m | 72 ++++++++++---------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index c560d39dc7d1..d2a67263dc6e 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -290,6 +290,14 @@ - (void)unregisterKeyboardListener object:nil]; } +static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCurve curve) +{ + // UIViewAnimationCurve #7 is used for keyboard and therefore private - so we can't use switch/case here. + // source: https://stackoverflow.com/a/7327374/5281431 + RCTAssert(UIViewAnimationCurveLinear << 16 == UIViewAnimationOptionCurveLinear, @"Unexpected implementation of UIViewAnimationCurve"); + return curve << 16; +} + - (void)keyboardWillChangeFrame:(NSNotification*)notification { if (![self automaticallyAdjustKeyboardInsets]) { @@ -299,58 +307,34 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification return; } - CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; - CGRect location = [self convertRect:self.bounds toView:nil]; - CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; - - CGFloat keyboardOverlap = (location.origin.y + location.size.height) - (screenHeight - keyboardSize.height); - - if (keyboardOverlap <= 0) { - // Keyboard does not overlap the ScrollView. - return; - } + double duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + UIViewAnimationCurve curve = (UIViewAnimationCurve)[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; + CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - UIEdgeInsets contentInsets = _scrollView.contentInset; + UIEdgeInsets newEdgeInsets = _scrollView.contentInset; + CGFloat inset = endFrame.size.height; if (self.inverted) { - contentInsets.top = keyboardOverlap; + newEdgeInsets.top = inset; } else { - contentInsets.bottom = keyboardOverlap; + newEdgeInsets.bottom = inset; } - _scrollView.contentInset = contentInsets; - - UIEdgeInsets scrollIndicatorInsets = _scrollView.scrollIndicatorInsets; + CGPoint newContentOffset = _scrollView.contentOffset; + CGFloat contentDiff = endFrame.origin.y - beginFrame.origin.y; if (self.inverted) { - scrollIndicatorInsets.top = keyboardOverlap; + newContentOffset.y += contentDiff; } else { - scrollIndicatorInsets.bottom = keyboardOverlap; + newContentOffset.y -= contentDiff; } - _scrollView.scrollIndicatorInsets = scrollIndicatorInsets; - - - - CGFloat bottom = self.inverted ? -keyboardOverlap : (location.size.height + keyboardOverlap); - NSNumber *autoscrollThreshold = self->_maintainVisibleContentPosition[@"autoscrollToTopThreshold"]; - BOOL shouldScrollToEnd = autoscrollThreshold == nil ? true : _scrollView.contentOffset.y <= [autoscrollThreshold intValue]; - if (shouldScrollToEnd) { - UIViewAnimationCurve curve = (UIViewAnimationCurve)[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue]; - double duration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - - UIViewAnimationOptions options; - switch (curve) { - case UIViewAnimationCurveEaseInOut: - options = UIViewAnimationOptionCurveEaseInOut; - case UIViewAnimationCurveEaseIn: - options = UIViewAnimationOptionCurveEaseIn; - case UIViewAnimationCurveEaseOut: - options = UIViewAnimationOptionCurveEaseOut; - case UIViewAnimationCurveLinear: - options =UIViewAnimationOptionCurveLinear; - } - [UIView animateWithDuration:duration delay:0 options:options animations:^{ - [self->_scrollView setContentOffset:CGPointMake(0, bottom) animated:false]; - } completion:nil]; - } + [UIView animateWithDuration:duration + delay:0.0 + options:animationOptionsWithCurve(curve) + animations:^{ + self->_scrollView.contentInset = newEdgeInsets; + self->_scrollView.scrollIndicatorInsets = newEdgeInsets; + self->_scrollView.contentOffset = newContentOffset; + } completion:nil]; } - (instancetype)initWithEventDispatcher:(id)eventDispatcher From 5aec9b61663267767f8268d88e3cb0d15ab03ba2 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 22 Apr 2021 17:51:37 +0200 Subject: [PATCH 12/17] Make `automaticallyAdjustKeyboardInsets` compatible with `autoscrollToTopThreshold` --- React/Views/ScrollView/RCTScrollView.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index d2a67263dc6e..1e08cbf713bc 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -333,7 +333,7 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification animations:^{ self->_scrollView.contentInset = newEdgeInsets; self->_scrollView.scrollIndicatorInsets = newEdgeInsets; - self->_scrollView.contentOffset = newContentOffset; + [self scrollToOffset:newContentOffset animated:NO]; } completion:nil]; } @@ -912,8 +912,10 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager // Find the first entirely visible view. This must be done after we update the content offset // or it will tend to grab rows that were made visible by the shift in position UIView *subview = self->_contentView.subviews[ii]; + CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; + CGFloat y = self->_scrollView.contentOffset.y + bottomInset; if ((horz ? subview.frame.origin.x >= self->_scrollView.contentOffset.x - : subview.frame.origin.y >= self->_scrollView.contentOffset.y) || + : subview.frame.origin.y >= y) || ii == self->_contentView.subviews.count - 1) { self->_prevFirstVisibleFrame = subview.frame; self->_firstVisibleView = subview; @@ -943,12 +945,14 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager CGRect newFrame = self->_firstVisibleView.frame; CGFloat deltaY = newFrame.origin.y - self->_prevFirstVisibleFrame.origin.y; if (ABS(deltaY) > 0.1) { + CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; + CGFloat y = self->_scrollView.contentOffset.y + bottomInset; self->_scrollView.contentOffset = CGPointMake(self->_scrollView.contentOffset.x, self->_scrollView.contentOffset.y + deltaY); if (autoscrollThreshold != nil) { // If the offset WAS within the threshold of the start, animate to the start. - if (self->_scrollView.contentOffset.y - deltaY <= [autoscrollThreshold integerValue]) { - [self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, 0) animated:YES]; + if (y - deltaY <= [autoscrollThreshold integerValue]) { + [self scrollToOffset:CGPointMake(self->_scrollView.contentOffset.x, -bottomInset) animated:YES]; } } } From 1af2bfe67b2a7f374169b4fbff6f48b1f5a1a60e Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Apr 2021 10:13:34 +0200 Subject: [PATCH 13/17] Fix ScrollViewViewConfig Co-authored-by: Bartosz Kaszubowski --- Libraries/Components/ScrollView/ScrollViewViewConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Components/ScrollView/ScrollViewViewConfig.js b/Libraries/Components/ScrollView/ScrollViewViewConfig.js index 374e98beb08e..3c708adb1a84 100644 --- a/Libraries/Components/ScrollView/ScrollViewViewConfig.js +++ b/Libraries/Components/ScrollView/ScrollViewViewConfig.js @@ -24,7 +24,7 @@ const ScrollViewViewConfig = { alwaysBounceHorizontal: true, alwaysBounceVertical: true, automaticallyAdjustContentInsets: true, - automaticallyAdjustKeyboardInsets: false, + automaticallyAdjustKeyboardInsets: true, bounces: true, bouncesZoom: true, canCancelContentTouches: true, From 5868706ecdcb1e5ea3b44a4209b34a34529d7da4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Apr 2021 16:36:49 +0200 Subject: [PATCH 14/17] Account for ScrollView's absolute position on screen --- React/Views/ScrollView/RCTScrollView.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 1e08cbf713bc..a4a896417c76 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -312,13 +312,17 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification CGRect beginFrame = [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGPoint absoluteViewOrigin = [self convertPoint:self.bounds.origin toView:nil]; + CGFloat scrollViewLowerY = self.inverted ? absoluteViewOrigin.y : absoluteViewOrigin.y + self.bounds.size.height; + UIEdgeInsets newEdgeInsets = _scrollView.contentInset; - CGFloat inset = endFrame.size.height; + CGFloat inset = MAX(scrollViewLowerY - endFrame.origin.y, 0); if (self.inverted) { newEdgeInsets.top = inset; } else { newEdgeInsets.bottom = inset; } + CGPoint newContentOffset = _scrollView.contentOffset; CGFloat contentDiff = endFrame.origin.y - beginFrame.origin.y; if (self.inverted) { From d63bb69a9ae03fdcf13a4adc077991cf5ae2567f Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Apr 2021 17:45:23 +0200 Subject: [PATCH 15/17] Fix `autoscrollToTopThreshold` not working with `automaticallyAdjustKeyboardInsets` enabled --- React/Views/ScrollView/RCTScrollView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index a4a896417c76..70313c9e2d06 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -919,7 +919,7 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; CGFloat y = self->_scrollView.contentOffset.y + bottomInset; if ((horz ? subview.frame.origin.x >= self->_scrollView.contentOffset.x - : subview.frame.origin.y >= y) || + : subview.frame.origin.y > y) || ii == self->_contentView.subviews.count - 1) { self->_prevFirstVisibleFrame = subview.frame; self->_firstVisibleView = subview; From 4bc8ce8eac70e28c933dcf7f3d1ff76a89f6523e Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 23 Apr 2021 17:53:24 +0200 Subject: [PATCH 16/17] Also respect contentInsets for autoscroll --- React/Views/ScrollView/RCTScrollView.m | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 70313c9e2d06..28d573d898b2 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -908,6 +908,7 @@ - (void)setMaintainVisibleContentPosition:(NSDictionary *)maintainVisibleContent - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager { RCTAssertUIManagerQueue(); + [manager prependUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { BOOL horz = [self isHorizontal:self->_scrollView]; @@ -916,11 +917,17 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager // Find the first entirely visible view. This must be done after we update the content offset // or it will tend to grab rows that were made visible by the shift in position UIView *subview = self->_contentView.subviews[ii]; - CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; - CGFloat y = self->_scrollView.contentOffset.y + bottomInset; - if ((horz ? subview.frame.origin.x >= self->_scrollView.contentOffset.x - : subview.frame.origin.y > y) || - ii == self->_contentView.subviews.count - 1) { + BOOL hasNewView = NO; + if (horz) { + CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; + CGFloat x = self->_scrollView.contentOffset.x + leftInset; + hasNewView = subview.frame.origin.x > x; + } else { + CGFloat bottomInset = self.inverted ? self->_scrollView.contentInset.top : self->_scrollView.contentInset.bottom; + CGFloat y = self->_scrollView.contentOffset.y + bottomInset; + hasNewView = subview.frame.origin.y > y; + } + if (hasNewView || ii == self->_contentView.subviews.count - 1) { self->_prevFirstVisibleFrame = subview.frame; self->_firstVisibleView = subview; break; @@ -936,12 +943,14 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager if ([self isHorizontal:self->_scrollView]) { CGFloat deltaX = self->_firstVisibleView.frame.origin.x - self->_prevFirstVisibleFrame.origin.x; if (ABS(deltaX) > 0.1) { + CGFloat leftInset = self.inverted ? self->_scrollView.contentInset.right : self->_scrollView.contentInset.left; + CGFloat x = self->_scrollView.contentOffset.x + leftInset; self->_scrollView.contentOffset = CGPointMake(self->_scrollView.contentOffset.x + deltaX, self->_scrollView.contentOffset.y); if (autoscrollThreshold != nil) { // If the offset WAS within the threshold of the start, animate to the start. - if (self->_scrollView.contentOffset.x - deltaX <= [autoscrollThreshold integerValue]) { - [self scrollToOffset:CGPointMake(0, self->_scrollView.contentOffset.y) animated:YES]; + if (x - deltaX <= [autoscrollThreshold integerValue]) { + [self scrollToOffset:CGPointMake(-leftInset, self->_scrollView.contentOffset.y) animated:YES]; } } } From 59f531f9655fd33abb685aa2be6f05cb8c7ff68a Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 2 Sep 2021 10:53:27 +0200 Subject: [PATCH 17/17] Also respect `_contentInset` --- React/Views/ScrollView/RCTScrollView.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 28d573d898b2..d9821ef05fdf 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -318,9 +318,9 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification UIEdgeInsets newEdgeInsets = _scrollView.contentInset; CGFloat inset = MAX(scrollViewLowerY - endFrame.origin.y, 0); if (self.inverted) { - newEdgeInsets.top = inset; + newEdgeInsets.top = MAX(inset, _contentInset.top); } else { - newEdgeInsets.bottom = inset; + newEdgeInsets.bottom = MAX(inset, _contentInset.bottom); } CGPoint newContentOffset = _scrollView.contentOffset;