Skip to content

Adjust contentInset for the keyboard frame properly#787

Merged
SD10 merged 9 commits intoMessageKit:developmentfrom
andreyvit:keyboard-inset-fix-to-rule-em-all
Aug 7, 2018
Merged

Adjust contentInset for the keyboard frame properly#787
SD10 merged 9 commits intoMessageKit:developmentfrom
andreyvit:keyboard-inset-fix-to-rule-em-all

Conversation

@andreyvit
Copy link
Contributor

I know that modifying the keyboard handling logic is all the rage these days, and wanted to get in on the action. :-) More seriously, I've been figuring out some keyboard handling issues in my app with a more complex structure of view controllers, and produced this PR to fix the issues found.

I believe this PR makes the code cleaner and more logical. It handles undocked keyboards and a bunch of other scenarios, and does not introduce weird hacks, animation blocks and the like.

Commit desciption follows:

This change provides better handling of keyboard frames. It copes with different values of UIViewController.edgesForExtendedLayout, UIScrollView.contentInsetAdjumentBehavior, view controller containment scenarios and undocked keyboards, and uses cleaner code.

Improvements and fixes in this change:

  1. Translates keyboard frame from screen coordinates into view controller coordinates. They aren’t the same thing when containment is involved, or edgesForExtendedLayout does not include .top.

  2. Avoids special casing any iPhone models and avoids dealing with safeAreaInsets manually. In iOS 11, UIScrollView may behave differently depending on contentInsetAdjumentBehavior, and the old code incorrectly assumed that the mode is .always (while it is in fact is .automatic) and that no extra safe area insets have been introduced at the bottom edge. Best part, we don’t have to deal with any of this at all, we can offload the logic to UIScrollView itself by looking at the difference between adjustedContentInset and contentInset.

  3. Handles view controller containment and undocked keyboards by taking into account an intersection of the scroll view frame and the keyboard’s frame. I think Apple demoes this in one of the WWDC videos, but I couldn’t find which one (or if I’m misremembering). Regardless, this technique makes sure we compute and use a sensible number.

Does this close any currently open issues?

#296 and likely #761.

Where has this been tested?

Devices/Simulators: Device: iPhone 8, Simulators: iPhone 8, iPhone X, iPhone SE, iPhone 6 Plus, iPad Pro 10", iPad Air 2.

iOS Version: 11.4, 10.3.1, 9.3

Swift Version: 4

MessageKit Version: on top of commit 57165b7, development branch, on top of 1.0.0.

This change provides better handling of keyboard frames. It copes with different values of UIViewController.edgesForExtendedLayout, UIScrollView.contentInsetAdjumentBehavior, view controller containment scenarios and undocked keyboards, and uses cleaner code.

Improvements and fixes in this change:

1. Translates keyboard frame from screen coordinates into view controller coordinates. They aren’t the same thing when containment is involved, or edgesForExtendedLayout does not include .top.

2. Avoids special casing any iPhone models and avoids dealing with safeAreaInsets manually. In iOS 11, UIScrollView may behave differently depending on contentInsetAdjumentBehavior, and the old code incorrectly assumed that the mode is .always (while it is in fact is .automatic) and that no extra safe area insets have been introduced at the bottom edge. Best part, we don’t have to deal with any of this at all, we can offload the logic to UIScrollView itself by looking at the difference between adjustedContentInset and contentInset.

3. Handles view controller containment and undocked keyboards by taking into account an intersection of the scroll view frame and the keyboard’s frame. I think Apple demoes this in one of the WWDC videos, but I couldn’t find which one (or if I’m misremembering). Regardless, this technique makes sure we compute and use a sensible number.

There’s also a couple of extra renames that make the intention of the modified code clearer.
Copy link
Member

@nathantannar4 nathantannar4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing! Im going to test this in my app

let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)

let newBottomInset = view.frame.height - keyboardEndFrame.minY - iPhoneXBottomInset
guard !isMessagesControllerBeingDismissed else { return }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep this above the keyboardEndFrame calculations as that extra computing time isn't needed if we are just going to exit out. I would place this at the top of the function.

}

// MARK: - Helpers
private func computeScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other functions in the project, could this function name be changed to requiredScrollViewBottomInset(for keybaordFrame: CGRect) -> CGFloat

@@ -68,8 +76,11 @@ extension MessagesViewController {
messageCollectionViewBottomInset = newBottomInset
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also wanted to add a new property to MessagesViewController so users can add additional inset. Something like, open var additionalBottomInset: CGFloat = 0 and then add this with newBottomInset

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this is a really good idea as a fallback if a user needs to adjust things on their own

@nathantannar4
Copy link
Member

From my tests, this seems to work on all iPhone models. Don't have an iPad to test the split keyboard.

@zhongwuzw
Copy link
Member

@nathantannar4 In #762 , you said it has issue when use iPhoneX, but the PR author said it's ok. #762 don't take the bottom inset into account. Would you figure out any reason?

@andreyvit
Copy link
Contributor Author

@zhongwuzw In #762, the additional auto-added inset isn't taken into account, so it will have issues on iPhone X where safe area insets are non-zero.

@andreyvit
Copy link
Contributor Author

@nathantannar4 Thanks for the review, will address your points.

guard !keyboardStartFrameInScreenCoords.isEmpty else {
// WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change
// notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to
// ignore this notification.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Nice trick

@SD10
Copy link
Member

SD10 commented Jul 31, 2018

Hey @nathantannar4, when you did your tests were you just using the MessageKit example app or did you also look at child controllers?

@nathantannar4
Copy link
Member

I did so using the new example app I have which uses child controllers

Sent with GitHawk

@SD10
Copy link
Member

SD10 commented Jul 31, 2018

@zhongwuzw Any input here? I've yet to test it myself but I'm pretty confident in this approach

// we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view
let intersection = messagesCollectionView.frame.intersection(keyboardFrame)

if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY - 1e-6 /* never compare floats without a tolerance */ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose here to compare intersection.maxY < messagesCollectionView.frame.maxY - 1e-6?
Is this situation, intersection.maxY == min(messagesCollectionView.frame.maxY, keyboardFrame.maxY, I think we only need to check isNull, if it fails, go to else statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equality does not always hold. The undocked keyboard still generates frame change notifications, and they go through the same code, so it's important to only apply the inset when the keyboard covers the bottom of the collection view.

I do agree that the check feels a bit weird. I've tried a different approach, one recommended by Apple: track whether the keyboard is visible using show/hide events, and ignore frame changes when it's not visible. (The undocked keyboard is considered hidden.) Unfortunately, I couldn't get this to work properly, because the input bar is a keyboard accessory, so it's crucial to differentiate between 2 cases — the keyboard being hidden (and the input bar being at the bottom) and the keyboard being undocked (and the input bar being tied to the keyboard). It turns out that just using the keyboard frame in all cases allows us to handle them all transparently without needing to branch.

I'll add a comment about this to the code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreyvit So what's problem if we remove intersection.maxY < messagesCollectionView.frame.maxY - 1e-6 check?

@andreyvit
Copy link
Contributor Author

The new update should address all the issues above. Introduced extraBottomInset property, moved code around and renamed functions as suggested by Nathan, added more explanations for the points raised by Wu Zhong, and filled in a CHANGELOG. Let me know if there's more I should change/add/explain. Also, do you want the commits squashed before merging?

@SD10
Copy link
Member

SD10 commented Aug 1, 2018

@andreyvit I see that you named the property extraBottomInset, can we rename it additionalBottomInset? The reason is that's the same name JSQMVC uses and I'd like to mirror that API

CHANGELOG.md Outdated

- Added customizable `accessoryView`, with a new `MessagesDisplayDelegate` function `configureAccessoryView`, and corresponding size & padding properties in `MessageSizeCalculator`. [#710](https://github.com/MessageKit/MessageKit/pull/710) by [@hyouuu](https://github.com/hyouuu)

- Added `extraBottomInset` property that allows to adjust the bottom content inset automatically set on the messages collection view by the view controller. [#787](https://github.com/MessageKit/MessageKit/pull/787) by [@andreyvit](https://github.com/andreyvit)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a friendly reminder to rename this as well

didSet {
let delta = extraBottomInset - oldValue
guard abs(delta) > coordinatePrecision else { return }
messageCollectionViewBottomInset += delta
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning behind not just applying the newValue to the bottom inset directly? I'm concerned this is unexpected behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SD10 Because the newValue here is the additional bottom inset, not the entire bottom inset. :-)

// we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view
let intersection = messagesCollectionView.frame.intersection(keyboardFrame)

if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY - 1e-6 /* never compare floats without a tolerance */ {
Copy link
Member

@SD10 SD10 Aug 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhongwuzw asks:

@andreyvit
So what's problem if we remove intersection.maxY < messagesCollectionView.frame.maxY - 1e-6 check?

Re-raising this comment

I really hate GitHub?

/// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used
/// as a measure of last resort when the built-in algorithm does not produce the right
/// value for your app. Please let us know when you end up having to use this property.
open var extraBottomInset: CGFloat = 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we can support an additionalTopInset as well? This can be used to allow users to overlay a view over the MessagesCollectionView. See #755

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SD10 🤔 Emm, how can we use additionalTopInset? If user set additionalTopInset, how we handle this? add to contentInset? maybe I think we don't need this currently.

@SD10
Copy link
Member

SD10 commented Aug 3, 2018

I've tested this and it seems to be working well -- the only problem is when you modally present another controller the bottom inset seems to be off if the keyboard isn't showing

open var extraBottomInset: CGFloat = 0 {
didSet {
let delta = extraBottomInset - oldValue
guard abs(delta) > coordinatePrecision else { return }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreyvit Seems like you are very careful to the comparison of double type. 😂 TBO these things maybe not needed, I think 1e-6 is only applied to compare the equality of two double, in our situations, we don't need accurate precision, just get the delta and assign to the messageCollectionViewBottomInset.

// see https://developer.apple.com/videos/play/wwdc2017/242/ for more details
let intersection = messagesCollectionView.frame.intersection(keyboardFrame)

if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY - coordinatePrecision /* never compare floats without a tolerance */ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intersection.isNull has take the precision into consideration. intersection.maxY < messagesCollectionView.frame.maxY - coordinatePrecision is not needed.

@zhongwuzw
Copy link
Member

zhongwuzw commented Aug 7, 2018

@andreyvit Thanks for your great PR, I removed the precision when compare double(If you think it would leads to problem, please let me know), and fix the insets calculation issue when present then dismiss vc.

Copy link
Member

@SD10 SD10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SD10 SD10 removed the under review label Aug 7, 2018
@SD10 SD10 merged commit f889487 into MessageKit:development Aug 7, 2018
@SD10
Copy link
Member

SD10 commented Aug 7, 2018

Thank you for contributing to MessageKit! I've invited you to join the MessageKit GitHub organization - no pressure to accept! If you'd like more information on what that means, check out our contributing guidelines and join the MessageKit Slack channel. Feel free to reach out if you have any questions! 😃

@ljs19923
Copy link

ljs19923 commented Aug 8, 2018

So which pod versions i need to install to have this update ? thanks @SD10

@SD10
Copy link
Member

SD10 commented Aug 8, 2018

I'll be doing a MessageKit 1.1 release in a couple hours @JulienLevallois

@ljs19923
Copy link

ljs19923 commented Aug 8, 2018

Okay thanks. Because i tried to install "development" branch 2 min ago,
and i have the same bug again :/ Its normal ?

@andreyvit
Copy link
Contributor Author

Hey, thanks for merging this. Do you still want me to do any changes you mentioned?

Regarding comparisons, I'm not sure if it's currently going to be a problem or not, but it's a rule when comparing floating-point numbers: never rely on them being exactly the same (or being exactly less or greater). It absolutely can be a problem in practice, although it's doesn't often occur in UI code because UI code doesn't do much arithmetics.

@zhongwuzw
Copy link
Member

@andreyvit Thanks for your PR, we'll do a release after SD10 checks everything is ok.

@SD10
Copy link
Member

SD10 commented Aug 10, 2018

Yeah it looks like there is still issues, could be related to the contentInset.top code now. Honestly, getting the inset to work properly for everyone is so frustrating. Debating starting from scratch

quyendx added a commit to quyendx/MessageKit that referenced this pull request Aug 13, 2018
@SD10 SD10 mentioned this pull request Aug 15, 2018
2 tasks
@ljs19923
Copy link

Is this problem now fixed?
Thanks @SD10

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants