Merge pull request #1247 from hyouuu/development

Fix tests; Add scrollToLastItem & scrollsToLastItemOnKeyboardBeginsEditing
This commit is contained in:
hyouuu
2020-02-18 14:46:06 -08:00
committed by GitHub
7 changed files with 69 additions and 29 deletions
+2 -2
View File
@@ -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 -7
View File
@@ -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`
+17 -1
View File
@@ -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() {
+7 -5
View File
@@ -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() {