diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2cdb2cf1d..23391d999 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -214,6 +214,7 @@ typedef enum : NSUInteger { @property (nonatomic) BOOL isViewCompletelyAppeared; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL shouldObserveVMUpdates; +@property (nonatomic) BOOL shouldAnimateKeyboardChanges; @property (nonatomic) BOOL viewHasEverAppeared; @property (nonatomic) BOOL hasUnreadMessages; @property (nonatomic) BOOL isPickingMediaAsDocument; @@ -1230,6 +1231,7 @@ typedef enum : NSUInteger { self.isViewCompletelyAppeared = YES; self.viewHasEverAppeared = YES; + self.shouldAnimateKeyboardChanges = YES; // HACK: Because the inputToolbar is the inputAccessoryView, we make some special considertations WRT it's firstResponder status. // @@ -1291,6 +1293,7 @@ typedef enum : NSUInteger { [super viewDidDisappear:animated]; self.userHasScrolled = NO; self.isViewVisible = NO; + self.shouldAnimateKeyboardChanges = NO; [self.audioAttachmentPlayer stop]; self.audioAttachmentPlayer = nil; @@ -3720,12 +3723,21 @@ typedef enum : NSUInteger { return; } CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue]; + CGRect keyboardEndFrameConverted = [self.view convertRect:keyboardEndFrame fromView:nil]; UIEdgeInsets oldInsets = self.collectionView.contentInset; UIEdgeInsets newInsets = oldInsets; - // bottomLayoutGuide accounts for extra offset needed on iPhoneX - newInsets.bottom = keyboardEndFrame.size.height - self.bottomLayoutGuide.length; + // Use a content inset that so that the conversation content + // is not hidden behind the keyboard + input accessory. + // + // Make sure to leave space for the bottom layout guide (the notch). + // + // Always reserve room for the input accessory, which we display even + // if the keyboard is not active. + newInsets.bottom = MAX(0, + MAX(self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y, + self.inputToolbar.height)); BOOL wasScrolledToBottom = [self isScrolledToBottom]; @@ -3774,7 +3786,7 @@ typedef enum : NSUInteger { } }; - if (self.isViewCompletelyAppeared) { + if (self.shouldAnimateKeyboardChanges && CurrentAppContext().isAppForegroundAndActive) { adjustInsets(); } else { // Even though we are scrolling without explicitly animating, the notification seems to occur within the context diff --git a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift index a1594d3ec..6d59d0f9e 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -73,6 +73,12 @@ public class OnboardingBaseViewController: OWSViewController { // MARK: - View Lifecycle + public override func viewDidLoad() { + super.viewDidLoad() + + self.shouldBottomViewReserveSpaceForKeyboard = true + } + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/SignalMessaging/ViewControllers/OWSViewController.h b/SignalMessaging/ViewControllers/OWSViewController.h index 6e0ed2c3c..71cb28ad2 100644 --- a/SignalMessaging/ViewControllers/OWSViewController.h +++ b/SignalMessaging/ViewControllers/OWSViewController.h @@ -20,6 +20,10 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void); // BUT adjust its location upward if the keyboard appears. - (void)autoPinViewToBottomOfViewControllerOrKeyboard:(UIView *)view avoidNotch:(BOOL)avoidNotch; +// If YES, the bottom view never "reclaims" layout space if the keyboard is dismissed. +// Defaults to NO. +@property (nonatomic) BOOL shouldBottomViewReserveSpaceForKeyboard; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/ViewControllers/OWSViewController.m b/SignalMessaging/ViewControllers/OWSViewController.m index b4d8a06ec..53d87d88f 100644 --- a/SignalMessaging/ViewControllers/OWSViewController.m +++ b/SignalMessaging/ViewControllers/OWSViewController.m @@ -23,6 +23,7 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void) @property (nonatomic, weak) UIView *bottomLayoutView; @property (nonatomic) NSLayoutConstraint *bottomLayoutConstraint; +@property (nonatomic) BOOL shouldAnimateBottomLayout; @end @@ -64,6 +65,22 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void) return self; } +#pragma mark - View Lifecycle + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + self.shouldAnimateBottomLayout = YES; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + + self.shouldAnimateBottomLayout = NO; +} + - (void)viewDidLoad { [super viewDidLoad]; @@ -73,6 +90,8 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void) } } +#pragma mark - + - (void)autoPinViewToBottomOfViewControllerOrKeyboard:(UIView *)view avoidNotch:(BOOL)avoidNotch { OWSAssertDebug(view); @@ -185,11 +204,27 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void) // bar. CGFloat offset = -MAX(0, (self.view.height - self.bottomLayoutGuide.length - keyboardEndFrameConverted.origin.y)); - // There's no need to use: [UIView animateWithDuration:...]. - // Any layout changes made during these notifications are - // automatically animated. - self.bottomLayoutConstraint.constant = offset; - [self.bottomLayoutView.superview layoutIfNeeded]; + dispatch_block_t updateLayout = ^{ + if (self.shouldBottomViewReserveSpaceForKeyboard && offset >= 0) { + // To avoid unnecessary animations / layout jitter, + // some views never reclaim layout space when the keyboard is dismissed. + // + // They _do_ need to relayout if the user switches keyboards. + return; + } + self.bottomLayoutConstraint.constant = offset; + [self.bottomLayoutView.superview layoutIfNeeded]; + }; + + + if (self.shouldAnimateBottomLayout && CurrentAppContext().isAppForegroundAndActive) { + updateLayout(); + } else { + // UIKit by default animates all changes in response to keyboard events. + // We want to suppress those animations if the view isn't visible, + // otherwise presentation animations don't work properly. + [UIView performWithoutAnimation:updateLayout]; + } } #pragma mark - Orientation