Better keyboard management.

- fixes problems on iOS11.2 where emoji keyboard sometimes obscures text
  input.
- better animation for interactive pan gesture when viewing message
  details
- more intuitive swipe-to-dismiss keyboard in conversation view
- converge on one mnethod for dismissing keyboard in conversation view

- [ ] Pop keyboard, then hit attachment, dismisses keyboard, which is
      fine, but the content should immediately scroll down with the
      keyboard, instead it stays up, and scrolls down only once the
      attachment action sheet has been dismissed.

// FREEBIE
This commit is contained in:
Michael Kirk 2018-01-02 15:55:40 -06:00
parent ced4e3b78c
commit 81268012e5
7 changed files with 228 additions and 72 deletions

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <PureLayout/PureLayout.h>
@ -83,6 +83,9 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
- (NSLayoutConstraint *)autoPinLeadingToSuperviewWithMargin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinTrailingToSuperview;
- (NSLayoutConstraint *)autoPinTrailingToSuperviewWithMargin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view;
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view margin:(CGFloat)margin;
- (NSLayoutConstraint *)autoPinTrailingToLeadingOfView:(UIView *)view;

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "UIView+OWS.h"
@ -299,6 +299,32 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
}
}
- (NSLayoutConstraint *)autoPinBottomToSuperviewWithMargin:(CGFloat)margin
{
if (@available(iOS 9.0, *)) {
NSLayoutConstraint *constraint =
[self.bottomAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.bottomAnchor
constant:-margin];
constraint.active = YES;
return constraint;
} else {
return [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:margin];
}
}
- (NSLayoutConstraint *)autoPinTopToSuperviewWithMargin:(CGFloat)margin
{
if (@available(iOS 9.0, *)) {
NSLayoutConstraint *constraint =
[self.topAnchor constraintEqualToAnchor:self.superview.layoutMarginsGuide.topAnchor
constant:margin];
constraint.active = YES;
return constraint;
} else {
return [self autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:margin];
}
}
- (NSLayoutConstraint *)autoPinLeadingToTrailingOfView:(UIView *)view
{
OWSAssert(view);

View File

@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)didApproveAttachment:(SignalAttachment *)attachment;
- (void)toolbarHeightDidChange:(CGFloat)newHeight;
@end
#pragma mark -

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationInputToolbar.h"
@ -15,7 +15,7 @@
NS_ASSUME_NONNULL_BEGIN
static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;
static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5;
@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>
@property (nonatomic, readonly) UIView *contentView;
@ -30,6 +30,8 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
@property (nonatomic) NSValue *lastTextContentSize;
@property (nonatomic) CGFloat toolbarHeight;
@property (nonatomic) CGFloat textViewHeight;
#pragma mark - Voice Memo Recording UI
@ -68,18 +70,34 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
[self removeKVOObservers];
}
- (CGSize)intrinsicContentSize
{
CGSize newSize = CGSizeMake(self.bounds.size.width, self.toolbarHeight + ConversationInputToolbarBorderViewHeight);
return newSize;
}
- (void)setToolbarHeight:(CGFloat)toolbarHeight
{
if (toolbarHeight == _toolbarHeight) {
return;
}
_toolbarHeight = toolbarHeight;
}
- (void)createContents
{
self.layoutMargins = UIEdgeInsetsZero;
self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor];
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
UIView *borderView = [UIView new];
borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
[self addSubview:borderView];
[borderView autoPinWidthToSuperview];
[borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[borderView autoSetDimension:ALDimensionHeight toSize:0.5f];
[borderView autoSetDimension:ALDimensionHeight toSize:ConversationInputToolbarBorderViewHeight];
_contentView = [UIView containerView];
[self addSubview:self.contentView];
@ -223,12 +241,16 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
+ self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
+ self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
const CGFloat kMaxTextViewHeight = 100.f;
// Exactly 4 lines of text with default sizing.
const CGFloat kMaxTextViewHeight = 98.f;
const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
+ self.inputTextView.contentInset.bottom);
const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;
self.textViewHeight = textViewHeight;
self.toolbarHeight = textViewHeight + textViewVInset * 2;
if (self.attachmentToApprove) {
OWSAssert(self.attachmentView);
@ -247,14 +269,14 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
self.contentContraints = @[
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
[self.attachmentView autoPinBottomToSuperviewWithMargin:textViewVInset],
[self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
[self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f],
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
@ -316,7 +338,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
self.contentContraints = @[
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
[self.leftButtonWrapper autoPinBottomToSuperviewWithMargin:0],
[leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
[leftButton autoPinLeadingToSuperviewWithMargin:contentHInset],
@ -325,18 +347,18 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
[self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
[self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
[self.inputTextView autoPinBottomToSuperviewWithMargin:textViewVInset],
[self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],
[self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],
[self.rightButtonWrapper autoPinBottomToSuperviewWithMargin:0],
[rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
[rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
[rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
[rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom]
];
// Layout immediately, unless the input toolbar hasn't even been laid out yet.
@ -709,6 +731,7 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f
|| fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
[self ensureContentConstraints];
[self invalidateIntrinsicContentSize];
}
}
}
@ -811,8 +834,6 @@ static void *kConversationInputTextViewObservingContext = &kConversationInputTex
- (void)viewWillDisappear:(BOOL)animated
{
[self.attachmentView viewWillDisappear:animated];
[self endEditingTextMessage];
}
- (nullable NSString *)textInputPrimaryLanguage

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewController.h"
@ -64,7 +64,6 @@
#import <JSQMessagesViewController/JSQSystemSoundPlayer+JSQMessages.h>
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
#import <JSQSystemSoundPlayer.h>
#import <MediaPlayer/MediaPlayer.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <SignalServiceKit/ContactsUpdater.h>
#import <SignalServiceKit/MimeTypeUtil.h>
@ -215,17 +214,18 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic, readonly) BOOL isGroupConversation;
@property (nonatomic) BOOL isUserScrolling;
@property (nonatomic) NSLayoutConstraint *scrollDownButtonButtomConstraint;
@property (nonatomic) ConversationScrollButton *scrollDownButton;
#ifdef DEBUG
@property (nonatomic) ConversationScrollButton *scrollUpButton;
#endif
@property (nonatomic) BOOL isViewCompletelyAppeared;
@property (nonatomic) BOOL isViewVisible;
@property (nonatomic) BOOL isAppInBackground;
@property (nonatomic) BOOL shouldObserveDBModifications;
@property (nonatomic) BOOL viewHasEverAppeared;
@property (nonatomic) BOOL wasScrolledToBottomBeforeKeyboardShow;
@property (nonatomic) BOOL wasScrolledToBottomBeforeLayoutChange;
@property (nonatomic) BOOL hasUnreadMessages;
@end
@ -319,6 +319,10 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
selector:@selector(signalAccountsDidChange:)
name:OWSContactsManagerSignalAccountsDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
}
- (void)signalAccountsDidChange:(NSNotification *)notification
@ -452,13 +456,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
{
if (_peek) {
self.inputToolbar.hidden = YES;
[self.inputToolbar endEditing:TRUE];
[self dismissKeyBoard];
return;
}
if (self.userLeftGroup) {
self.inputToolbar.hidden = YES; // user has requested they leave the group. further sends disallowed
[self.inputToolbar endEditing:TRUE];
[self dismissKeyBoard];
} else {
self.inputToolbar.hidden = NO;
}
@ -501,6 +505,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
self.collectionView.dataSource = self;
self.collectionView.showsVerticalScrollIndicator = YES;
self.collectionView.showsHorizontalScrollIndicator = NO;
self.collectionView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
self.collectionView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.collectionView];
[self.collectionView autoPinWidthToSuperview];
@ -517,10 +522,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
_inputToolbar = [ConversationInputToolbar new];
self.inputToolbar.inputToolbarDelegate = self;
self.inputToolbar.inputTextViewDelegate = self;
[self.view addSubview:self.inputToolbar];
[self.inputToolbar autoPinWidthToSuperview];
[self.inputToolbar autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.collectionView];
[self autoPinViewToBottomGuideOrKeyboard:self.inputToolbar];
[self.collectionView autoPinToBottomLayoutGuideOfViewController:self withInset:0];
self.loadMoreHeader = [UILabel new];
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES",
@ -534,6 +536,16 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self.loadMoreHeader autoSetDimension:ALDimensionHeight toSize:kLoadMoreHeaderHeight];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (nullable UIView *)inputAccessoryView
{
return self.inputToolbar;
}
- (void)registerCellClasses
{
[self.collectionView registerClass:[OWSSystemMessageCell class]
@ -883,6 +895,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}];
[actionSheetController addAction:dismissAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
}
@ -999,6 +1012,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
_callOnOpen = NO;
}
self.isViewCompletelyAppeared = YES;
self.viewHasEverAppeared = YES;
}
@ -1012,6 +1026,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[super viewWillDisappear:animated];
self.isViewCompletelyAppeared = NO;
[self.inputToolbar viewWillDisappear:animated];
}
@ -1029,7 +1044,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self markVisibleMessagesAsRead];
[self cancelVoiceMemo];
[self.cellMediaCache removeAllObjects];
[self.inputToolbar endEditingTextMessage];
self.isUserScrolling = NO;
}
@ -1388,11 +1402,13 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
return;
}
// FIXME inputAccessoryView - if using numeric keyboard, switch back to alpha after
// sending.
// The JSQ event listeners cause a bounce animation, so we temporarily disable them.
[self setShouldIgnoreKeyboardChanges:YES];
[self dismissKeyBoard];
[self popKeyBoard];
[self setShouldIgnoreKeyboardChanges:NO];
// [self setShouldIgnoreKeyboardChanges:YES];
// [self dismissKeyBoard];
// [self popKeyBoard];
// [self setShouldIgnoreKeyboardChanges:NO];
}
#pragma mark - Dynamic Text
@ -1635,6 +1651,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[actionSheetController addAction:resendMessageAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
@ -1669,6 +1686,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[actionSheetController addAction:resendMessageAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
@ -1796,6 +1814,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}];
[alertController addAction:resetSessionAction];
[self dismissKeyBoard];
[self presentViewController:alertController animated:YES completion:nil];
}
@ -1836,6 +1855,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}];
[actionSheetController addAction:acceptSafetyNumberAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
@ -1865,9 +1885,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[alertController addAction:callAction];
[alertController addAction:[OWSAlerts cancelAction]];
[[UIApplication sharedApplication].frontmostViewController presentViewController:alertController
animated:YES
completion:nil];
[self dismissKeyBoard];
[self presentViewController:alertController animated:YES completion:nil];
}
#pragma mark - ConversationViewCellDelegate
@ -1916,6 +1935,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}];
[actionSheetController addAction:blockAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
@ -2018,6 +2038,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// MPMoviePlayerController will animate a crop of its
// contents rather than scaling them.
_videoPlayer.view.frame = self.view.bounds;
// FIXME inputAccessoryView - we lose and regain first responder here, causing keyboard to appear above video
// Approaches:
// - put the video player in a separate VC (like the full image view controller)
// - some kind of "showing video" flag to supress first responder.
[_videoPlayer setFullscreen:YES animated:NO];
}
@ -2235,7 +2260,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self.view addSubview:self.scrollDownButton];
[self.scrollDownButton autoSetDimension:ALDimensionWidth toSize:ConversationScrollButton.buttonSize];
[self.scrollDownButton autoSetDimension:ALDimensionHeight toSize:ConversationScrollButton.buttonSize];
[self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:self.inputToolbar];
self.scrollDownButtonButtomConstraint =
[self.scrollDownButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.collectionView];
[self.scrollDownButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
#ifdef DEBUG
@ -2351,6 +2378,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[[UIDocumentMenuViewController alloc] initWithDocumentTypes:documentTypes inMode:pickerMode];
menuController.delegate = self;
[self dismissKeyBoard];
[self presentViewController:menuController animated:YES completion:nil];
}
@ -2362,6 +2390,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[[GifPickerViewController alloc] initWithThread:self.thread messageSender:self.messageSender];
view.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:view];
[self dismissKeyBoard];
[self presentViewController:navigationController animated:YES completion:nil];
}
@ -2401,6 +2431,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
// post iOS11, document picker has no blue header.
[UIUtil applyDefaultSystemAppearence];
}
[self dismissKeyBoard];
[self presentViewController:documentPicker animated:YES completion:nil];
}
@ -2497,8 +2529,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
picker.allowsEditing = NO;
picker.delegate = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissKeyBoard];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
});
}];
@ -2518,6 +2551,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
picker.delegate = self;
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
[self dismissKeyBoard];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
}
@ -3081,7 +3115,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
const CGFloat kIsAtBottomTolerancePts = 5;
// Note the usage of MAX() to handle the case where there isn't enough
// content to fill the collection view at its current size.
CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height);
CGFloat contentOffsetYBottom
= MAX(0.f, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height);
BOOL isScrolledToBottom = (self.collectionView.contentOffset.y > contentOffsetYBottom - kIsAtBottomTolerancePts);
return isScrolledToBottom;
@ -3274,6 +3309,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)attachmentButtonPressed
{
[self dismissKeyBoard];
__weak ConversationViewController *weakSelf = self;
if ([self isBlockedContactConversation]) {
@ -3349,6 +3385,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[gifAction setValue:gifImage forKey:@"image"];
[actionSheetController addAction:gifAction];
[self dismissKeyBoard];
[self presentViewController:actionSheetController animated:true completion:nil];
}
@ -3658,6 +3695,100 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
});
}
- (void)keyboardWillChangeFrame:(NSNotification *)notification
{
// `willChange` is the correct keyboard notifiation to observe when adjusting contentInset
// in lockstep with the keyboard presentation animation. `didChange` results in the contentInset
// not adjusting until after the keyboard is fully up.
DDLogVerbose(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
[self handleKeyboardNotification:notification];
}
- (void)handleKeyboardNotification:(NSNotification *)notification
{
AssertIsOnMainThread();
NSDictionary *userInfo = [notification userInfo];
NSValue *_Nullable keyboardBeginFrameValue = userInfo[UIKeyboardFrameBeginUserInfoKey];
if (!keyboardBeginFrameValue) {
OWSFail(@"%@ Missing keyboard begin frame", self.logTag);
return;
}
NSValue *_Nullable keyboardEndFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey];
if (!keyboardEndFrameValue) {
OWSFail(@"%@ Missing keyboard end frame", self.logTag);
return;
}
CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue];
// DDLogVerbose(@"%@ keyboard change. Old Frame: %@, New Frame: %@",
// self.logTag,
// NSStringFromCGRect(keyboardBeginFrame),
// NSStringFromCGRect(keyboardEndFrame));
UIEdgeInsets oldInsets = self.collectionView.contentInset;
UIEdgeInsets newInsets = oldInsets;
// bottomLayoutGuide accounts for extra offset needed on iPhoneX
newInsets.bottom = keyboardEndFrame.size.height - self.bottomLayoutGuide.length;
BOOL wasScrolledToBottom = [self isScrolledToBottom];
void (^adjustInsets)(void) = ^(void) {
self.collectionView.contentInset = newInsets;
self.collectionView.scrollIndicatorInsets = newInsets;
// Note there is a bug in iOS11.2 which where switching to the emoji keyboard
// does not fire a UIKeyboardFrameWillChange notification. In that case, the scroll
// down button gets mostly obscured by the keyboard.
// RADAR: #36297652
self.scrollDownButtonButtomConstraint.constant = -1 * newInsets.bottom;
[self.scrollDownButton setNeedsLayout];
[self.scrollDownButton layoutIfNeeded];
// HACK: I've made the assumption that we are already in the context of an animation, in which case the
// above should be sufficient to smoothly move the scrollDown button in step with the keyboard presentation
// animation. Yet, setting the constraint doesn't animate the movement of the button - it "jumps" to it's final
// position. So here we manually lay out the scroll down button frame (seemingly redundantly), which allows it
// to be smoothly animated.
CGRect newButtonFrame = self.scrollDownButton.frame;
newButtonFrame.origin.y
= self.scrollDownButton.superview.height - (newInsets.bottom + self.scrollDownButton.height);
self.scrollDownButton.frame = newButtonFrame;
// Adjust content offset to prevent the presented keyboard from obscuring content.
if (wasScrolledToBottom) {
// If we were scrolled to the bottom, don't do any fancy math. Just stay at the bottom.
[self scrollToBottomAnimated:NO];
} else {
// If we were scrolled away from the bottom, shift the content in lockstep with the
// keyboard, up to the limits of the content bounds.
CGFloat insetChange = newInsets.bottom - oldInsets.bottom;
CGFloat oldYOffset = self.collectionView.contentOffset.y;
CGFloat newYOffset = Clamp(oldYOffset + insetChange, 0, self.safeContentHeight);
CGPoint newOffset = CGPointMake(0, newYOffset);
// If the user is dismissing the keyboard via interactive scrolling, any additional conset offset feels
// redundant, so we only adjust content offset when *presenting* the keyboard.
if (insetChange > 0 && newYOffset > keyboardEndFrame.origin.y) {
[self.collectionView setContentOffset:newOffset animated:NO];
}
}
};
if (self.isViewCompletelyAppeared) {
adjustInsets();
} else {
// Even though we are scrolling without explicitly animating, the notification seems to occur within the context
// of a system animation, which is desirable when the view is visible, because the user sees the content rise
// in sync with the keyboard. However, when the view hasn't yet been presented, the animation conflicts and the
// result is that initial load causes the collection cells to visably "animate" to their final position once the
// view appears.
[UIView performWithoutAnimation:adjustInsets];
}
}
- (void)didApproveAttachment:(SignalAttachment *)attachment
{
OWSAssert(attachment);
@ -3690,13 +3821,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
return [self.collectionView.collectionViewLayout collectionViewContentSize].height;
}
- (void)scrollToBottomImmediately
{
OWSAssert([NSThread isMainThread]);
[self scrollToBottomAnimated:NO];
}
- (void)scrollToBottomAnimated:(BOOL)animated
{
OWSAssert([NSThread isMainThread]);
@ -3706,9 +3830,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}
CGFloat contentHeight = self.safeContentHeight;
CGFloat dstY = MAX(0, contentHeight - self.collectionView.height);
[self.collectionView setContentOffset:CGPointMake(0, dstY) animated:animated];
CGFloat dstY
= MAX(0, contentHeight + self.collectionView.contentInset.bottom - self.collectionView.bounds.size.height);
[self.collectionView setContentOffset:CGPointMake(0, dstY) animated:NO];
[self didScrollToBottom];
}
@ -3718,22 +3844,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
{
[self updateLastVisibleTimestamp];
[self autoLoadMoreIfNecessary];
if (self.isUserScrolling && [self isScrolledAwayFromBottom]) {
[self.inputToolbar endEditingTextMessage];
}
}
// See the comments on isScrolledToBottom.
- (BOOL)isScrolledAwayFromBottom
{
CGFloat contentHeight = self.safeContentHeight;
// Note the usage of MAX() to handle the case where there isn't enough
// content to fill the collection view at its current size.
CGFloat contentOffsetYBottom = MAX(0.f, contentHeight - self.collectionView.bounds.size.height);
const CGFloat kThreshold = 250;
BOOL isScrolledAwayFromBottom = (self.collectionView.contentOffset.y < contentOffsetYBottom - kThreshold);
return isScrolledAwayFromBottom;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
@ -4034,8 +4144,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)collectionViewWillChangeLayout
{
OWSAssert([NSThread isMainThread]);
self.wasScrolledToBottomBeforeLayoutChange = [self isScrolledToBottom];
}
- (void)collectionViewDidChangeLayout
@ -4043,15 +4151,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
OWSAssert([NSThread isMainThread]);
[self updateLastVisibleTimestamp];
// JSQMessageView has glitchy behavior. When presenting/dismissing view
// controllers, the size of the input toolbar and/or collection view can
// repeatedly change, leaving scroll state in an invalid state. The
// simplest fix that covers most cases is to ensure that we remain
// "scrolled to bottom" across these changes.
if (self.wasScrolledToBottomBeforeLayoutChange) {
[self scrollToBottomImmediately];
}
}
#pragma mark - View Items

View File

@ -68,6 +68,11 @@ NS_ASSUME_NONNULL_BEGIN
[self clearState];
return;
}
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
[self.collectionView layoutIfNeeded];
}
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
OWSFail(
@"%@ Collection view has invalid size: %@", self.logTag, NSStringFromCGRect(self.collectionView.bounds));

View File

@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)auditAndCleanupAsync:(void (^_Nullable)())completion
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion];
// [OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion];
});
}