mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
Merge pull request #1247 from hyouuu/development
Fix tests; Add scrollToLastItem & scrollsToLastItemOnKeyboardBeginsEditing
This commit is contained in:
+2
-2
@@ -1,3 +1,3 @@
|
||||
github "Quick/Nimble" "v8.0.1"
|
||||
github "Quick/Quick" "v2.0.0"
|
||||
github "Quick/Nimble" "v8.0.5"
|
||||
github "Quick/Quick" "v2.2.0"
|
||||
github "nathantannar4/InputBarAccessoryView" "4.3.0"
|
||||
|
||||
@@ -15,17 +15,18 @@ public protocol MessageType {
|
||||
var kind: MessageKind { get }
|
||||
}
|
||||
```
|
||||
First, each `MessageType` is required to have a `Sender` which contains two properties, `id` and `displayName`:
|
||||
First, each `MessageType` is required to have a `SenderType` which contains two properties, `senderId` and `displayName`:
|
||||
### Sender
|
||||
```Swift
|
||||
public struct Sender {
|
||||
public protocol SenderType {
|
||||
|
||||
public let id: String
|
||||
|
||||
public let displayName: String
|
||||
var senderId: String { get }
|
||||
|
||||
var displayName: String { get }
|
||||
}
|
||||
|
||||
```
|
||||
**MessageKit** uses the `Sender` type to determine if a message was sent by the current user or to the current user.
|
||||
**MessageKit** uses the `SenderType` type to determine if a message was sent by the current user or to the current user.
|
||||
|
||||
Second, each message must have its own `messageId` which is a unique `String` identifier for the message.
|
||||
|
||||
@@ -91,13 +92,20 @@ class ChatViewController: MessagesViewController {
|
||||
You must implement the following 3 methods to conform to `MessagesDataSource`:
|
||||
|
||||
```Swift
|
||||
|
||||
public struct Sender: SenderType {
|
||||
public let senderId: String
|
||||
|
||||
public let displayName: String
|
||||
}
|
||||
|
||||
// Some global variables for the sake of the example. Using globals is not recommended!
|
||||
let sender = Sender(id: "any_unique_id", displayName: "Steven")
|
||||
let messages: [MessageType] = []
|
||||
|
||||
extension ChatViewController: MessagesDataSource {
|
||||
|
||||
func currentSender() -> Sender {
|
||||
func currentSender() -> SenderType {
|
||||
return Sender(id: "any_unique_id", displayName: "Steven")
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,15 @@ internal extension MessagesViewController {
|
||||
|
||||
@objc
|
||||
private func handleTextViewDidBeginEditing(_ notification: Notification) {
|
||||
if scrollsToBottomOnKeyboardBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return }
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
if scrollsToLastItemOnKeyboardBeginsEditing || scrollsToBottomOnKeyboardBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView,
|
||||
inputTextView === messageInputBar.inputTextView else { return }
|
||||
|
||||
if scrollsToLastItemOnKeyboardBeginsEditing {
|
||||
messagesCollectionView.scrollToLastItem()
|
||||
} else {
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +68,12 @@ internal extension MessagesViewController {
|
||||
// ignore this notification.
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard self.presentedViewController == nil else {
|
||||
// This is important to skip notifications from child modal controllers in iOS >= 13.0
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Note that the check above does not exclude all notifications from an undocked keyboard, only the weird ones.
|
||||
//
|
||||
// We've tried following Apple's recommended approach of tracking UIKeyboardWillShow / UIKeyboardDidHide and ignoring frame
|
||||
@@ -82,18 +88,18 @@ internal extension MessagesViewController {
|
||||
// We could make it work by adding extra checks for the state of the keyboard and compensating accordingly, but it seems easier
|
||||
// to simply check whether the current keyboard frame, whatever it is (even when undocked), covers the bottom of the collection
|
||||
// view.
|
||||
|
||||
|
||||
guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window)
|
||||
|
||||
|
||||
let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame)
|
||||
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
|
||||
|
||||
|
||||
if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 {
|
||||
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
|
||||
messagesCollectionView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
|
||||
messageCollectionViewBottomInset = newBottomInset
|
||||
}
|
||||
|
||||
@@ -116,7 +122,7 @@ internal extension MessagesViewController {
|
||||
// we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view;
|
||||
// see https://developer.apple.com/videos/play/wwdc2017/242/ for more details
|
||||
let intersection = messagesCollectionView.frame.intersection(keyboardFrame)
|
||||
|
||||
|
||||
if intersection.isNull || (messagesCollectionView.frame.maxY - intersection.maxY) > 0.001 {
|
||||
// The keyboard is hidden, is a hardware one, or is undocked and does not cover the bottom of the collection view.
|
||||
// Note: intersection.maxY may be less than messagesCollectionView.frame.maxY when dealing with undocked keyboards.
|
||||
|
||||
@@ -36,10 +36,18 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
|
||||
/// 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 calls scrollToLastItem where as the below flag calls scrollToBottom - check methods for differences
|
||||
open var scrollsToLastItemOnKeyboardBeginsEditing: Bool = false
|
||||
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
|
||||
/// bottom whenever the `InputTextView` begins editing.
|
||||
///
|
||||
/// The default value of this property is `false`.
|
||||
/// NOTE: This calls scrollToBottome where as the above flag calls scrollToLastItem - check methods for differences
|
||||
open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false
|
||||
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView`
|
||||
|
||||
@@ -100,8 +100,24 @@ open class MessagesCollectionView: UICollectionView {
|
||||
cell?.handleTapGesture(gesture)
|
||||
}
|
||||
|
||||
// NOTE: It's possible for small content size this wouldn't work - https://github.com/MessageKit/MessageKit/issues/725
|
||||
public func scrollToLastItem(at pos: UICollectionView.ScrollPosition = .bottom, animated: Bool = true) {
|
||||
guard numberOfSections > 0 else { return }
|
||||
|
||||
let lastSection = numberOfSections - 1
|
||||
let lastItemIndex = numberOfItems(inSection: lastSection) - 1
|
||||
|
||||
guard lastItemIndex >= 0 else { return }
|
||||
|
||||
let indexPath = IndexPath(row: lastItemIndex, section: lastSection)
|
||||
scrollToItem(at: indexPath, at: pos, animated: animated)
|
||||
}
|
||||
|
||||
// NOTE: This method seems to cause crash in certain cases - https://github.com/MessageKit/MessageKit/issues/725
|
||||
// Could try using `scrollToLastItem` above
|
||||
public func scrollToBottom(animated: Bool = false) {
|
||||
performBatchUpdates(nil) { _ in
|
||||
performBatchUpdates(nil) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let collectionViewContentHeight = self.collectionViewLayout.collectionViewContentSize.height
|
||||
self.scrollRectToVisible(CGRect(0.0, collectionViewContentHeight - 1.0, 1.0, 1.0), animated: animated)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class MessagesDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(backgroundColor, .white)
|
||||
XCTAssertEqual(backgroundColor, .backgroundColor)
|
||||
}
|
||||
|
||||
func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() {
|
||||
@@ -143,7 +143,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .white)
|
||||
XCTAssertEqual(textColor, .backgroundColor)
|
||||
}
|
||||
|
||||
func testTextColorFromYou_returnsDarkTextForDefault() {
|
||||
@@ -151,7 +151,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .darkText)
|
||||
XCTAssertEqual(textColor, .labelColor)
|
||||
}
|
||||
|
||||
func testTextColorWithoutDataSource_returnsDarkTextForDefault() {
|
||||
@@ -161,7 +161,7 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
|
||||
XCTAssertEqual(textColor, .darkText)
|
||||
XCTAssertEqual(textColor, .labelColor)
|
||||
}
|
||||
|
||||
func testEnableDetectors_returnsEmptyForDefault() {
|
||||
|
||||
@@ -42,7 +42,9 @@ class AvatarViewTests: XCTestCase {
|
||||
|
||||
func testNoParams() {
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
// For certain dynamic colors, need to compare cgColor in XCTest
|
||||
// https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testWithImage() {
|
||||
@@ -50,7 +52,7 @@ class AvatarViewTests: XCTestCase {
|
||||
avatarView.set(avatar: avatar)
|
||||
XCTAssertEqual(avatar.initials, "?")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testInitialsOnly() {
|
||||
@@ -59,13 +61,13 @@ class AvatarViewTests: XCTestCase {
|
||||
XCTAssertEqual(avatarView.initials, avatar.initials)
|
||||
XCTAssertEqual(avatar.initials, "DL")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
}
|
||||
|
||||
func testSetBackground() {
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.grayColor.cgColor)
|
||||
avatarView.backgroundColor = UIColor.red
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.red)
|
||||
XCTAssertEqual(avatarView.backgroundColor!, UIColor.red)
|
||||
}
|
||||
|
||||
func testGetImage() {
|
||||
|
||||
Reference in New Issue
Block a user