Skip to content

Commit 7d66a98

Browse files
authored
Updating messagesCollectionView's bottom inset and content position on inpuBar height change (#1705)
* refactor: InpuBar height change behavior * refactor: InpuBar height change behavior Changelog
1 parent 2137ce7 commit 7d66a98

8 files changed

Lines changed: 81 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa
1414

1515
- **Breaking change**: Dropped support for iOS 12 [2bd234b](https://github.com/MessageKit/MessageKit/commit/2bd234b1e878f392089f166d6868ce644d6c9e95) by [@martinpucik](https://github.com/martinpucik)
1616
- **Breaking change**: Moved messageInputBar from inputAccessoryView to a subview in MessagesViewController [#1704](https://github.com/MessageKit/MessageKit/pull/1704) by [@martinpucik](https://github.com/martinpucik)
17+
- **Deprecation**: Deprecated `maintainPositionOnKeyboardFrameChangedMoved` in favor of `maintainPositionOnInputBarHeightChanged` which better describes the intended use of this property [#1704](https://github.com/MessageKit/MessageKit/pull/1705) by [@martinpucik](https://github.com/martinpucik)
1718
- **Breaking change**: Added an argument to `messageContainerMaxWidth` [cd4f75b](https://github.com/MessageKit/MessageKit/commit/cd4f75b561129fc25e6c4576000e5a92ccd81cad) by [@martinpucik](https://github.com/martinpucik)
1819
```swift
1920
MessageSizeCalculator.messageContainerMaxWidth(for message: MessageType) -> CGFloat

Example/Sources/View Controllers/ChatViewController.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,11 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
6060
configureMessageCollectionView()
6161
configureMessageInputBar()
6262
loadFirstMessages()
63-
6463
}
6564

6665
override func viewDidAppear(_ animated: Bool) {
6766
super.viewDidAppear(animated)
6867

69-
messagesCollectionView.scrollToLastItem(animated: false)
7068
MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
7169
.onNewMessage { [weak self] message in
7270
self?.insertMessage(message)
@@ -86,6 +84,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
8684
DispatchQueue.main.async {
8785
self.messageList = messages
8886
self.messagesCollectionView.reloadData()
87+
self.messagesCollectionView.scrollToLastItem(animated: false)
8988
}
9089
}
9190
}
@@ -109,8 +108,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
109108
messagesCollectionView.messageCellDelegate = self
110109

111110
scrollsToLastItemOnKeyboardBeginsEditing = true // default false
112-
maintainPositionOnKeyboardFrameChanged = true // default false
113-
111+
maintainPositionOnInputBarHeightChanged = true // default false
114112
showMessageTimestampOnSwipeLeft = true // default false
115113

116114
messagesCollectionView.refreshControl = refreshControl

Example/Sources/Views/SwiftUI/MessagesView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct MessagesView: UIViewControllerRepresentable {
4848
messagesVC.messagesCollectionView.messagesDataSource = context.coordinator
4949
messagesVC.messageInputBar.delegate = context.coordinator
5050
messagesVC.scrollsToLastItemOnKeyboardBeginsEditing = true // default false
51-
messagesVC.maintainPositionOnKeyboardFrameChanged = true // default false
51+
messagesVC.maintainPositionOnInputBarHeightChanged = true // default false
5252
messagesVC.showMessageTimestampOnSwipeLeft = true // default false
5353

5454
return messagesVC

Sources/Controllers/MessagesViewController+Keyboard.swift

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ internal extension MessagesViewController {
3434
func addKeyboardObservers() {
3535
keyboardManager.bind(inputAccessoryView: inputContainerView)
3636
keyboardManager.bind(to: messagesCollectionView)
37-
37+
38+
/// Observe didBeginEditing to scroll down the content
3839
NotificationCenter.default
3940
.publisher(for: UITextView.textDidBeginEditingNotification)
4041
.subscribe(on: DispatchQueue.global())
@@ -43,12 +44,30 @@ internal extension MessagesViewController {
4344
self?.handleTextViewDidBeginEditing(notification)
4445
}
4546
.store(in: &disposeBag)
46-
47+
48+
NotificationCenter.default
49+
.publisher(for: UITextView.textDidChangeNotification)
50+
.subscribe(on: DispatchQueue.global())
51+
.receive(on: DispatchQueue.main)
52+
.compactMap { $0.object as? InputTextView }
53+
.filter { [weak self] textView in
54+
return textView == self?.messageInputBar.inputTextView
55+
}
56+
.map(\.text)
57+
.removeDuplicates()
58+
.delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
59+
.sink { [weak self] _ in
60+
self?.updateMessageCollectionViewBottomInset()
61+
62+
if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
63+
self?.messagesCollectionView.scrollToLastItem()
64+
}
65+
}
66+
.store(in: &disposeBag)
67+
4768
Publishers.MergeMany(
4869
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
49-
NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification),
50-
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
51-
NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)
70+
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
5271
)
5372
.subscribe(on: DispatchQueue.global())
5473
.receive(on: DispatchQueue.main)
@@ -66,49 +85,50 @@ internal extension MessagesViewController {
6685
guard self.presentedViewController == nil else { return }
6786
let collectionViewHeight = messagesCollectionView.frame.height
6887
let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) - automaticallyAddedBottomInset
88+
let normalizedNewBottomInset = max(0, newBottomInset)
6989
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
7090

7191
UIView.performWithoutAnimation {
7292
guard differenceOfBottomInset != 0 else { return }
73-
messagesCollectionView.contentInset.bottom = max(0, newBottomInset)
93+
messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
7494
messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
7595
}
76-
77-
if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
78-
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
79-
// Changing contentOffset to bigger number than the contentSize will result in a jump of content
80-
// https://github.com/MessageKit/MessageKit/issues/1486
81-
guard contentOffset.y <= messagesCollectionView.contentSize.height else { return }
82-
messagesCollectionView.setContentOffset(contentOffset, animated: false)
83-
}
8496
}
8597

8698
// MARK: - Private methods
8799

88100
private func handleTextViewDidBeginEditing(_ notification: Notification) {
89-
guard scrollsToLastItemOnKeyboardBeginsEditing || scrollsToLastItemOnKeyboardBeginsEditing else { return }
90101
guard
102+
scrollsToLastItemOnKeyboardBeginsEditing,
91103
let inputTextView = notification.object as? InputTextView,
92104
inputTextView === messageInputBar.inputTextView
93105
else {
94106
return
95107
}
96-
if scrollsToLastItemOnKeyboardBeginsEditing {
97-
messagesCollectionView.scrollToLastItem()
98-
} else {
99-
messagesCollectionView.scrollToLastItem(animated: true)
100-
}
108+
messagesCollectionView.scrollToLastItem()
101109
}
102110

103111
/// UIScrollView can automatically add safe area insets to its contentInset,
104112
/// which needs to be accounted for when setting the contentInset based on screen coordinates.
105113
///
106114
/// - Returns: The distance automatically added to contentInset.bottom, if any.
107115
private var automaticallyAddedBottomInset: CGFloat {
108-
return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom
116+
return messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset
109117
}
110118

111119
private var messageCollectionViewBottomInset: CGFloat {
112120
return messagesCollectionView.contentInset.bottom
113121
}
122+
123+
/// UIScrollView can automatically add safe area insets to its contentInset,
124+
/// which needs to be accounted for when setting the contentInset based on screen coordinates.
125+
///
126+
/// - Returns: The distance automatically added to contentInset.top, if any.
127+
private var automaticallyAddedTopInset: CGFloat {
128+
return messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
129+
}
130+
131+
private var messageCollectionViewTopInset: CGFloat {
132+
return messagesCollectionView.contentInset.top
133+
}
114134
}

Sources/Controllers/MessagesViewController+State.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ extension MessagesViewController {
3131
class State {
3232
/// Pan gesture for display the date of message by swiping left.
3333
var panGesture: UIPanGestureRecognizer?
34+
var maintainPositionOnInputBarHeightChanged: Bool = false
35+
var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false
3436

3537
let inputContainerView: MessagesInputContainerView = .init()
3638
let keyboardManager: KeyboardManager = KeyboardManager()
@@ -52,3 +54,35 @@ extension MessagesViewController {
5254
set { state.disposeBag = newValue }
5355
}
5456
}
57+
58+
public extension MessagesViewController {
59+
/// A Boolean value that determines whether the `MessagesCollectionView`
60+
/// maintains it's current position when the height of the `MessageInputBar` changes.
61+
///
62+
/// The default value of this property is `false`.
63+
@available(*, deprecated, renamed: "maintainPositionOnInputBarHeightChanged", message: "Please use new property - maintainPositionOnInputBarHeightChanged")
64+
var maintainPositionOnKeyboardFrameChanged: Bool {
65+
get { state.maintainPositionOnInputBarHeightChanged }
66+
set { state.maintainPositionOnInputBarHeightChanged = newValue }
67+
}
68+
69+
/// A Boolean value that determines whether the `MessagesCollectionView`
70+
/// maintains it's current position when the height of the `MessageInputBar` changes.
71+
///
72+
/// The default value of this property is `false` and the `MessagesCollectionView` will scroll to bottom after the
73+
/// height of the `MessageInputBar` changes.
74+
var maintainPositionOnInputBarHeightChanged: Bool {
75+
get { state.maintainPositionOnInputBarHeightChanged }
76+
set { state.maintainPositionOnInputBarHeightChanged = newValue }
77+
}
78+
79+
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
80+
/// last item whenever the `InputTextView` begins editing.
81+
///
82+
/// The default value of this property is `false`.
83+
/// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
84+
var scrollsToLastItemOnKeyboardBeginsEditing: Bool {
85+
get { state.scrollsToLastItemOnKeyboardBeginsEditing }
86+
set { state.scrollsToLastItemOnKeyboardBeginsEditing = newValue }
87+
}
88+
}

Sources/Controllers/MessagesViewController.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,6 @@ open class MessagesViewController: UIViewController, UICollectionViewDelegateFlo
3737
/// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
3838
open lazy var messageInputBar = InputBarAccessoryView()
3939

40-
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
41-
/// last item whenever the `InputTextView` begins editing.
42-
///
43-
/// The default value of this property is `false`.
44-
/// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
45-
open var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false
46-
47-
/// A Boolean value that determines whether the `MessagesCollectionView`
48-
/// maintains it's current position when the height of the `MessageInputBar` changes.
49-
///
50-
/// The default value of this property is `false`.
51-
open var maintainPositionOnKeyboardFrameChanged: Bool = false
52-
5340
/// Display the date of message by swiping left.
5441
/// The default value of this property is `false`.
5542
open var showMessageTimestampOnSwipeLeft: Bool = false {

Sources/Views/Cells/AudioMessageCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ open class AudioMessageCell: MessageContentCell {
4848
}()
4949

5050
public lazy var activityIndicatorView: UIActivityIndicatorView = {
51-
let activityIndicatorView = UIActivityIndicatorView(style: .gray)
51+
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
5252
activityIndicatorView.hidesWhenStopped = true
5353
activityIndicatorView.isHidden = true
5454
return activityIndicatorView

Sources/Views/Cells/LocationMessageCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import MapKit
2929
open class LocationMessageCell: MessageContentCell {
3030

3131
/// The activity indicator to be displayed while the map image is loading.
32-
open var activityIndicator = UIActivityIndicatorView(style: .gray)
32+
open var activityIndicator = UIActivityIndicatorView(style: .medium)
3333

3434
/// The image view holding the map image.
3535
open var imageView = UIImageView()

0 commit comments

Comments
 (0)