I have a UIScrollView
which I've taught to automatically adjust contentInset
and scrollIndicatorInsets
to avoid the Keyboard.
I have achieved this by listening to the UIKeyboardWillChangeFrameNotification
notification, and automatically inset the UIScrollView
by the Keyboard Frame's height.
Here's the code:
- (void)registerKeyboardListener
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
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]) {
return;
}
if ([self isHorizontal:_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];
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 = 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) {
newContentOffset.y += contentDiff;
} else {
newContentOffset.y -= contentDiff;
}
[UIView animateWithDuration:duration
delay:0.0
options:animationOptionsWithCurve(curve)
animations:^{
self->_scrollView.contentInset = newEdgeInsets;
self->_scrollView.scrollIndicatorInsets = newEdgeInsets;
[self scrollToOffset:newContentOffset animated:NO];
} completion:nil];
}
Full source code: RCTScrollView.m
While this works perfectly fine when the Keyboard shows/hides, I'm experiencing a bit weird behaviour while navigating.
When the user navigates to a new page or pops the current page, my keyboardWillChangeFrame
notification is called with a Keyboard .origin.y
of exactly the screen height, in other words - the keyboard is fully dismissed.
This is a problem for two reasons:
- My
inputAccessoryView
also belongs to the Keyboard - The Keyboard (and the
inputAccessoryView
) don't actually dismiss, so thekeyboardWillChangeFrame
notification seems to be fired incorrectly.
This looks really weird as everything seems to move when you navigate to a new page or pop the current one, and it gets even worse when you try to interactively dismiss the page via the pop gesture.
Here's a Demo Video of how that looks:
Full-res 60FPS version: Here
What really baffles me about all this, is that the keyboardWillChangeFrame
function is only called once, and the UIScrollView
animates that change interactively. Surely there must be a property to disable this?
Does anyone know why that's happening and how I can avoid it?
Link to the GitHub PR/discussion with more context: facebook/react-native #31402