Adjust contentInset for the keyboard frame properly#787
Adjust contentInset for the keyboard frame properly#787SD10 merged 9 commits intoMessageKit:developmentfrom
Conversation
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.
nathantannar4
left a comment
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yeah I think this is a really good idea as a fallback if a user needs to adjust things on their own
|
From my tests, this seems to work on all iPhone models. Don't have an iPad to test the split keyboard. |
|
@nathantannar4 In #762 , you said it has issue when use |
|
@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. |
|
@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. |
|
Hey @nathantannar4, when you did your tests were you just using the MessageKit example app or did you also look at child controllers? |
|
I did so using the new example app I have which uses child controllers Sent with GitHawk |
|
@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 */ { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@andreyvit So what's problem if we remove intersection.maxY < messagesCollectionView.frame.maxY - 1e-6 check?
|
The new update should address all the issues above. Introduced |
|
@andreyvit I see that you named the property |
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) |
There was a problem hiding this comment.
Just a friendly reminder to rename this as well
| didSet { | ||
| let delta = extraBottomInset - oldValue | ||
| guard abs(delta) > coordinatePrecision else { return } | ||
| messageCollectionViewBottomInset += delta |
There was a problem hiding this comment.
What's the reasoning behind not just applying the newValue to the bottom inset directly? I'm concerned this is unexpected behavior
There was a problem hiding this comment.
@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 */ { |
There was a problem hiding this comment.
@zhongwuzw asks:
@andreyvit
So what's problem if we removeintersection.maxY < messagesCollectionView.frame.maxY - 1e-6check?
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
@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.
|
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 } |
There was a problem hiding this comment.
@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 */ { |
There was a problem hiding this comment.
intersection.isNull has take the precision into consideration. intersection.maxY < messagesCollectionView.frame.maxY - coordinatePrecision is not needed.
…n when compare double
|
@andreyvit Thanks for your great |
SD10
left a comment
There was a problem hiding this comment.
Thanks @andreyvit @zhongwuzw @nathantannar4 💯 🥇
|
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! 😃 |
|
So which pod versions i need to install to have this update ? thanks @SD10 |
|
I'll be doing a MessageKit 1.1 release in a couple hours @JulienLevallois |
|
Okay thanks. Because i tried to install "development" branch 2 min ago, |
|
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. |
|
@andreyvit Thanks for your |
|
Yeah it looks like there is still issues, could be related to the |
|
Is this problem now fixed? |
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:
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.
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.
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.