Skip to content

Commit

Permalink
Merge pull request MessageKit#1247 from hyouuu/development
Browse files Browse the repository at this point in the history
Fix tests; Add scrollToLastItem & scrollsToLastItemOnKeyboardBeginsEditing
  • Loading branch information
hyouuu authored Feb 18, 2020
2 parents ea1df34 + d4f8a46 commit c4318f0
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 29 deletions.
4 changes: 2 additions & 2 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 15 additions & 7 deletions Documentation/QuickStart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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")
}

Expand Down
26 changes: 16 additions & 10 deletions Sources/Controllers/MessagesViewController+Keyboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions Sources/Controllers/MessagesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
18 changes: 17 additions & 1 deletion Sources/Views/MessagesCollectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -143,15 +143,15 @@ class TextMessageDisplayDelegateTests: XCTestCase {
at: IndexPath(item: 0, section: 0),
in: sut.messagesCollectionView)

XCTAssertEqual(textColor, .white)
XCTAssertEqual(textColor, .backgroundColor)
}

func testTextColorFromYou_returnsDarkTextForDefault() {
let textColor = sut.textColor(for: sut.dataProvider.messages[1],
at: IndexPath(item: 0, section: 0),
in: sut.messagesCollectionView)

XCTAssertEqual(textColor, .darkText)
XCTAssertEqual(textColor, .labelColor)
}

func testTextColorWithoutDataSource_returnsDarkTextForDefault() {
Expand All @@ -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() {
Expand Down
12 changes: 7 additions & 5 deletions Tests/ViewsTests/AvatarViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ 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() {
let avatar = Avatar(image: UIImage())
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() {
Expand All @@ -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() {
Expand Down

0 comments on commit c4318f0

Please sign in to comment.