Skip to content

Commit

Permalink
feat(iOS): implement automicallyAdjustsKeyboardInsets for new arch (#…
Browse files Browse the repository at this point in the history
…45819)

Summary:
This PR implements the missing `automicallyAdjustsKeyboardInsets` for new architecture.

After fixing this I've noticed there is an open issue (#45647) with somebody assigned (sorry shubhamguptadream11 for taking your task)

Here is the app running on new arch:

https://github.com/user-attachments/assets/673f0587-6a67-47e3-8050-d6ee33a45724

## Changelog:

[IOS] [FIXED] - implement automicallyAdjustsKeyboardInsets for new arch

Pull Request resolved: #45819

Test Plan:
1. Test out ScrollViewKeyboardInsets example
2. See if it works the same with old and new arch

Reviewed By: sammy-SC

Differential Revision: D60453404

Pulled By: cipolleschi

fbshipit-source-id: bd7ce5bac8facffc527106b50c54112acf687bc3
  • Loading branch information
okwasniewski authored and facebook-github-bot committed Aug 6, 2024
1 parent 3572ef3 commit 21bdce7
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, strong, readonly) UIScrollView *scrollView;

/** Focus area of newly-activated text input relative to the window to compare against UIKeyboardFrameBegin/End */
@property (nonatomic, assign) CGRect firstResponderFocus;

/*
* Returns the subview of the scroll view that the component uses to mount all subcomponents into. That's useful to
* separate component views from auxiliary views to be able to reliably implement pull-to-refresh- and RTL-related
Expand All @@ -59,4 +62,10 @@ NS_ASSUME_NONNULL_BEGIN

@end

@interface UIView (RCTScrollViewComponentView)

- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ @implementation RCTScrollViewComponentView {
// some other part of the system scrolls scroll view.
BOOL _isUserTriggeredScrolling;
BOOL _shouldUpdateContentInsetAdjustmentBehavior;
BOOL _automaticallyAdjustKeyboardInsets;
BOOL _inverted;

CGPoint _contentOffsetWhenClipped;

Expand All @@ -120,6 +122,7 @@ + (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIV
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self _registerKeyboardListener];
_props = ScrollViewShadowNode::defaultSharedProps();
_scrollView = [[RCTEnhancedScrollView alloc] initWithFrame:self.bounds];
_scrollView.clipsToBounds = _props->getClipsContentToBounds();
Expand All @@ -128,6 +131,8 @@ - (instancetype)initWithFrame:(CGRect)frame
((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self;
_isUserTriggeredScrolling = NO;
_shouldUpdateContentInsetAdjustmentBehavior = YES;
_automaticallyAdjustKeyboardInsets = YES;
_inverted = NO;
[self addSubview:_scrollView];

_containerView = [[UIView alloc] initWithFrame:CGRectZero];
Expand All @@ -149,6 +154,103 @@ - (void)dealloc
[self.scrollViewDelegateSplitter removeAllDelegates];
}

- (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 (!_automaticallyAdjustKeyboardInsets) {
return;
}
BOOL isHorizontal = _scrollView.contentSize.width > self.frame.size.width;
if (isHorizontal) {
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];

CGPoint absoluteViewOrigin = [self convertPoint:self.bounds.origin toView:nil];
CGFloat scrollViewLowerY = _inverted ? absoluteViewOrigin.y : absoluteViewOrigin.y + self.bounds.size.height;

UIEdgeInsets newEdgeInsets = _scrollView.contentInset;
CGFloat inset = MAX(scrollViewLowerY - endFrame.origin.y, 0);
if (_inverted) {
newEdgeInsets.top = MAX(inset, _scrollView.contentInset.top);
} else {
newEdgeInsets.bottom = MAX(inset, _scrollView.contentInset.bottom);
}

CGPoint newContentOffset = _scrollView.contentOffset;
self.firstResponderFocus = CGRectNull;

CGFloat contentDiff = 0;
if ([[UIApplication sharedApplication] sendAction:@selector(reactUpdateResponderOffsetForScrollView:)
to:nil
from:self
forEvent:nil]) {
// Inner text field focusedS
CGFloat focusEnd = CGRectGetMaxY(self.firstResponderFocus);
BOOL didFocusExternalTextField = focusEnd == INFINITY;
if (!didFocusExternalTextField && focusEnd > endFrame.origin.y) {
// Text field active region is below visible area with keyboard - update diff to bring into view
contentDiff = endFrame.origin.y - focusEnd;
}
} else if (endFrame.origin.y <= beginFrame.origin.y) {
// Keyboard opened for other reason
contentDiff = endFrame.origin.y - beginFrame.origin.y;
}
if (_inverted) {
newContentOffset.y += contentDiff;
} else {
newContentOffset.y -= contentDiff;
}

if (@available(iOS 14.0, *)) {
// On iOS when Prefer Cross-Fade Transitions is enabled, the keyboard position
// & height is reported differently (0 instead of Y position value matching height of frame)
// Fixes similar issue we saw with https://github.com/facebook/react-native/pull/34503
if (UIAccessibilityPrefersCrossFadeTransitions() && endFrame.size.height == 0) {
newContentOffset.y = 0;
newEdgeInsets.bottom = 0;
}
}

[UIView animateWithDuration:duration
delay:0.0
options:animationOptionsWithCurve(curve)
animations:^{
self->_scrollView.contentInset = newEdgeInsets;
self->_scrollView.verticalScrollIndicatorInsets = newEdgeInsets;
[self scrollToOffset:newContentOffset animated:NO];
}
completion: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;
}

- (RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *)scrollViewDelegateSplitter
{
return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter;
Expand Down Expand Up @@ -225,6 +327,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
MAP_SCROLL_VIEW_PROP(showsHorizontalScrollIndicator);
MAP_SCROLL_VIEW_PROP(showsVerticalScrollIndicator);

if (oldScrollViewProps.isInvertedVirtualizedList != newScrollViewProps.isInvertedVirtualizedList) {
_inverted = newScrollViewProps.isInvertedVirtualizedList;
}

if (oldScrollViewProps.automaticallyAdjustKeyboardInsets != newScrollViewProps.automaticallyAdjustKeyboardInsets) {
_automaticallyAdjustKeyboardInsets = newScrollViewProps.automaticallyAdjustKeyboardInsets;
}

if (oldScrollViewProps.scrollIndicatorInsets != newScrollViewProps.scrollIndicatorInsets) {
_scrollView.scrollIndicatorInsets = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.scrollIndicatorInsets);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>

#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTScrollViewComponentView.h>
#import <React/RCTUITextField.h>
#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
Expand All @@ -22,6 +23,9 @@

#import "RCTFabricComponentsPlugins.h"

/** Native iOS text field bottom keyboard offset amount */
static const CGFloat kSingleLineKeyboardBottomOffset = 15.0;

using namespace facebook::react;

@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate, RCTTextInputViewProtocol>
Expand Down Expand Up @@ -96,6 +100,30 @@ - (void)didMoveToWindow
[self _restoreTextSelection];
}

- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView
{
if (![self isDescendantOfView:scrollView.scrollView]) {
// View is outside scroll view
return;
}

UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
UITextSelectionRect *selection = [_backedTextInputView selectionRectsForRange:selectedTextRange].firstObject;
CGRect focusRect;
if (selection == nil) {
// No active selection or caret - fallback to entire input frame
focusRect = self.bounds;
} else {
// Focus on text selection frame
focusRect = selection.rect;
BOOL isMultiline = [_backedTextInputView isKindOfClass:[UITextView class]];
if (!isMultiline) {
focusRect.size.height += kSingleLineKeyboardBottomOffset;
}
}
scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil];
}

#pragma mark - RCTViewComponentView overrides

- (NSObject *)accessibilityElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ ScrollViewProps::ScrollViewProps(
"automaticallyAdjustsScrollIndicatorInsets",
sourceProps.automaticallyAdjustsScrollIndicatorInsets,
true)),
automaticallyAdjustKeyboardInsets(
CoreFeatures::enablePropIteratorSetter
? sourceProps.automaticallyAdjustKeyboardInsets
: convertRawProp(
context,
rawProps,
"automaticallyAdjustKeyboardInsets",
sourceProps.automaticallyAdjustKeyboardInsets,
true)),
decelerationRate(
CoreFeatures::enablePropIteratorSetter
? sourceProps.decelerationRate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ScrollViewProps final : public ViewProps {
bool centerContent{};
bool automaticallyAdjustContentInsets{};
bool automaticallyAdjustsScrollIndicatorInsets{true};
bool automaticallyAdjustKeyboardInsets{true};
Float decelerationRate{0.998f};
Float endDraggingSensitivityMultiplier{1};
bool enableSyncOnScroll{false};
Expand Down

0 comments on commit 21bdce7

Please sign in to comment.