Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Example/Sources/View Controllers/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,11 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
configureMessageCollectionView()
configureMessageInputBar()
loadFirstMessages()

}

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

messagesCollectionView.scrollToLastItem(animated: false)
MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
.onNewMessage { [weak self] message in
self?.insertMessage(message)
Expand All @@ -86,6 +84,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
DispatchQueue.main.async {
self.messageList = messages
self.messagesCollectionView.reloadData()
self.messagesCollectionView.scrollToLastItem(animated: false)
}
}
}
Expand All @@ -109,8 +108,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource {
messagesCollectionView.messageCellDelegate = self

scrollsToLastItemOnKeyboardBeginsEditing = true // default false
maintainPositionOnKeyboardFrameChanged = true // default false

maintainPositionOnInputBarHeightChanged = true // default false
showMessageTimestampOnSwipeLeft = true // default false

messagesCollectionView.refreshControl = refreshControl
Expand Down
2 changes: 1 addition & 1 deletion Example/Sources/Views/SwiftUI/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct MessagesView: UIViewControllerRepresentable {
messagesVC.messagesCollectionView.messagesDataSource = context.coordinator
messagesVC.messageInputBar.delegate = context.coordinator
messagesVC.scrollsToLastItemOnKeyboardBeginsEditing = true // default false
messagesVC.maintainPositionOnKeyboardFrameChanged = true // default false
messagesVC.maintainPositionOnInputBarHeightChanged = true // default false
messagesVC.showMessageTimestampOnSwipeLeft = true // default false

return messagesVC
Expand Down
62 changes: 41 additions & 21 deletions Sources/Controllers/MessagesViewController+Keyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal extension MessagesViewController {
func addKeyboardObservers() {
keyboardManager.bind(inputAccessoryView: inputContainerView)
keyboardManager.bind(to: messagesCollectionView)


/// Observe didBeginEditing to scroll down the content
NotificationCenter.default
.publisher(for: UITextView.textDidBeginEditingNotification)
.subscribe(on: DispatchQueue.global())
Expand All @@ -43,12 +44,30 @@ internal extension MessagesViewController {
self?.handleTextViewDidBeginEditing(notification)
}
.store(in: &disposeBag)


NotificationCenter.default
.publisher(for: UITextView.textDidChangeNotification)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.compactMap { $0.object as? InputTextView }
.filter { [weak self] textView in
return textView == self?.messageInputBar.inputTextView
}
.map(\.text)
.removeDuplicates()
.delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
.sink { [weak self] _ in
self?.updateMessageCollectionViewBottomInset()

if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
self?.messagesCollectionView.scrollToLastItem()
}
}
.store(in: &disposeBag)

Publishers.MergeMany(
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
Expand All @@ -66,49 +85,50 @@ internal extension MessagesViewController {
guard self.presentedViewController == nil else { return }
let collectionViewHeight = messagesCollectionView.frame.height
let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) - automaticallyAddedBottomInset
let normalizedNewBottomInset = max(0, newBottomInset)
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset

UIView.performWithoutAnimation {
guard differenceOfBottomInset != 0 else { return }
messagesCollectionView.contentInset.bottom = max(0, newBottomInset)
messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
}

if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
// Changing contentOffset to bigger number than the contentSize will result in a jump of content
// https://github.com/MessageKit/MessageKit/issues/1486
guard contentOffset.y <= messagesCollectionView.contentSize.height else { return }
messagesCollectionView.setContentOffset(contentOffset, animated: false)
}
}

// MARK: - Private methods

private func handleTextViewDidBeginEditing(_ notification: Notification) {
guard scrollsToLastItemOnKeyboardBeginsEditing || scrollsToLastItemOnKeyboardBeginsEditing else { return }
guard
scrollsToLastItemOnKeyboardBeginsEditing,
let inputTextView = notification.object as? InputTextView,
inputTextView === messageInputBar.inputTextView
else {
return
}
if scrollsToLastItemOnKeyboardBeginsEditing {
messagesCollectionView.scrollToLastItem()
} else {
messagesCollectionView.scrollToLastItem(animated: true)
}
messagesCollectionView.scrollToLastItem()
}

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

private var messageCollectionViewBottomInset: CGFloat {
return messagesCollectionView.contentInset.bottom
}

/// UIScrollView can automatically add safe area insets to its contentInset,
/// which needs to be accounted for when setting the contentInset based on screen coordinates.
///
/// - Returns: The distance automatically added to contentInset.top, if any.
private var automaticallyAddedTopInset: CGFloat {
return messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
}

private var messageCollectionViewTopInset: CGFloat {
return messagesCollectionView.contentInset.top
}
}
34 changes: 34 additions & 0 deletions Sources/Controllers/MessagesViewController+State.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ extension MessagesViewController {
class State {
/// Pan gesture for display the date of message by swiping left.
var panGesture: UIPanGestureRecognizer?
var maintainPositionOnInputBarHeightChanged: Bool = false
var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false

let inputContainerView: MessagesInputContainerView = .init()
let keyboardManager: KeyboardManager = KeyboardManager()
Expand All @@ -52,3 +54,35 @@ extension MessagesViewController {
set { state.disposeBag = newValue }
}
}

public extension MessagesViewController {
/// A Boolean value that determines whether the `MessagesCollectionView`
/// maintains it's current position when the height of the `MessageInputBar` changes.
///
/// The default value of this property is `false`.
@available(*, deprecated, renamed: "maintainPositionOnInputBarHeightChanged", message: "Please use new property - maintainPositionOnInputBarHeightChanged")
var maintainPositionOnKeyboardFrameChanged: Bool {
get { state.maintainPositionOnInputBarHeightChanged }
set { state.maintainPositionOnInputBarHeightChanged = newValue }
}

/// A Boolean value that determines whether the `MessagesCollectionView`
/// maintains it's current position when the height of the `MessageInputBar` changes.
///
/// The default value of this property is `false` and the `MessagesCollectionView` will scroll to bottom after the
/// height of the `MessageInputBar` changes.
var maintainPositionOnInputBarHeightChanged: Bool {
get { state.maintainPositionOnInputBarHeightChanged }
set { state.maintainPositionOnInputBarHeightChanged = newValue }
}

/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
/// last item whenever the `InputTextView` begins editing.
///
/// The default value of this property is `false`.
/// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
var scrollsToLastItemOnKeyboardBeginsEditing: Bool {
get { state.scrollsToLastItemOnKeyboardBeginsEditing }
set { state.scrollsToLastItemOnKeyboardBeginsEditing = newValue }
}
}
13 changes: 0 additions & 13 deletions Sources/Controllers/MessagesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,6 @@ open class MessagesViewController: UIViewController, UICollectionViewDelegateFlo
/// The `InputBarAccessoryView` used as the `inputAccessoryView` in the view controller.
open lazy var messageInputBar = InputBarAccessoryView()

/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
/// last item whenever the `InputTextView` begins editing.
///
/// The default value of this property is `false`.
/// NOTE: This is related to `scrollToLastItem` whereas the below flag is related to `scrollToBottom` - check each function for differences
open var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false

/// A Boolean value that determines whether the `MessagesCollectionView`
/// maintains it's current position when the height of the `MessageInputBar` changes.
///
/// The default value of this property is `false`.
open var maintainPositionOnKeyboardFrameChanged: Bool = false

/// Display the date of message by swiping left.
/// The default value of this property is `false`.
open var showMessageTimestampOnSwipeLeft: Bool = false {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Views/Cells/AudioMessageCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ open class AudioMessageCell: MessageContentCell {
}()

public lazy var activityIndicatorView: UIActivityIndicatorView = {
let activityIndicatorView = UIActivityIndicatorView(style: .gray)
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
activityIndicatorView.hidesWhenStopped = true
activityIndicatorView.isHidden = true
return activityIndicatorView
Expand Down
2 changes: 1 addition & 1 deletion Sources/Views/Cells/LocationMessageCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import MapKit
open class LocationMessageCell: MessageContentCell {

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

/// The image view holding the map image.
open var imageView = UIImageView()
Expand Down