From 38047d64b2e4f1ab6fb80ee9f452b5c34f0e2aac Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 14 Aug 2017 16:20:07 -0600 Subject: [PATCH 01/27] Adding tests, `textColorMethod` and making MessagesDisplayDataSource have a default implementation. --- MessageKit.xcodeproj/project.pbxproj | 16 ++++++ Sources/MessagesDisplayDataSource.swift | 45 +++++++++------ Tests/MessagesDisplayDataSourceTests.swift | 64 ++++++++++++++++++++++ Tests/TestMessageModel.swift | 23 ++++++++ 4 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 Tests/MessagesDisplayDataSourceTests.swift create mode 100644 Tests/TestMessageModel.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 42f3fefe..5aa5cfba 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171D5AB81F36712B0053DF69 /* InputTextView.swift */; }; 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEA1F36C15600B57FBD /* AvatarView.swift */; }; 372F6AEF1F36C61000B57FBD /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */; }; + 376AD1821F4258D80083072A /* TestMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AD1811F4258D80083072A /* TestMessageModel.swift */; }; + 376AD1861F4259270083072A /* MessagesDisplayDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AD1851F4259270083072A /* MessagesDisplayDataSourceTests.swift */; }; 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C936971F38F6AC00853DF2 /* Avatar.swift */; }; 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D75831DE507320033F95F /* MessagesDataSource.swift */; }; 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */; }; @@ -51,6 +53,8 @@ 171D5AB81F36712B0053DF69 /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 372F6AEA1F36C15600B57FBD /* AvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; + 376AD1811F4258D80083072A /* TestMessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestMessageModel.swift; sourceTree = ""; }; + 376AD1851F4259270083072A /* MessagesDisplayDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSourceTests.swift; sourceTree = ""; }; 37C936971F38F6AC00853DF2 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; 882D75831DE507320033F95F /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; @@ -100,6 +104,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 376AD1801F4258C50083072A /* Support Files */ = { + isa = PBXGroup; + children = ( + 376AD1811F4258D80083072A /* TestMessageModel.swift */, + ); + name = "Support Files"; + sourceTree = ""; + }; 88916B181CF0DF2F00469F91 = { isa = PBXGroup; children = ( @@ -138,6 +150,8 @@ 88916B421CF0DF5900469F91 /* Info.plist */, 88916B431CF0DF5900469F91 /* MessageKitTests.swift */, 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */, + 376AD1851F4259270083072A /* MessagesDisplayDataSourceTests.swift */, + 376AD1801F4258C50083072A /* Support Files */, ); path = Tests; sourceTree = SOURCE_ROOT; @@ -355,6 +369,7 @@ B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */, B0655A2A1F23D77200542A83 /* Sender.swift in Sources */, B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, + 376AD1821F4258D80083072A /* TestMessageModel.swift in Sources */, B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */, B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */, B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */, @@ -375,6 +390,7 @@ buildActionMask = 2147483647; files = ( 88916B451CF0DF5900469F91 /* MessageKitTests.swift in Sources */, + 376AD1861F4259270083072A /* MessagesDisplayDataSourceTests.swift in Sources */, 372F6AEF1F36C61000B57FBD /* AvatarViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index cafc2431..f3c980af 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -25,41 +25,52 @@ import Foundation public protocol MessagesDisplayDataSource: class, MessagesDataSource { - - func messageColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - - func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar - + + func textColor(for message: MessageType, at indexPath: IndexPath) -> UIColor + + func backgroundColor(for message: MessageType, at indexPath: IndexPath) -> UIColor + + func avatar(for message: MessageType) -> Avatar + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? - + func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? - + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - + } -public extension MessagesDisplayDataSource { - - func messageColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { +// "where Self: MessagesViewController" make these methods a `default` implimentation for any view that subclasses of MessagesViewController +public extension MessagesDisplayDataSource where Self: MessagesViewController { + + func textColor(for message: MessageType, at indexPath: IndexPath) -> UIColor { + return isFromCurrentSender(message: message) ? .white : .black + } + + func backgroundColor(for message: MessageType, at indexPath: IndexPath) -> UIColor { return isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray } - + + func avatar(for message: MessageType) -> Avatar { + return Avatar() + } + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { return nil } - + func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? { return nil } - + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } - + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } - + } diff --git a/Tests/MessagesDisplayDataSourceTests.swift b/Tests/MessagesDisplayDataSourceTests.swift new file mode 100644 index 00000000..92cc72d0 --- /dev/null +++ b/Tests/MessagesDisplayDataSourceTests.swift @@ -0,0 +1,64 @@ +// +// MessagesDisplayDataSourceTests.swift +// MessageKit +// +// Created by Dan Leonard on 8/14/17. +// Copyright © 2017 MessageKit. All rights reserved. +// + +import XCTest +@testable import MessageKit + +class MessagesDisplayDataSourceTests: XCTestCase { + + let testClass = TestMessagesViewControllerModel() + override func setUp() { + super.setUp() + // Ensures that the messageList has been set. + testClass.viewDidLoad() + } + + override func tearDown() { + super.tearDown() + } + + func testInit() { + XCTAssertNotNil(testClass) + XCTAssertNotNil(testClass.messageList) + } + + func testMessageTextColorDefaultState() { + XCTAssertEqual(testClass.textColorFor(testClass.messageList[0]), UIColor.white) + XCTAssertEqual(testClass.textColorFor(testClass.messageList[1]), UIColor.black) + } + + func testBackGroundColorDefaultState() { + XCTAssertEqual(testClass.backgroundColorFor(testClass.messageList[0]), UIColor.outgoingGreen) + XCTAssertNotEqual(testClass.backgroundColorFor(testClass.messageList[0]), UIColor.incomingGray) + XCTAssertEqual(testClass.backgroundColorFor(testClass.messageList[1]), UIColor.incomingGray) + XCTAssertNotEqual(testClass.backgroundColorFor(testClass.messageList[1]), UIColor.outgoingGreen) + } + + func testAvatarDefaultState() { + XCTAssertNotNil(testClass.avatarForMessage(testClass.messageList[0])) + XCTAssertEqual(testClass.avatarForMessage(testClass.messageList[0]).initals, "?") + XCTAssertNotNil(testClass.avatarForMessage(testClass.messageList[1])) + XCTAssertEqual(testClass.avatarForMessage(testClass.messageList[1]).initals, "?") + } + + func testHeaderDefaultState() { + XCTAssertNil(testClass.headerForMessage(testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView)) + } + + func testFooterDefaultState() { + XCTAssertNil(testClass.footerForMessage(testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView)) + } + + func testCellTopLabelDefaultState() { + XCTAssertNil(testClass.cellTopLabelAttributedText(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0))) + } + + func testCellBottomLabelDefaultState() { + XCTAssertNil(testClass.cellBottomLabelAttributedText(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0))) + } +} diff --git a/Tests/TestMessageModel.swift b/Tests/TestMessageModel.swift new file mode 100644 index 00000000..35d701c5 --- /dev/null +++ b/Tests/TestMessageModel.swift @@ -0,0 +1,23 @@ +// +// TestMessageModel.swift +// MessageKit +// +// Created by Dan Leonard on 8/14/17. +// Copyright © 2017 MessageKit. All rights reserved. +// + +import Foundation +struct TestMessage: MessageType { + var messageId: String + var sender: Sender + var sentDate: Date + var data: MessageData + + init(text: String, sender: Sender, messageId: String) { + data = .text(text) + self.sender = sender + self.messageId = messageId + self.sentDate = Date() + } + +} From ef1b167363424fc7aa9155c5b41ab39a1b5712da Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 14 Aug 2017 16:42:35 -0600 Subject: [PATCH 02/27] updated parameters and fixed tests accordingly. --- MessageKit.xcodeproj/project.pbxproj | 4 ++ Sources/MessagesCollectionView.swift | 2 + Sources/MessagesViewController.swift | 6 ++- Tests/MessagesDisplayDataSourceTests.swift | 28 +++++--------- Tests/TestMessagesViewControllerModel.swift | 42 +++++++++++++++++++++ 5 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 Tests/TestMessagesViewControllerModel.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 5aa5cfba..721073d2 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 372F6AEF1F36C61000B57FBD /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */; }; 376AD1821F4258D80083072A /* TestMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AD1811F4258D80083072A /* TestMessageModel.swift */; }; 376AD1861F4259270083072A /* MessagesDisplayDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AD1851F4259270083072A /* MessagesDisplayDataSourceTests.swift */; }; + 376AD1881F4259D20083072A /* TestMessagesViewControllerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AD1871F4259D20083072A /* TestMessagesViewControllerModel.swift */; }; 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C936971F38F6AC00853DF2 /* Avatar.swift */; }; 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D75831DE507320033F95F /* MessagesDataSource.swift */; }; 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */; }; @@ -55,6 +56,7 @@ 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; 376AD1811F4258D80083072A /* TestMessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestMessageModel.swift; sourceTree = ""; }; 376AD1851F4259270083072A /* MessagesDisplayDataSourceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSourceTests.swift; sourceTree = ""; }; + 376AD1871F4259D20083072A /* TestMessagesViewControllerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestMessagesViewControllerModel.swift; sourceTree = ""; }; 37C936971F38F6AC00853DF2 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; 882D75831DE507320033F95F /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; @@ -108,6 +110,7 @@ isa = PBXGroup; children = ( 376AD1811F4258D80083072A /* TestMessageModel.swift */, + 376AD1871F4259D20083072A /* TestMessagesViewControllerModel.swift */, ); name = "Support Files"; sourceTree = ""; @@ -367,6 +370,7 @@ B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */, B0655A281F23D71400542A83 /* MessageDirection.swift in Sources */, B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */, + 376AD1881F4259D20083072A /* TestMessagesViewControllerModel.swift in Sources */, B0655A2A1F23D77200542A83 /* Sender.swift in Sources */, B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, 376AD1821F4258D80083072A /* TestMessageModel.swift in Sources */, diff --git a/Sources/MessagesCollectionView.swift b/Sources/MessagesCollectionView.swift index ea60e5ce..2cabe8cc 100644 --- a/Sources/MessagesCollectionView.swift +++ b/Sources/MessagesCollectionView.swift @@ -33,6 +33,8 @@ open class MessagesCollectionView: UICollectionView { open weak var messagesLayoutDelegate: MessagesLayoutDelegate? open weak var messageCellDelegate: MessageCellDelegate? + + open weak var messagesDisplayDataSource: MessagesDisplayDataSource? private var indexPathForLastItem: IndexPath? { diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 1d0eda26..5183e1f7 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -165,14 +165,16 @@ extension MessagesViewController: UICollectionViewDataSource { guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let messageColor = displayDataSource.messageColor(for: message, at: indexPath, in: messagesCollectionView) - let avatar = displayDataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) + let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath) + let textColor = displayDataSource.textColor(for: message, at: indexPath) + let avatar = displayDataSource.avatar(for: message) let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) cell.cellTopLabel.attributedText = topLabelText cell.cellBottomLabel.attributedText = bottomLabelText cell.avatarView.set(avatar: avatar) + cell.messageLabel.textColor = textColor cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) diff --git a/Tests/MessagesDisplayDataSourceTests.swift b/Tests/MessagesDisplayDataSourceTests.swift index 92cc72d0..ce65adeb 100644 --- a/Tests/MessagesDisplayDataSourceTests.swift +++ b/Tests/MessagesDisplayDataSourceTests.swift @@ -28,30 +28,22 @@ class MessagesDisplayDataSourceTests: XCTestCase { } func testMessageTextColorDefaultState() { - XCTAssertEqual(testClass.textColorFor(testClass.messageList[0]), UIColor.white) - XCTAssertEqual(testClass.textColorFor(testClass.messageList[1]), UIColor.black) + XCTAssertEqual(testClass.textColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.white) + XCTAssertEqual(testClass.textColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.black) } func testBackGroundColorDefaultState() { - XCTAssertEqual(testClass.backgroundColorFor(testClass.messageList[0]), UIColor.outgoingGreen) - XCTAssertNotEqual(testClass.backgroundColorFor(testClass.messageList[0]), UIColor.incomingGray) - XCTAssertEqual(testClass.backgroundColorFor(testClass.messageList[1]), UIColor.incomingGray) - XCTAssertNotEqual(testClass.backgroundColorFor(testClass.messageList[1]), UIColor.outgoingGreen) + XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.outgoingGreen) + XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.incomingGray) + XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.incomingGray) + XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.outgoingGreen) } func testAvatarDefaultState() { - XCTAssertNotNil(testClass.avatarForMessage(testClass.messageList[0])) - XCTAssertEqual(testClass.avatarForMessage(testClass.messageList[0]).initals, "?") - XCTAssertNotNil(testClass.avatarForMessage(testClass.messageList[1])) - XCTAssertEqual(testClass.avatarForMessage(testClass.messageList[1]).initals, "?") - } - - func testHeaderDefaultState() { - XCTAssertNil(testClass.headerForMessage(testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView)) - } - - func testFooterDefaultState() { - XCTAssertNil(testClass.footerForMessage(testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView)) + XCTAssertNotNil(testClass.avatar(for: testClass.messageList[0]).initals) + XCTAssertNotNil(testClass.avatar(for: testClass.messageList[1]).initals) + XCTAssertEqual(testClass.avatar(for: testClass.messageList[0]).initals, "?") + XCTAssertEqual(testClass.avatar(for: testClass.messageList[1]).initals, "?") } func testCellTopLabelDefaultState() { diff --git a/Tests/TestMessagesViewControllerModel.swift b/Tests/TestMessagesViewControllerModel.swift new file mode 100644 index 00000000..010a435a --- /dev/null +++ b/Tests/TestMessagesViewControllerModel.swift @@ -0,0 +1,42 @@ +// +// TestMessagesViewControllerModel.swift +// MessageKit +// +// Created by Dan Leonard on 8/14/17. +// Copyright © 2017 MessageKit. All rights reserved. +// + +import Foundation +import Foundation +class TestMessagesViewControllerModel: MessagesViewController, MessagesDisplayDataSource { + + var messageList: [TestMessage] = [] + + static let sender1 = Sender(id: "1", displayName: "Dan") + static let sender2 = Sender(id: "2", displayName: "jobs") + + let testMessage1 = TestMessage(text: "Hi", sender: sender1, messageId: "asdf") + let testMessage2 = TestMessage(text: "sup", sender: sender2, messageId: "dddf") + + override func viewDidLoad() { + super.viewDidLoad() + messageList = [testMessage1, testMessage2] + self.messagesCollectionView.messagesDisplayDataSource = self + } +} + +// - MARK: MessagesDataSource conformace +extension TestMessagesViewControllerModel: MessagesDataSource { + + func currentSender() -> Sender { + return Sender(id: "1", displayName: "Dan") + } + + func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { + return messageList.count + } + + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { + return messageList[indexPath.section] + } +} From 84a973608bad021c4ecd09e15f8a5c5e1010d08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCng=C3=B6r?= Date: Mon, 14 Aug 2017 15:55:42 -0700 Subject: [PATCH 03/27] Bug: Collection View Content Size Issue #33 is fixed. (#54) --- Sources/MessagesViewController.swift | 61 +++++++++++++++------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 1d0eda26..5b9dc638 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -222,37 +222,40 @@ extension MessagesViewController: UICollectionViewDataSource { // MARK: - Keyboard Handling extension MessagesViewController { - - fileprivate func addKeyboardObservers() { - - NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: .UIKeyboardWillHide, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil) - + + fileprivate func addKeyboardObservers() { + + NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: .UIKeyboardWillHide, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillShow), name: .UIKeyboardWillShow, object: nil) + } - + fileprivate func removeKeyboardObservers() { - NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil) - NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) - + NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil) } - - func handleKeyboardWillHide(_ notification: Notification) { - - messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - - } - - func handleKeyboardWillChangeFrame(_ notification: Notification) { - - guard let keyboardSizeValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return } - - let keyboardRect = keyboardSizeValue.cgRectValue - let messageInputBarHeight = inputAccessoryView?.bounds.size.height ?? 0 - let keyboardHeight = keyboardRect.height - messageInputBarHeight - print(keyboardHeight) - messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) - - } - + + func handleKeyboardWillShow(_ notification: Notification) { + guard let keyboardEndSizeValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return } + + let keyboardFrame = keyboardEndSizeValue.cgRectValue + let keyboard = self.view.convert(keyboardFrame, from: self.view.window) + let height = self.view.frame.size.height + let messageInputBarHeight = inputAccessoryView?.bounds.size.height ?? 0 + let keyboardHeight = keyboardFrame.height - messageInputBarHeight + + if (keyboard.origin.y + keyboard.size.height) > height { + // Hardware keyboard is found + messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + + } else { + // Software keyboard is found + messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) + } + } + + func handleKeyboardWillHide(_ notification: Notification) { + messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } + } From dca9c4d2ec7e506ca6d90422b411baad60832b4a Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 14 Aug 2017 17:10:25 -0600 Subject: [PATCH 04/27] Adding bloat just for fun. --- Sources/MessagesDisplayDataSource.swift | 12 ++++++------ Sources/MessagesViewController.swift | 6 +++--- Tests/MessagesDisplayDataSourceTests.swift | 17 +++++++---------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index f3c980af..4b509bf8 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -26,11 +26,11 @@ import Foundation public protocol MessagesDisplayDataSource: class, MessagesDataSource { - func textColor(for message: MessageType, at indexPath: IndexPath) -> UIColor + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - func backgroundColor(for message: MessageType, at indexPath: IndexPath) -> UIColor + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - func avatar(for message: MessageType) -> Avatar + func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? @@ -45,15 +45,15 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { // "where Self: MessagesViewController" make these methods a `default` implimentation for any view that subclasses of MessagesViewController public extension MessagesDisplayDataSource where Self: MessagesViewController { - func textColor(for message: MessageType, at indexPath: IndexPath) -> UIColor { + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .white : .black } - func backgroundColor(for message: MessageType, at indexPath: IndexPath) -> UIColor { + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray } - func avatar(for message: MessageType) -> Avatar { + func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { return Avatar() } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 5183e1f7..406ced0d 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -165,9 +165,9 @@ extension MessagesViewController: UICollectionViewDataSource { guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath) - let textColor = displayDataSource.textColor(for: message, at: indexPath) - let avatar = displayDataSource.avatar(for: message) + let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) + let textColor = displayDataSource.textColor(for: message, at: indexPath, in: messagesCollectionView) + let avatar = displayDataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) diff --git a/Tests/MessagesDisplayDataSourceTests.swift b/Tests/MessagesDisplayDataSourceTests.swift index ce65adeb..7f7ccfe5 100644 --- a/Tests/MessagesDisplayDataSourceTests.swift +++ b/Tests/MessagesDisplayDataSourceTests.swift @@ -28,22 +28,19 @@ class MessagesDisplayDataSourceTests: XCTestCase { } func testMessageTextColorDefaultState() { - XCTAssertEqual(testClass.textColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.white) - XCTAssertEqual(testClass.textColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.black) + XCTAssertEqual(testClass.textColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView), UIColor.white) + XCTAssertEqual(testClass.textColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0), in: testClass.messagesCollectionView), UIColor.black) } func testBackGroundColorDefaultState() { - XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.outgoingGreen) - XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0)), UIColor.incomingGray) - XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.incomingGray) - XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0)), UIColor.outgoingGreen) + XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView), UIColor.outgoingGreen) + XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView), UIColor.incomingGray) + XCTAssertEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0), in: testClass.messagesCollectionView), UIColor.incomingGray) + XCTAssertNotEqual(testClass.backgroundColor(for: testClass.messageList[1], at: IndexPath(item: 1, section: 0), in: testClass.messagesCollectionView), UIColor.outgoingGreen) } func testAvatarDefaultState() { - XCTAssertNotNil(testClass.avatar(for: testClass.messageList[0]).initals) - XCTAssertNotNil(testClass.avatar(for: testClass.messageList[1]).initals) - XCTAssertEqual(testClass.avatar(for: testClass.messageList[0]).initals, "?") - XCTAssertEqual(testClass.avatar(for: testClass.messageList[1]).initals, "?") + XCTAssertNotNil(testClass.avatar(for: testClass.messageList[0], at: IndexPath(item: 0, section: 0), in: testClass.messagesCollectionView).initals) } func testCellTopLabelDefaultState() { From 156b2abdd378d1067bc3157c5e32ffd91d744172 Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 14 Aug 2017 17:33:54 -0600 Subject: [PATCH 05/27] adding code review suggestions. --- Sources/MessagesCollectionView.swift | 2 -- Tests/TestMessagesViewControllerModel.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/MessagesCollectionView.swift b/Sources/MessagesCollectionView.swift index 2cabe8cc..ea60e5ce 100644 --- a/Sources/MessagesCollectionView.swift +++ b/Sources/MessagesCollectionView.swift @@ -33,8 +33,6 @@ open class MessagesCollectionView: UICollectionView { open weak var messagesLayoutDelegate: MessagesLayoutDelegate? open weak var messageCellDelegate: MessageCellDelegate? - - open weak var messagesDisplayDataSource: MessagesDisplayDataSource? private var indexPathForLastItem: IndexPath? { diff --git a/Tests/TestMessagesViewControllerModel.swift b/Tests/TestMessagesViewControllerModel.swift index 010a435a..d81be895 100644 --- a/Tests/TestMessagesViewControllerModel.swift +++ b/Tests/TestMessagesViewControllerModel.swift @@ -21,7 +21,7 @@ class TestMessagesViewControllerModel: MessagesViewController, MessagesDisplayDa override func viewDidLoad() { super.viewDidLoad() messageList = [testMessage1, testMessage2] - self.messagesCollectionView.messagesDisplayDataSource = self + self.messagesCollectionView.messagesDataSource = self } } From f8f14dc8ea37e2ba8481af1433552da3b5921640 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 15 Aug 2017 00:47:31 -0500 Subject: [PATCH 06/27] Fix estimated width layout bug by rounding up --- Sources/MessagesCollectionViewFlowLayout.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 8e578c0d..275fd3c0 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -294,12 +294,12 @@ extension MessagesCollectionViewFlowLayout { switch message.data { case .text(let text): - estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont) + estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont).rounded(.up) case .attributedText(let text): - estimatedWidth = text.width(considering: containerHeight) + estimatedWidth = text.width(considering: containerHeight).rounded(.up) } - let widthToUse = estimatedWidth.rounded(.up) > availableWidth ? availableWidth : estimatedWidth + let widthToUse = estimatedWidth > availableWidth ? availableWidth : estimatedWidth let finalWidth = widthToUse + horizontalMessageInsets From 5ca8b9e6df8b3e1825a1e162e37f6859118a5457 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 15 Aug 2017 02:13:55 -0500 Subject: [PATCH 07/27] Use placeHolder UILabel to determined NSAttributedString size --- MessageKit.xcodeproj/project.pbxproj | 4 -- .../MessagesCollectionViewFlowLayout.swift | 20 ++++++--- ...ssagesCollectionViewLayoutAttributes.swift | 2 +- Sources/MessagesViewController.swift | 2 - Sources/NSAttributedString+Extensions.swift | 45 ------------------- 5 files changed, 15 insertions(+), 58 deletions(-) delete mode 100644 Sources/NSAttributedString+Extensions.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 42f3fefe..cc2b61fc 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -31,7 +31,6 @@ B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */; }; B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */; }; B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */; }; - B074EEAA1F3BE8F500ABB8C8 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */; }; B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09643851F286C9E004D0129 /* String+Extensions.swift */; }; B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */; }; B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; @@ -75,7 +74,6 @@ B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = ""; }; B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; - B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = ""; }; B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; @@ -147,7 +145,6 @@ children = ( B09643851F286C9E004D0129 /* String+Extensions.swift */, B096438F1F289142004D0129 /* UIColor+Extensions.swift */, - B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */, ); name = Extensions; sourceTree = ""; @@ -344,7 +341,6 @@ buildActionMask = 2147483647; files = ( 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */, - B074EEAA1F3BE8F500ABB8C8 /* NSAttributedString+Extensions.swift in Sources */, B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */, 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */, 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */, diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 275fd3c0..93dd2913 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -41,6 +41,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var incomingAvatarSize: CGSize open var outgoingAvatarSize: CGSize + fileprivate var placeholderLabel = MessageLabel() + fileprivate var avatarBottomPadding: CGFloat = 2 fileprivate var avatarMessagePadding: CGFloat = 4 @@ -230,9 +232,11 @@ extension MessagesCollectionViewFlowLayout { guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } guard let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) else { return 0 } - let availableWidth = cellTopLabelWidth(for: message) + let availableWidth = cellTopLabelWidth(for: message) - cellTopLabelInsets.left - cellTopLabelInsets.right - let estimatedHeight = topLabelText.height(considering: availableWidth) + placeholderLabel.attributedText = topLabelText + + let estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height return estimatedHeight.rounded(.up) @@ -244,9 +248,11 @@ extension MessagesCollectionViewFlowLayout { guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } guard let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) else { return 0 } - let availableWidth = cellBottomLabelWidth(for: message) + let availableWidth = cellBottomLabelWidth(for: message) - cellBottomLabelInsets.left - cellBottomLabelInsets.right - let estimatedHeight = bottomLabelText.height(considering: availableWidth) + placeholderLabel.attributedText = bottomLabelText + + let estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height return estimatedHeight.rounded(.up) } @@ -274,7 +280,8 @@ extension MessagesCollectionViewFlowLayout { case .text(let text): estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) case .attributedText(let text): - estimatedHeight = text.height(considering: availableWidth) + placeholderLabel.attributedText = text + estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height } let finalHeight = estimatedHeight.rounded(.up) + verticalMessageInsets @@ -296,7 +303,8 @@ extension MessagesCollectionViewFlowLayout { case .text(let text): estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont).rounded(.up) case .attributedText(let text): - estimatedWidth = text.width(considering: containerHeight).rounded(.up) + placeholderLabel.attributedText = text + estimatedWidth = placeholderLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: containerHeight)).width.rounded(.up) } let widthToUse = estimatedWidth > availableWidth ? availableWidth : estimatedWidth diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 462992f7..4ddfacc0 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -42,7 +42,7 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib var bottomLabelExtendsPastAvatar = false var avatarSize: CGSize = CGSize(width: 30, height: 30) - var avatarBottomPadding: CGFloat = 4.0 + var avatarBottomPadding: CGFloat = 2.0 var avatarMessagePadding: CGFloat = 4.0 var direction: MessageDirection = .incoming diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 5b9dc638..2f3b9856 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -121,8 +121,6 @@ open class MessagesViewController: UIViewController { // MARK: - UICollectionViewDelegate & UICollectionViewDelegateFlowLayout Conformance -//swiftlint:disable line_length - extension MessagesViewController: UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { diff --git a/Sources/NSAttributedString+Extensions.swift b/Sources/NSAttributedString+Extensions.swift deleted file mode 100644 index 316d9540..00000000 --- a/Sources/NSAttributedString+Extensions.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - MIT License - - Copyright (c) 2017 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -extension NSAttributedString { - - func height(considering width: CGFloat) -> CGFloat { - - let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) - return rect.height - - } - - func width(considering height: CGFloat) -> CGFloat { - - let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) - return rect.width - - } - -} From 92be0af82c59dbe4d618748fbc3d8881e9520ffc Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 15 Aug 2017 03:27:16 -0500 Subject: [PATCH 08/27] Add delegate methods for cellToplabel and cellBottomLabel tap recognition --- Example/Sources/ConversationViewController.swift | 8 ++++++++ Sources/MessageCellDelegate.swift | 8 ++++++++ Sources/MessageCollectionViewCell.swift | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 2015e2c6..854f0e0d 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -114,6 +114,14 @@ extension ConversationViewController: MessageCellDelegate { print("Message tapped") } + func didTapTopLabel(in cell: MessageCollectionViewCell) { + print("Top label tapped") + } + + func didTapBottomLabel(in cell: MessageCollectionViewCell) { + print("Bottom label tapped") + } + } // MARK: - MessageInputBarDelegate diff --git a/Sources/MessageCellDelegate.swift b/Sources/MessageCellDelegate.swift index 3b8a56d9..b09ee895 100644 --- a/Sources/MessageCellDelegate.swift +++ b/Sources/MessageCellDelegate.swift @@ -30,6 +30,10 @@ public protocol MessageCellDelegate: class { func didTapAvatar(in cell: MessageCollectionViewCell) + func didTapBottomLabel(in cell: MessageCollectionViewCell) + + func didTapTopLabel(in cell: MessageCollectionViewCell) + } extension MessageCellDelegate { @@ -38,4 +42,8 @@ extension MessageCellDelegate { func didTapAvatar(in cell: MessageCollectionViewCell) {} + func didTapBottomLabel(in cell: MessageCollectionViewCell) {} + + func didTapTopLabel(in cell: MessageCollectionViewCell) {} + } diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 6ba295c3..9f18208b 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -177,6 +177,14 @@ open class MessageCollectionViewCell: UICollectionViewCell { let messageTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapMessage)) messageContainerView.addGestureRecognizer(messageTapGesture) + let topLabelTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTopLabel)) + cellTopLabel.addGestureRecognizer(topLabelTapGesture) + cellTopLabel.isUserInteractionEnabled = true + + let bottomlabelTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBottomLabel)) + cellBottomLabel.addGestureRecognizer(bottomlabelTapGesture) + cellBottomLabel.isUserInteractionEnabled = true + } // MARK: - Delegate Methods @@ -188,4 +196,12 @@ open class MessageCollectionViewCell: UICollectionViewCell { func didTapMessage() { delegate?.didTapMessage(in: self) } + + func didTapTopLabel() { + delegate?.didTapTopLabel(in: self) + } + + func didTapBottomLabel() { + delegate?.didTapBottomLabel(in: self) + } } From a98b512d34c71ec627f5db7664fd0e1c2161cd37 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 15 Aug 2017 04:08:44 -0500 Subject: [PATCH 09/27] Add AvatarPosition model to customize Avatar position in cell --- .../Sources/ConversationViewController.swift | 4 +++ MessageKit.xcodeproj/project.pbxproj | 4 +++ Sources/AvatarPosition.swift | 33 +++++++++++++++++++ Sources/MessageCollectionViewCell.swift | 20 +++++++---- .../MessagesCollectionViewFlowLayout.swift | 11 ++++--- ...ssagesCollectionViewLayoutAttributes.swift | 4 +-- Sources/MessagesDisplayDataSource.swift | 6 ++++ 7 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 Sources/AvatarPosition.swift diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 854f0e0d..a8b37052 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -66,6 +66,10 @@ extension ConversationViewController: MessagesDisplayDataSource { return SampleData().getAvatarFor(sender: message.sender) } + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { + return .cellTop + } + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { return messagesCollectionView.dequeueMessageHeaderView(for: indexPath) } diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index cc2b61fc..ce72cecc 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09643851F286C9E004D0129 /* String+Extensions.swift */; }; B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */; }; B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; + B0AA1F511F42E91A00BAE583 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -77,6 +78,7 @@ B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = ""; }; B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -156,6 +158,7 @@ B0655A2B1F23D81600542A83 /* MessageData.swift */, 37C936971F38F6AC00853DF2 /* Avatar.swift */, B0655A271F23D71400542A83 /* MessageDirection.swift */, + B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */, ); name = Models; sourceTree = ""; @@ -359,6 +362,7 @@ B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */, 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */, 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, + B0AA1F511F42E91A00BAE583 /* AvatarPosition.swift in Sources */, B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */, B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */, diff --git a/Sources/AvatarPosition.swift b/Sources/AvatarPosition.swift new file mode 100644 index 00000000..bbb91494 --- /dev/null +++ b/Sources/AvatarPosition.swift @@ -0,0 +1,33 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +public enum AvatarPosition { + + case cellTop + case messageCenter + case cellBottom + +} diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 9f18208b..54f58b03 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -102,7 +102,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = .zero - if !attributes.topLabelExtendsPastAvatar { + if attributes.topLabelExtendsPastAvatar == false { origin = CGPoint(x: attributes.avatarSize.width + attributes.avatarMessagePadding, y: 0) } @@ -113,7 +113,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = CGPoint(x: 0, y: contentView.frame.height - attributes.cellBottomLabelSize.height) - if !attributes.bottomLabelExtendsPastAvatar { + if attributes.bottomLabelExtendsPastAvatar == false { origin.x = attributes.avatarSize.width + attributes.avatarMessagePadding } @@ -143,14 +143,22 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = .zero - let yPosition = contentView.frame.height - attributes.avatarSize.height - attributes.avatarBottomPadding - attributes.cellBottomLabelSize.height + switch attributes.avatarPosition { + case .cellTop: + origin.y = attributes.cellTopLabelSize.height + case .messageCenter: + let messageMidY = (attributes.messageContainerSize.height / 2) + let avatarMidY = (attributes.avatarSize.height / 2) + origin.y = contentView.frame.height - attributes.cellTopLabelSize.height - messageMidY - avatarMidY + case .cellBottom: + origin.y = contentView.frame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height + } switch attributes.direction { case .outgoing: - let xPosition = contentView.frame.width - attributes.avatarSize.width - origin = CGPoint(x: xPosition, y: yPosition) + origin.x = contentView.frame.width - attributes.avatarSize.width case .incoming: - origin = CGPoint(x: 0, y: yPosition) + origin.x = 0 } return CGRect(origin: origin, size: attributes.avatarSize) diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 93dd2913..0e4988cc 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -43,7 +43,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { fileprivate var placeholderLabel = MessageLabel() - fileprivate var avatarBottomPadding: CGFloat = 2 + //fileprivate var avatarBottomPadding: CGFloat = 2 fileprivate var avatarMessagePadding: CGFloat = 4 fileprivate var messagesCollectionView: MessagesCollectionView? { @@ -116,6 +116,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { guard let messagesCollectionView = messagesCollectionView else { return } guard let dataSource = messagesCollectionView.messagesDataSource else { return } + guard let displayDataSource = dataSource as? MessagesDisplayDataSource else { return } let indexPath = attributes.indexPath @@ -135,9 +136,11 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { attributes.bottomLabelExtendsPastAvatar = bottomLabelExtendsPastAvatar attributes.avatarSize = avatarSize(for: message) - attributes.avatarBottomPadding = avatarBottomPadding attributes.avatarMessagePadding = avatarMessagePadding + let avatarPosition = displayDataSource.avatarPosition(for: message, at: indexPath, in: messagesCollectionView) + attributes.avatarPosition = avatarPosition + attributes.direction = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming } @@ -260,11 +263,11 @@ extension MessagesCollectionViewFlowLayout { private func minimumCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { let size = avatarSize(for: message) - let avatarHeightPlusBottomPadding = size.height == 0 ? 0 : size.height + avatarBottomPadding + let avatarHeight = size.height let bottomLabelHeight = cellBottomLabelHeight(for: message, at: indexPath) let topLabelHeight = cellTopLabelHeight(for: message, at: indexPath) - let minimumHeight = topLabelHeight + avatarHeightPlusBottomPadding + bottomLabelHeight + let minimumHeight = topLabelHeight + avatarHeight + bottomLabelHeight return minimumHeight diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 4ddfacc0..4602a3c5 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -42,7 +42,7 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib var bottomLabelExtendsPastAvatar = false var avatarSize: CGSize = CGSize(width: 30, height: 30) - var avatarBottomPadding: CGFloat = 2.0 + var avatarPosition: AvatarPosition = .cellBottom var avatarMessagePadding: CGFloat = 4.0 var direction: MessageDirection = .incoming @@ -63,7 +63,7 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib copy.cellBottomLabelInsets = cellBottomLabelInsets copy.bottomLabelExtendsPastAvatar = bottomLabelExtendsPastAvatar copy.avatarSize = avatarSize - copy.avatarBottomPadding = avatarBottomPadding + copy.avatarPosition = avatarPosition copy.avatarMessagePadding = avatarMessagePadding copy.direction = direction return copy diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index cafc2431..14b9a72b 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -30,6 +30,8 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition + func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? @@ -42,6 +44,10 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { public extension MessagesDisplayDataSource { + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { + return .cellBottom + } + func messageColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray } From 80060909843417bfd65dd5db29134816f1abd30a Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 15 Aug 2017 04:25:46 -0500 Subject: [PATCH 10/27] Add messageTop and messageBottom AvatarPositions --- Example/Sources/ConversationViewController.swift | 2 +- Sources/AvatarPosition.swift | 2 ++ Sources/MessageCollectionViewCell.swift | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index a8b37052..a877e644 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -67,7 +67,7 @@ extension ConversationViewController: MessagesDisplayDataSource { } func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - return .cellTop + return .messageTop } func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { diff --git a/Sources/AvatarPosition.swift b/Sources/AvatarPosition.swift index bbb91494..d28d7a01 100644 --- a/Sources/AvatarPosition.swift +++ b/Sources/AvatarPosition.swift @@ -27,7 +27,9 @@ import Foundation public enum AvatarPosition { case cellTop + case messageTop case messageCenter + case messageBottom case cellBottom } diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 54f58b03..3cd7ae47 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -145,13 +145,25 @@ open class MessageCollectionViewCell: UICollectionViewCell { switch attributes.avatarPosition { case .cellTop: + if attributes.topLabelExtendsPastAvatar { + origin.y = attributes.cellTopLabelSize.height + } else { + origin.y = 0 + } + case .cellBottom: + if attributes.bottomLabelExtendsPastAvatar { + origin.y = contentView.frame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height + } else { + origin.y = contentView.frame.height - attributes.avatarSize.height + } + case .messageTop: origin.y = attributes.cellTopLabelSize.height + case .messageBottom: + origin.y = contentView.frame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height case .messageCenter: let messageMidY = (attributes.messageContainerSize.height / 2) let avatarMidY = (attributes.avatarSize.height / 2) origin.y = contentView.frame.height - attributes.cellTopLabelSize.height - messageMidY - avatarMidY - case .cellBottom: - origin.y = contentView.frame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height } switch attributes.direction { From 7e28572e19be1f007f096ad4bcafb14896754c49 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 16 Aug 2017 04:55:18 -0500 Subject: [PATCH 11/27] Add back NSAttributedString extension for calculations and remove temporary UILabel --- Example/Sources/SampleData.swift | 6 ++- MessageKit.xcodeproj/project.pbxproj | 4 ++ .../MessagesCollectionViewFlowLayout.swift | 17 ++----- Sources/NSAttributedString+Extensions.swift | 44 +++++++++++++++++++ 4 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 Sources/NSAttributedString+Extensions.swift diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index c7dccf8e..25c4f50e 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -39,9 +39,11 @@ struct SampleData { let msg6 = MockMessage(text: "I think if you do something and it turns out pretty good, then you should go do something else wonderful, not dwell on it for too long. Just figure out what’s next.", sender: Jobs, messageId: UUID().uuidString) let msg7 = MockMessage(text: "Remembering that I'll be dead soon is the most important tool I've ever encountered to help me make the big choices in life. Because almost everything - all external expectations, all pride, all fear of embarrassment or failure - these things just fall away in the face of death, leaving only what is truly important.", sender: Jobs, messageId: UUID().uuidString) let msg8 = MockMessage(text: "Price is rarely the most important thing. A cheap product might sell some units. Somebody gets it home and they feel great when they pay the money, but then they get it home and use it and the joy is gone.", sender: Cook, messageId: UUID().uuidString) - - let msg9Text = NSString(string: "Use .attributedText() to add bold, italic, colored text and more...") + + let msg9String = "Use .attributedText() to add bold, italic, colored text and more..." + let msg9Text = NSString(string: msg9String) let msg9AttributedText = NSMutableAttributedString(string: String(msg9Text)) + msg9AttributedText.addAttribute(NSFontAttributeName, value: UIFont.preferredFont(forTextStyle: .body), range: NSRange(location: 0, length: msg9Text.length)) if #available(iOS 9.0, *) { msg9AttributedText.addAttributes([NSFontAttributeName: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFontWeightBold)], range: msg9Text.range(of: ".attributedText()")) diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index ce72cecc..8d349a12 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */; }; B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; B0AA1F511F42E91A00BAE583 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */; }; + B0AA1F531F44388C00BAE583 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -79,6 +80,7 @@ B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = ""; }; B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; + B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -147,6 +149,7 @@ children = ( B09643851F286C9E004D0129 /* String+Extensions.swift */, B096438F1F289142004D0129 /* UIColor+Extensions.swift */, + B0AA1F521F44388900BAE583 /* NSAttributedString+Extensions.swift */, ); name = Extensions; sourceTree = ""; @@ -356,6 +359,7 @@ B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */, B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */, + B0AA1F531F44388C00BAE583 /* NSAttributedString+Extensions.swift in Sources */, B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */, B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */, B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */, diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 0e4988cc..17e32ec6 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -41,9 +41,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var incomingAvatarSize: CGSize open var outgoingAvatarSize: CGSize - fileprivate var placeholderLabel = MessageLabel() - - //fileprivate var avatarBottomPadding: CGFloat = 2 fileprivate var avatarMessagePadding: CGFloat = 4 fileprivate var messagesCollectionView: MessagesCollectionView? { @@ -237,9 +234,7 @@ extension MessagesCollectionViewFlowLayout { let availableWidth = cellTopLabelWidth(for: message) - cellTopLabelInsets.left - cellTopLabelInsets.right - placeholderLabel.attributedText = topLabelText - - let estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height + let estimatedHeight = topLabelText.height(considering: availableWidth) return estimatedHeight.rounded(.up) @@ -253,9 +248,7 @@ extension MessagesCollectionViewFlowLayout { let availableWidth = cellBottomLabelWidth(for: message) - cellBottomLabelInsets.left - cellBottomLabelInsets.right - placeholderLabel.attributedText = bottomLabelText - - let estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height + let estimatedHeight = bottomLabelText.height(considering: availableWidth) return estimatedHeight.rounded(.up) } @@ -283,8 +276,7 @@ extension MessagesCollectionViewFlowLayout { case .text(let text): estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) case .attributedText(let text): - placeholderLabel.attributedText = text - estimatedHeight = placeholderLabel.sizeThatFits(CGSize(width: availableWidth, height: .greatestFiniteMagnitude)).height + estimatedHeight = text.height(considering: availableWidth) } let finalHeight = estimatedHeight.rounded(.up) + verticalMessageInsets @@ -306,8 +298,7 @@ extension MessagesCollectionViewFlowLayout { case .text(let text): estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont).rounded(.up) case .attributedText(let text): - placeholderLabel.attributedText = text - estimatedWidth = placeholderLabel.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: containerHeight)).width.rounded(.up) + estimatedWidth = text.width(considering: containerHeight).rounded(.up) } let widthToUse = estimatedWidth > availableWidth ? availableWidth : estimatedWidth diff --git a/Sources/NSAttributedString+Extensions.swift b/Sources/NSAttributedString+Extensions.swift new file mode 100644 index 00000000..4b239062 --- /dev/null +++ b/Sources/NSAttributedString+Extensions.swift @@ -0,0 +1,44 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +extension NSAttributedString { + + func height(considering width: CGFloat) -> CGFloat { + + let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) + return rect.height + + } + + func width(considering height: CGFloat) -> CGFloat { + + let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) + return rect.width + + } +} From ea5af9cff6007b46c7a9847dfc712d9d7acbb46c Mon Sep 17 00:00:00 2001 From: Andrew Lauder Date: Wed, 16 Aug 2017 10:56:14 -0700 Subject: [PATCH 12/27] Fix for Crash issue when no message #61 --- Sources/MessagesCollectionView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MessagesCollectionView.swift b/Sources/MessagesCollectionView.swift index ea60e5ce..277a8c99 100644 --- a/Sources/MessagesCollectionView.swift +++ b/Sources/MessagesCollectionView.swift @@ -37,7 +37,7 @@ open class MessagesCollectionView: UICollectionView { private var indexPathForLastItem: IndexPath? { let lastSection = numberOfSections > 0 ? numberOfSections - 1 : 0 - guard numberOfItems(inSection: lastSection) > 0 else { return nil } + guard lastSection > 0, numberOfItems(inSection: lastSection) > 0 else { return nil } return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) } From 1b6d480c7ab477886d67a1c2ce343b453452b000 Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Fri, 18 Aug 2017 13:12:38 -0600 Subject: [PATCH 13/27] Fixed the headers. --- Sources/MessagesViewController.swift | 43 +++++++++++---------- Tests/MessagesDisplayDataSourceTests.swift | 31 +++++++++++---- Tests/TestMessageModel.swift | 32 +++++++++++---- Tests/TestMessagesViewControllerModel.swift | 34 ++++++++++++---- 4 files changed, 97 insertions(+), 43 deletions(-) diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 7863f5b8..aa874e60 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -1,26 +1,27 @@ /* -MIT License + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ -Copyright (c) 2017 MessageKit - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ import UIKit diff --git a/Tests/MessagesDisplayDataSourceTests.swift b/Tests/MessagesDisplayDataSourceTests.swift index 7f7ccfe5..cd53ef3c 100644 --- a/Tests/MessagesDisplayDataSourceTests.swift +++ b/Tests/MessagesDisplayDataSourceTests.swift @@ -1,10 +1,27 @@ -// -// MessagesDisplayDataSourceTests.swift -// MessageKit -// -// Created by Dan Leonard on 8/14/17. -// Copyright © 2017 MessageKit. All rights reserved. -// +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + import XCTest @testable import MessageKit diff --git a/Tests/TestMessageModel.swift b/Tests/TestMessageModel.swift index 35d701c5..e5dc3ad2 100644 --- a/Tests/TestMessageModel.swift +++ b/Tests/TestMessageModel.swift @@ -1,10 +1,28 @@ -// -// TestMessageModel.swift -// MessageKit -// -// Created by Dan Leonard on 8/14/17. -// Copyright © 2017 MessageKit. All rights reserved. -// +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + + import Foundation struct TestMessage: MessageType { diff --git a/Tests/TestMessagesViewControllerModel.swift b/Tests/TestMessagesViewControllerModel.swift index d81be895..7885ecb3 100644 --- a/Tests/TestMessagesViewControllerModel.swift +++ b/Tests/TestMessagesViewControllerModel.swift @@ -1,13 +1,31 @@ -// -// TestMessagesViewControllerModel.swift -// MessageKit -// -// Created by Dan Leonard on 8/14/17. -// Copyright © 2017 MessageKit. All rights reserved. -// +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + + import Foundation -import Foundation + class TestMessagesViewControllerModel: MessagesViewController, MessagesDisplayDataSource { var messageList: [TestMessage] = [] From efa9a5c753eddef59f5d95547aa21f60c1bf4292 Mon Sep 17 00:00:00 2001 From: Bojan Stefanovic Date: Fri, 18 Aug 2017 12:21:15 -0700 Subject: [PATCH 14/27] Fixed spelling mistake --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80f4b83f..310c756d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ If you're new to Open Source or Swift, the MessageKit community is a great place - Please check the [README](https://github.com/MessageKit/MessageKit/blob/master/README.md) to see if your question is answered there. - Search [open issues](https://github.com/MessageKit/MessageKit/issues?q=is%3Aopen+is%3Aissue) and [closed issues](https://github.com/MessageKit/MessageKit/issues?q=is%3Aissue+is%3Aclosed) to avoid opening a duplicate issue. - Avoiding duplicate issues organizes all relevant information for project maintainers and other users. -- If no issues represent your problem, please open a new issue with a good tile and useful description. +- If no issues represent your problem, please open a new issue with a good title and useful description. - Information to Provide When Opening an Issue: - MessageKit version(s) From 3b498b368c8c11c380b13396fabcdee1c0c5c289 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Sat, 19 Aug 2017 22:45:25 -0500 Subject: [PATCH 15/27] Add DetectorType --- Example/Sources/SampleData.swift | 5 +- MessageKit.xcodeproj/project.pbxproj | 4 + Sources/DetectorType.swift | 45 ++++ Sources/MessageLabel.swift | 223 +++++++++++++++++++- Sources/MessagesViewController.swift | 1 - Tests/TestMessageModel.swift | 3 +- Tests/TestMessagesViewControllerModel.swift | 2 - 7 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 Sources/DetectorType.swift diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index 25c4f50e..25266371 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -61,8 +61,11 @@ struct SampleData { range: msg9Text.range(of: "colored")) let msg9 = MockMessage(attributedText: msg9AttributedText, sender: Jobs, messageId: UUID().uuidString) + + let msg10 = MockMessage(text: "1-800-555-0000", sender: Steven, messageId: UUID().uuidString) + let msg11 = MockMessage(text: "One Infinite Loop Cupertino, CA 95014", sender: Cook, messageId: UUID().uuidString) - return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9] + return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11] } func getCurrentSender() -> Sender { diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index e2f62a2c..10ca5086 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */; }; B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */; }; B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */; }; + B05530B51F493CFA008BB420 /* DetectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05530B41F493CFA008BB420 /* DetectorType.swift */; }; B0655A281F23D71400542A83 /* MessageDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A271F23D71400542A83 /* MessageDirection.swift */; }; B0655A2A1F23D77200542A83 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A291F23D77200542A83 /* Sender.swift */; }; B0655A2C1F23D81600542A83 /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A2B1F23D81600542A83 /* MessageData.swift */; }; @@ -71,6 +72,7 @@ B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = ""; }; B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = ""; }; + B05530B41F493CFA008BB420 /* DetectorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectorType.swift; sourceTree = ""; }; B0655A271F23D71400542A83 /* MessageDirection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDirection.swift; sourceTree = ""; }; B0655A291F23D77200542A83 /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = ""; }; B0655A2B1F23D81600542A83 /* MessageData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = ""; }; @@ -179,6 +181,7 @@ 37C936971F38F6AC00853DF2 /* Avatar.swift */, B0655A271F23D71400542A83 /* MessageDirection.swift */, B0AA1F501F42E91A00BAE583 /* AvatarPosition.swift */, + B05530B41F493CFA008BB420 /* DetectorType.swift */, ); name = Models; sourceTree = ""; @@ -384,6 +387,7 @@ B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */, B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */, 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */, + B05530B51F493CFA008BB420 /* DetectorType.swift in Sources */, 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, B0AA1F511F42E91A00BAE583 /* AvatarPosition.swift in Sources */, B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, diff --git a/Sources/DetectorType.swift b/Sources/DetectorType.swift new file mode 100644 index 00000000..dc760e1b --- /dev/null +++ b/Sources/DetectorType.swift @@ -0,0 +1,45 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +public enum DetectorType { + + case address +// case date + case phoneNumber +// case url + +// case mention +// case hashtag +// case custom + + var textCheckingType: NSTextCheckingResult.CheckingType { + switch self { + case .address: return .address + case .phoneNumber: return .phoneNumber + } + } + +} diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index f46c5284..54718029 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -26,7 +26,82 @@ import UIKit open class MessageLabel: UILabel { - // MARK: - Properties + // MARK: - Private Properties + + private lazy var layoutManager: NSLayoutManager = { + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(self.textContainer) + return layoutManager + }() + + private lazy var textContainer: NSTextContainer = { + let textContainer = NSTextContainer() + textContainer.lineFragmentPadding = 0 + textContainer.maximumNumberOfLines = self.numberOfLines + textContainer.lineBreakMode = self.lineBreakMode + textContainer.size = self.bounds.size + return textContainer + }() + + private lazy var textStorage: NSTextStorage = { + let textStorage = NSTextStorage() + textStorage.addLayoutManager(self.layoutManager) + return textStorage + }() + + private lazy var activeDetectorRanges = [DetectorType: [NSRange]]() + + // MARK: - Public Properties + + // MARK: Text Properties + + open override var attributedText: NSAttributedString? { + didSet { + guard attributedText != oldValue else { return } + setTextStorage() + } + } + + open override var text: String? { + didSet { + guard text != oldValue else { return } + setTextStorage() + } + } + + open override var font: UIFont! { + didSet { + guard font != oldValue else { return } + guard let attributedText = attributedText else { return } + textStorage.setAttributedString(attributedText) + setNeedsDisplay() + } + } + + open override var textColor: UIColor! { + didSet { + guard textColor != oldValue else { return } + guard let attributedText = attributedText else { return } + textStorage.setAttributedString(attributedText) + setNeedsDisplay() + } + } + + open override var lineBreakMode: NSLineBreakMode { + didSet { + guard lineBreakMode != oldValue else { return } + textContainer.lineBreakMode = lineBreakMode + setNeedsDisplay() + } + } + + open override var numberOfLines: Int { + didSet { + guard numberOfLines != oldValue else { return } + textContainer.maximumNumberOfLines = numberOfLines + setNeedsDisplay() + } + } open var textInsets: UIEdgeInsets = .zero { didSet { @@ -35,19 +110,155 @@ open class MessageLabel: UILabel { } } + // MARK: DetectorType Properties + + open var enabledDetectors: [DetectorType] = [.phoneNumber, .address] + + open var addressAttributes: [String: Any] = [:] { + didSet { + updateAttributes(for: .address) + setNeedsDisplay() + } + } + + open var phoneNumberAttributes: [String: Any] = [:] { + didSet { + updateAttributes(for: .phoneNumber) + setNeedsDisplay() + } + } + + // MARK: - Initializers + public override init(frame: CGRect) { super.init(frame: frame) - numberOfLines = 0 + + // Message Label Specific + self.numberOfLines = 0 + self.lineBreakMode = .byWordWrapping + // End + + let defaultAttributes: [String: Any] = [ + NSForegroundColorAttributeName: self.textColor, + NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue, + NSUnderlineColorAttributeName: self.textColor + ] + + self.addressAttributes = defaultAttributes + self.phoneNumberAttributes = defaultAttributes + } - + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: - Methods + // MARK: - Public Methods - open override func draw(_ rect: CGRect) { - super.drawText(in: UIEdgeInsetsInsetRect(rect, textInsets)) + open override func drawText(in rect: CGRect) { + + let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + textContainer.size = CGSize(width: insetRect.width, height: rect.height) + + let origin = insetRect.origin + let range = layoutManager.glyphRange(for: textContainer) + + layoutManager.drawBackground(forGlyphRange: range, at: origin) + layoutManager.drawGlyphs(forGlyphRange: range, at: origin) + } + + // MARK: - Private Methods + + private func addDetectorAttributes(to text: NSAttributedString) -> NSMutableAttributedString { + guard let checkingResults = parse(text: text, for: enabledDetectors), checkingResults.isEmpty == false else { + return NSMutableAttributedString(attributedString: text) + } + + setRangesForActiveDetectors(from: checkingResults) + + let mutableAttributedString = NSMutableAttributedString(attributedString: text) + + checkingResults.forEach { result in + let detectorTypeAttributes = attributes(for: result.resultType) + mutableAttributedString.addAttributes(detectorTypeAttributes, range: result.range) + } + + return mutableAttributedString + } + + private func updateAttributes(for detectorType: DetectorType) { + guard let attributedText = attributedText, attributedText.length > 0 else { return } + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + + switch detectorType { + case .address: + guard let ranges = activeDetectorRanges[.address] else { return } + ranges.forEach { mutableAttributedString.addAttributes(addressAttributes, range: $0) } + case .phoneNumber: + guard let ranges = activeDetectorRanges[.phoneNumber] else { return } + ranges.forEach { mutableAttributedString.addAttributes(phoneNumberAttributes, range: $0) } + } + + textStorage.setAttributedString(mutableAttributedString) + + } + + // MARK: - Text Parsing + + private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? { + + let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue } + let detector = try? NSDataDetector(types: checkingTypes) + + return detector?.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length)) + } + + private func setTextStorage() { + + guard let attributedText = attributedText, attributedText.length > 0 else { + activeDetectorRanges.removeAll() + textStorage.setAttributedString(NSAttributedString()) + setNeedsDisplay() + return + } + + var mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) + + mutableAttributedString = addDetectorAttributes(to: mutableAttributedString) + + textStorage.setAttributedString(mutableAttributedString) + + setNeedsDisplay() + + } + + private func attributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [String: Any] { + switch checkingResultType { + case NSTextCheckingResult.CheckingType.address: + return addressAttributes + case NSTextCheckingResult.CheckingType.phoneNumber: + return phoneNumberAttributes + default: + fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") + } + } + + private func setRangesForActiveDetectors(from checkingResults: [NSTextCheckingResult]) { + checkingResults.forEach { result in + + switch result.resultType { + case NSTextCheckingResult.CheckingType.address: + var ranges = activeDetectorRanges[.address] ?? [] + ranges.append(result.range) + activeDetectorRanges.updateValue(ranges, forKey: .address) + case NSTextCheckingResult.CheckingType.phoneNumber: + var ranges = activeDetectorRanges[.phoneNumber] ?? [] + ranges.append(result.range) + activeDetectorRanges.updateValue(ranges, forKey: .phoneNumber) + default: + fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") + } + } } } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index aa874e60..5366cc1b 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -22,7 +22,6 @@ SOFTWARE. */ - import UIKit open class MessagesViewController: UIViewController { diff --git a/Tests/TestMessageModel.swift b/Tests/TestMessageModel.swift index e5dc3ad2..4bdef39e 100644 --- a/Tests/TestMessageModel.swift +++ b/Tests/TestMessageModel.swift @@ -22,9 +22,8 @@ SOFTWARE. */ - - import Foundation + struct TestMessage: MessageType { var messageId: String var sender: Sender diff --git a/Tests/TestMessagesViewControllerModel.swift b/Tests/TestMessagesViewControllerModel.swift index 7885ecb3..db8f0e4e 100644 --- a/Tests/TestMessagesViewControllerModel.swift +++ b/Tests/TestMessagesViewControllerModel.swift @@ -22,8 +22,6 @@ SOFTWARE. */ - - import Foundation class TestMessagesViewControllerModel: MessagesViewController, MessagesDisplayDataSource { From bd7e9f7581883bc38d789183d0bffb14d0ef8508 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 22 Aug 2017 02:04:58 -0500 Subject: [PATCH 16/27] Add url and date detectors --- Example/Sources/SampleData.swift | 6 ++- Sources/DetectorType.swift | 14 ++++--- Sources/MessageLabel.swift | 68 ++++++++++++++++++++++++++------ 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index 25266371..205fe576 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -63,9 +63,11 @@ struct SampleData { let msg9 = MockMessage(attributedText: msg9AttributedText, sender: Jobs, messageId: UUID().uuidString) let msg10 = MockMessage(text: "1-800-555-0000", sender: Steven, messageId: UUID().uuidString) - let msg11 = MockMessage(text: "One Infinite Loop Cupertino, CA 95014", sender: Cook, messageId: UUID().uuidString) + let msg11 = MockMessage(text: "One Infinite Loop Cupertino, CA 95014 this is some extra text to make sure it is not detected", sender: Cook, messageId: UUID().uuidString) + let msg12 = MockMessage(text: "This is Christmas Day 12-25-2000, will February 22nd be recognized? How about 11/11/2017? This Friday. Sunday, April 1st at 11:37pm ", sender: Steven, messageId: UUID().uuidString) + let msg13 = MockMessage(text: "https//:github.com/SD10", sender: Steven, messageId: UUID().uuidString) - return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11] + return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11, msg12, msg13] } func getCurrentSender() -> Sender { diff --git a/Sources/DetectorType.swift b/Sources/DetectorType.swift index dc760e1b..480faa59 100644 --- a/Sources/DetectorType.swift +++ b/Sources/DetectorType.swift @@ -27,18 +27,22 @@ import Foundation public enum DetectorType { case address -// case date + case date case phoneNumber -// case url + case url -// case mention -// case hashtag -// case custom + // MARK: - Not supported yet + + //case mention + //case hashtag + //case custom var textCheckingType: NSTextCheckingResult.CheckingType { switch self { case .address: return .address + case .date: return .date case .phoneNumber: return .phoneNumber + case .url: return .link } } diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index 54718029..c08ee4a8 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -112,7 +112,7 @@ open class MessageLabel: UILabel { // MARK: DetectorType Properties - open var enabledDetectors: [DetectorType] = [.phoneNumber, .address] + open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url] open var addressAttributes: [String: Any] = [:] { didSet { @@ -121,6 +121,13 @@ open class MessageLabel: UILabel { } } + open var dateAttributes: [String: Any] = [:] { + didSet { + updateAttributes(for: .date) + setNeedsDisplay() + } + } + open var phoneNumberAttributes: [String: Any] = [:] { didSet { updateAttributes(for: .phoneNumber) @@ -128,6 +135,13 @@ open class MessageLabel: UILabel { } } + open var urlAttributes: [String: Any] = [:] { + didSet { + updateAttributes(for: .url) + setNeedsDisplay() + } + } + // MARK: - Initializers public override init(frame: CGRect) { @@ -145,7 +159,9 @@ open class MessageLabel: UILabel { ] self.addressAttributes = defaultAttributes + self.dateAttributes = defaultAttributes self.phoneNumberAttributes = defaultAttributes + self.urlAttributes = defaultAttributes } @@ -170,14 +186,15 @@ open class MessageLabel: UILabel { // MARK: - Private Methods private func addDetectorAttributes(to text: NSAttributedString) -> NSMutableAttributedString { + + let mutableAttributedString = NSMutableAttributedString(attributedString: text) + guard let checkingResults = parse(text: text, for: enabledDetectors), checkingResults.isEmpty == false else { - return NSMutableAttributedString(attributedString: text) + return mutableAttributedString } setRangesForActiveDetectors(from: checkingResults) - let mutableAttributedString = NSMutableAttributedString(attributedString: text) - checkingResults.forEach { result in let detectorTypeAttributes = attributes(for: result.resultType) mutableAttributedString.addAttributes(detectorTypeAttributes, range: result.range) @@ -187,22 +204,36 @@ open class MessageLabel: UILabel { } private func updateAttributes(for detectorType: DetectorType) { + guard let attributedText = attributedText, attributedText.length > 0 else { return } let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - switch detectorType { - case .address: - guard let ranges = activeDetectorRanges[.address] else { return } - ranges.forEach { mutableAttributedString.addAttributes(addressAttributes, range: $0) } - case .phoneNumber: - guard let ranges = activeDetectorRanges[.phoneNumber] else { return } - ranges.forEach { mutableAttributedString.addAttributes(phoneNumberAttributes, range: $0) } + guard let ranges = activeDetectorRanges[detectorType] else { return } + + ranges.forEach { range in + let detectorAttributes = attributes(for: detectorType) + mutableAttributedString.addAttributes(detectorAttributes, range: range) } textStorage.setAttributedString(mutableAttributedString) } + private func attributes(for detectorType: DetectorType) -> [String: Any] { + + switch detectorType { + case .address: + return addressAttributes + case .date: + return dateAttributes + case .phoneNumber: + return phoneNumberAttributes + case .url: + return urlAttributes + } + + } + // MARK: - Text Parsing private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? { @@ -222,6 +253,8 @@ open class MessageLabel: UILabel { return } + activeDetectorRanges.removeAll() + var mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) mutableAttributedString = addDetectorAttributes(to: mutableAttributedString) @@ -236,14 +269,19 @@ open class MessageLabel: UILabel { switch checkingResultType { case NSTextCheckingResult.CheckingType.address: return addressAttributes + case NSTextCheckingResult.CheckingType.date: + return dateAttributes case NSTextCheckingResult.CheckingType.phoneNumber: return phoneNumberAttributes + case NSTextCheckingResult.CheckingType.link: + return urlAttributes default: fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") } } private func setRangesForActiveDetectors(from checkingResults: [NSTextCheckingResult]) { + checkingResults.forEach { result in switch result.resultType { @@ -251,10 +289,18 @@ open class MessageLabel: UILabel { var ranges = activeDetectorRanges[.address] ?? [] ranges.append(result.range) activeDetectorRanges.updateValue(ranges, forKey: .address) + case NSTextCheckingResult.CheckingType.date: + var ranges = activeDetectorRanges[.date] ?? [] + ranges.append(result.range) + activeDetectorRanges.updateValue(ranges, forKey: .date) case NSTextCheckingResult.CheckingType.phoneNumber: var ranges = activeDetectorRanges[.phoneNumber] ?? [] ranges.append(result.range) activeDetectorRanges.updateValue(ranges, forKey: .phoneNumber) + case NSTextCheckingResult.CheckingType.link: + var ranges = activeDetectorRanges[.url] ?? [] + ranges.append(result.range) + activeDetectorRanges.updateValue(ranges, forKey: .url) default: fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") } From 866a9b5ce624862d592ceaff9fc885d9492b86dc Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 22 Aug 2017 02:30:12 -0500 Subject: [PATCH 17/27] Fix empty data detector crash --- Example/Sources/SampleData.swift | 4 ++-- Sources/MessageCollectionViewCell.swift | 12 ++++++++++-- Sources/MessageLabel.swift | 6 +++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index 205fe576..8e022bbd 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -63,8 +63,8 @@ struct SampleData { let msg9 = MockMessage(attributedText: msg9AttributedText, sender: Jobs, messageId: UUID().uuidString) let msg10 = MockMessage(text: "1-800-555-0000", sender: Steven, messageId: UUID().uuidString) - let msg11 = MockMessage(text: "One Infinite Loop Cupertino, CA 95014 this is some extra text to make sure it is not detected", sender: Cook, messageId: UUID().uuidString) - let msg12 = MockMessage(text: "This is Christmas Day 12-25-2000, will February 22nd be recognized? How about 11/11/2017? This Friday. Sunday, April 1st at 11:37pm ", sender: Steven, messageId: UUID().uuidString) + let msg11 = MockMessage(text: "One Infinite Loop Cupertino, CA 95014 This is some extra text that should not be detected.", sender: Cook, messageId: UUID().uuidString) + let msg12 = MockMessage(text: "This is an example of the date detector 11/11/2017. April 1st is April Fools Day. Next Friday is not Friday the 13th.", sender: Steven, messageId: UUID().uuidString) let msg13 = MockMessage(text: "https//:github.com/SD10", sender: Steven, messageId: UUID().uuidString) return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10, msg11, msg12, msg13] diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 3cd7ae47..f2307b44 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -37,11 +37,19 @@ open class MessageCollectionViewCell: UICollectionViewCell { open var avatarView: AvatarView = AvatarView() - open var cellTopLabel: MessageLabel = MessageLabel() + open var cellTopLabel: MessageLabel = { + let topLabel = MessageLabel() + topLabel.enabledDetectors = [] + return topLabel + }() open var messageLabel: MessageLabel = MessageLabel() - open var cellBottomLabel: MessageLabel = MessageLabel() + open var cellBottomLabel: MessageLabel = { + let bottomLabel = MessageLabel() + bottomLabel.enabledDetectors = [] + return bottomLabel + }() open weak var delegate: MessageCellDelegate? diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index c08ee4a8..96291299 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -189,9 +189,9 @@ open class MessageLabel: UILabel { let mutableAttributedString = NSMutableAttributedString(attributedString: text) - guard let checkingResults = parse(text: text, for: enabledDetectors), checkingResults.isEmpty == false else { - return mutableAttributedString - } + guard enabledDetectors.isEmpty == false else { return mutableAttributedString } + guard let checkingResults = parse(text: text, for: enabledDetectors) else { return mutableAttributedString } + guard checkingResults.isEmpty == false else { return mutableAttributedString } setRangesForActiveDetectors(from: checkingResults) From e2fc3c6471073fc13b63a7e86059c824cce63c34 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 22 Aug 2017 02:48:50 -0500 Subject: [PATCH 18/27] Update MessageKit.podspec for v0.5.0 --- CHANGELOG.md | 8 ++++++++ MessageKit.podspec | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70178dbd..6ca2c911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release ---------------- +## [Prerelease] 0.5.0 + +This release closes the [0.5 milestone](https://github.com/MessageKit/MessageKit/milestone/5?closed=1). + +## [Prerelease] 0.4.0 + +This release closes the [0.4 milestone](https://github.com/MessageKit/MessageKit/milestone/4?closed=1). + ## [Prerelease] 0.3.0 This release closes the [0.3 milestone](https://github.com/MessageKit/MessageKit/milestone/3?closed=1). diff --git a/MessageKit.podspec b/MessageKit.podspec index faca6b81..7a245088 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '0.4.0' + s.version = '0.5.0' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.' From 09e325da3e58dc6e04391af09945d498ec83af0a Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 23 Aug 2017 23:05:19 -0500 Subject: [PATCH 19/27] Add MessageLabelDelegate --- MessageKit.xcodeproj/project.pbxproj | 4 + Sources/MessageLabel.swift | 225 +++++++++++++++------ Sources/MessageLabelDelegate.swift | 35 ++++ Tests/MessagesDisplayDataSourceTests.swift | 2 +- 4 files changed, 204 insertions(+), 62 deletions(-) create mode 100644 Sources/MessageLabelDelegate.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 10ca5086..75cecdf8 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 88916B401CF0DF5100469F91 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 88916B3E1CF0DF5100469F91 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 88916B451CF0DF5900469F91 /* MessageKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88916B431CF0DF5900469F91 /* MessageKitTests.swift */; }; 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88916B461CF0DFE600469F91 /* MessageType.swift */; }; + B01280F31F4E8798004BCD3E /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */; }; B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */; }; B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */; }; B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */; }; @@ -69,6 +70,7 @@ 88916B421CF0DF5900469F91 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88916B431CF0DF5900469F91 /* MessageKitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageKitTests.swift; sourceTree = ""; }; 88916B461CF0DFE600469F91 /* MessageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = ""; }; + B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = ""; }; B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = ""; }; B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = ""; }; @@ -210,6 +212,7 @@ B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */, B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */, B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */, + B01280F21F4E8798004BCD3E /* MessageLabelDelegate.swift */, ); name = Protocols; sourceTree = ""; @@ -375,6 +378,7 @@ B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */, B0655A281F23D71400542A83 /* MessageDirection.swift in Sources */, B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */, + B01280F31F4E8798004BCD3E /* MessageLabelDelegate.swift in Sources */, 376AD1881F4259D20083072A /* TestMessagesViewControllerModel.swift in Sources */, B0655A2A1F23D77200542A83 /* Sender.swift in Sources */, B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index 96291299..75921aef 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -24,7 +24,7 @@ import UIKit -open class MessageLabel: UILabel { +open class MessageLabel: UILabel, UIGestureRecognizerDelegate { // MARK: - Private Properties @@ -49,11 +49,13 @@ open class MessageLabel: UILabel { return textStorage }() - private lazy var activeDetectorRanges = [DetectorType: [NSRange]]() + private lazy var rangesForDetectors: [DetectorType: [NSRange]] = [:] // MARK: - Public Properties - // MARK: Text Properties + open var delegate: MessageLabelDelegate? + + open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url] open override var attributedText: NSAttributedString? { didSet { @@ -110,10 +112,6 @@ open class MessageLabel: UILabel { } } - // MARK: DetectorType Properties - - open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url] - open var addressAttributes: [String: Any] = [:] { didSet { updateAttributes(for: .address) @@ -150,7 +148,6 @@ open class MessageLabel: UILabel { // Message Label Specific self.numberOfLines = 0 self.lineBreakMode = .byWordWrapping - // End let defaultAttributes: [String: Any] = [ NSForegroundColorAttributeName: self.textColor, @@ -163,13 +160,15 @@ open class MessageLabel: UILabel { self.phoneNumberAttributes = defaultAttributes self.urlAttributes = defaultAttributes + setupGestureRecognizers() + } public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - // MARK: - Public Methods + // MARK: - Open Methods open override func drawText(in rect: CGRect) { @@ -183,9 +182,52 @@ open class MessageLabel: UILabel { layoutManager.drawGlyphs(forGlyphRange: range, at: origin) } + // MARK: - Public Methods + + // MARK: UIGestureRecognizer Delegate + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + + let touchLocation = touch.location(in: self) + guard let index = stringIndex(at: touchLocation) else { return false } + + for (_, ranges) in rangesForDetectors { + for nsRange in ranges { + guard let range = nsRange.toRange() else { return false } + if range.contains(index) { return true } + } + } + + return false + + } + // MARK: - Private Methods - private func addDetectorAttributes(to text: NSAttributedString) -> NSMutableAttributedString { + private func setTextStorage() { + + // Anytime we update the text storage we need to clear the previous ranges + rangesForDetectors.removeAll() + + guard let attributedText = attributedText, attributedText.length > 0 else { + textStorage.setAttributedString(NSAttributedString()) + setNeedsDisplay() + return + } + + let textWithDetectorAttributes = addDetectorAttributes(to: attributedText) + + textStorage.setAttributedString(textWithDetectorAttributes) + + setNeedsDisplay() + + } + + private func addDetectorAttributes(to text: NSAttributedString) -> NSAttributedString { let mutableAttributedString = NSMutableAttributedString(attributedString: text) @@ -193,11 +235,9 @@ open class MessageLabel: UILabel { guard let checkingResults = parse(text: text, for: enabledDetectors) else { return mutableAttributedString } guard checkingResults.isEmpty == false else { return mutableAttributedString } - setRangesForActiveDetectors(from: checkingResults) - checkingResults.forEach { result in - let detectorTypeAttributes = attributes(for: result.resultType) - mutableAttributedString.addAttributes(detectorTypeAttributes, range: result.range) + let attributes = detectorAttributes(for: result.resultType) + mutableAttributedString.addAttributes(attributes, range: result.range) } return mutableAttributedString @@ -208,18 +248,18 @@ open class MessageLabel: UILabel { guard let attributedText = attributedText, attributedText.length > 0 else { return } let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - guard let ranges = activeDetectorRanges[detectorType] else { return } + guard let ranges = rangesForDetectors[detectorType] else { return } ranges.forEach { range in - let detectorAttributes = attributes(for: detectorType) - mutableAttributedString.addAttributes(detectorAttributes, range: range) + let attributes = detectorAttributes(for: detectorType) + mutableAttributedString.addAttributes(attributes, range: range) } textStorage.setAttributedString(mutableAttributedString) - + } - private func attributes(for detectorType: DetectorType) -> [String: Any] { + private func detectorAttributes(for detectorType: DetectorType) -> [String: Any] { switch detectorType { case .address: @@ -234,38 +274,7 @@ open class MessageLabel: UILabel { } - // MARK: - Text Parsing - - private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? { - - let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue } - let detector = try? NSDataDetector(types: checkingTypes) - - return detector?.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length)) - } - - private func setTextStorage() { - - guard let attributedText = attributedText, attributedText.length > 0 else { - activeDetectorRanges.removeAll() - textStorage.setAttributedString(NSAttributedString()) - setNeedsDisplay() - return - } - - activeDetectorRanges.removeAll() - - var mutableAttributedString = NSMutableAttributedString(attributedString: attributedText) - - mutableAttributedString = addDetectorAttributes(to: mutableAttributedString) - - textStorage.setAttributedString(mutableAttributedString) - - setNeedsDisplay() - - } - - private func attributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [String: Any] { + private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [String: Any] { switch checkingResultType { case NSTextCheckingResult.CheckingType.address: return addressAttributes @@ -280,31 +289,125 @@ open class MessageLabel: UILabel { } } - private func setRangesForActiveDetectors(from checkingResults: [NSTextCheckingResult]) { + // MARK: - Parsing Text - checkingResults.forEach { result in + private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? { + + let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue } + let detector = try? NSDataDetector(types: checkingTypes) + + return detector?.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length)) + } + + private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) { + + for result in checkingResults { switch result.resultType { case NSTextCheckingResult.CheckingType.address: - var ranges = activeDetectorRanges[.address] ?? [] + var ranges = rangesForDetectors[.address] ?? [] ranges.append(result.range) - activeDetectorRanges.updateValue(ranges, forKey: .address) + rangesForDetectors.updateValue(ranges, forKey: .address) case NSTextCheckingResult.CheckingType.date: - var ranges = activeDetectorRanges[.date] ?? [] + var ranges = rangesForDetectors[.date] ?? [] ranges.append(result.range) - activeDetectorRanges.updateValue(ranges, forKey: .date) + rangesForDetectors.updateValue(ranges, forKey: .date) case NSTextCheckingResult.CheckingType.phoneNumber: - var ranges = activeDetectorRanges[.phoneNumber] ?? [] + var ranges = rangesForDetectors[.phoneNumber] ?? [] ranges.append(result.range) - activeDetectorRanges.updateValue(ranges, forKey: .phoneNumber) + rangesForDetectors.updateValue(ranges, forKey: .phoneNumber) case NSTextCheckingResult.CheckingType.link: - var ranges = activeDetectorRanges[.url] ?? [] + var ranges = rangesForDetectors[.url] ?? [] ranges.append(result.range) - activeDetectorRanges.updateValue(ranges, forKey: .url) + rangesForDetectors.updateValue(ranges, forKey: .url) default: fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") } + + } + + } + + // MARK: - Gesture Handling + + private func stringIndex(at location: CGPoint) -> Int? { + guard textStorage.length > 0 else { return nil } + + var location = location + let textOffset = CGPoint(x: textInsets.left, y: textInsets.right) + + location.x -= textOffset.x + location.y -= textOffset.y + + let glyphIndex = layoutManager.glyphIndex(for: location, in: textContainer) + + let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: nil) + + if lineRect.contains(location) { + return layoutManager.characterIndexForGlyph(at: glyphIndex) + } else { + return nil + } + + } + + private func setupGestureRecognizers() { + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleGesture(_:))) + addGestureRecognizer(tapGesture) + tapGesture.delegate = self + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleGesture(_:))) + addGestureRecognizer(longPressGesture) + tapGesture.delegate = self + + isUserInteractionEnabled = true + } + + func handleGesture(_ gesture: UIGestureRecognizer) { + + let touchLocation = gesture.location(ofTouch: 0, in: self) + guard let index = stringIndex(at: touchLocation) else { return } + + for (detectorType, ranges) in rangesForDetectors { + for nsRange in ranges { + guard let range = nsRange.toRange() else { return } + if range.contains(index) { + guard let text = attributedText?.attributedSubstring(from: nsRange) else { return } + handleGesture(for: detectorType, text: text) + + } + } } } + private func handleGesture(for detectorType: DetectorType, text: NSAttributedString) { + switch detectorType { + case .address: + handleAddress(address: text) + case .phoneNumber: + handlePhoneNumber(phoneNumber: text) + case .date: + handleDate(date: text) + case .url: + handleURL(url: text) + } + } + + private func handleAddress(address: NSAttributedString) { + delegate?.didSelectAddress(address.string) + } + + private func handleDate(date: NSAttributedString) { + delegate?.didSelectDate(date.string) + } + + private func handleURL(url: NSAttributedString) { + delegate?.didSelectURL(url.string) + } + + private func handlePhoneNumber(phoneNumber: NSAttributedString) { + delegate?.didSelectPhoneNumber(phoneNumber.string) + } + } diff --git a/Sources/MessageLabelDelegate.swift b/Sources/MessageLabelDelegate.swift new file mode 100644 index 00000000..97068722 --- /dev/null +++ b/Sources/MessageLabelDelegate.swift @@ -0,0 +1,35 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +public protocol MessageLabelDelegate: class { + + func didSelectAddress(_ address: String) + func didSelectDate(_ date: String) + func didSelectPhoneNumber(_ phoneNumber: String) + func didSelectURL(_ url: String) // Maybe convert to URL for them + + +} diff --git a/Tests/MessagesDisplayDataSourceTests.swift b/Tests/MessagesDisplayDataSourceTests.swift index cd53ef3c..d7487cdc 100644 --- a/Tests/MessagesDisplayDataSourceTests.swift +++ b/Tests/MessagesDisplayDataSourceTests.swift @@ -22,8 +22,8 @@ SOFTWARE. */ - import XCTest + @testable import MessageKit class MessagesDisplayDataSourceTests: XCTestCase { From 7b16bcfef0d3609421cf28d5cbf5b2d3c03931fe Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 23 Aug 2017 23:09:19 -0500 Subject: [PATCH 20/27] Update example app to use MessageLabelDelegate and set default implementations --- .../Sources/ConversationViewController.swift | 22 +++++++++++++++++++ Sources/MessageLabelDelegate.swift | 20 ++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index a877e644..921236ef 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -128,6 +128,28 @@ extension ConversationViewController: MessageCellDelegate { } +// MARK: - MessageLabelDelegate + +extension ConversationViewController: MessageLabelDelegate { + + func didSelectAddress(_ address: String) { + print("Address Selected: \(address)") + } + + func didSelectDate(_ date: String) { + print("Date Selected: \(date)") + } + + func didSelectPhoneNumber(_ phoneNumber: String) { + print("Phone Number Selected: \(phoneNumber)") + } + + func didSelectURL(_ url: String) { + print("URL Selected: \(url)") + } + +} + // MARK: - MessageInputBarDelegate extension ConversationViewController: MessageInputBarDelegate { diff --git a/Sources/MessageLabelDelegate.swift b/Sources/MessageLabelDelegate.swift index 97068722..2104d091 100644 --- a/Sources/MessageLabelDelegate.swift +++ b/Sources/MessageLabelDelegate.swift @@ -27,9 +27,23 @@ import Foundation public protocol MessageLabelDelegate: class { func didSelectAddress(_ address: String) + func didSelectDate(_ date: String) + func didSelectPhoneNumber(_ phoneNumber: String) - func didSelectURL(_ url: String) // Maybe convert to URL for them - - + + func didSelectURL(_ url: String) // Maybe convert to URL here + +} + +extension MessageLabelDelegate { + + func didSelectAddress(_ address: String) {} + + func didSelectDate(_ date: String) {} + + func didSelectPhoneNumber(_ phoneNumber: String) {} + + func didSelectURL(_ url: String) {} + } From 035afbdcb698a76057f8cb0ae3545c5af7430a7d Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 23 Aug 2017 23:29:16 -0500 Subject: [PATCH 21/27] Add MessageLabelDelegate to MessagesCollectionView --- .../Sources/ConversationViewController.swift | 4 +++- Sources/MessagesCollectionView.swift | 2 ++ Sources/MessagesViewController.swift | 18 ++++++++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 921236ef..bdb2ae0d 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -34,8 +34,10 @@ class ConversationViewController: MessagesViewController { messageList = SampleData().getMessages() messagesCollectionView.messagesDataSource = self - messagesCollectionView.messageCellDelegate = self messagesCollectionView.messagesLayoutDelegate = self + + messagesCollectionView.messageCellDelegate = self + messagesCollectionView.messageLabelDelegate = self messageInputBar.delegate = self } } diff --git a/Sources/MessagesCollectionView.swift b/Sources/MessagesCollectionView.swift index 277a8c99..b8cff960 100644 --- a/Sources/MessagesCollectionView.swift +++ b/Sources/MessagesCollectionView.swift @@ -34,6 +34,8 @@ open class MessagesCollectionView: UICollectionView { open weak var messageCellDelegate: MessageCellDelegate? + open weak var messageLabelDelegate: MessageLabelDelegate? + private var indexPathForLastItem: IndexPath? { let lastSection = numberOfSections > 0 ? numberOfSections - 1 : 0 diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 5366cc1b..1c8f4f8a 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -155,14 +155,21 @@ extension MessagesViewController: UICollectionViewDataSource { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MessageCell", for: indexPath) as? MessageCollectionViewCell ?? MessageCollectionViewCell() guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return cell } - guard let messageCellDelegate = messagesCollectionView.messageCellDelegate else { return cell } - - cell.delegate = messageCellDelegate - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return cell } + + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + cell.configure(with: message) + + if let cellDelegate = messagesCollectionView.messageCellDelegate { + cell.delegate = cellDelegate + } + + if let messageLabelDelegate = messagesCollectionView.messageLabelDelegate { + cell.messageLabel.delegate = messageLabelDelegate + } + guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } - let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) let textColor = displayDataSource.textColor(for: message, at: indexPath, in: messagesCollectionView) let avatar = displayDataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) @@ -174,7 +181,6 @@ extension MessagesViewController: UICollectionViewDataSource { cell.avatarView.set(avatar: avatar) cell.messageLabel.textColor = textColor cell.messageContainerView.backgroundColor = messageColor - cell.configure(with: message) return cell From ba85191dd09cede56ddd12b83ab73aca5b2f7fe8 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 23 Aug 2017 23:50:52 -0500 Subject: [PATCH 22/27] Fix refactoring bug and add MessageLabelDelegate property --- Sources/MessageLabel.swift | 27 +++++++++++++++++++-------- Sources/MessagesViewController.swift | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index 75921aef..d83684e4 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -53,7 +53,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { // MARK: - Public Properties - open var delegate: MessageLabelDelegate? + open weak var delegate: MessageLabelDelegate? open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url] @@ -219,7 +219,15 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { return } - let textWithDetectorAttributes = addDetectorAttributes(to: attributedText) + guard let checkingResults = parse(text: attributedText, for: enabledDetectors), checkingResults.isEmpty == false else { + textStorage.setAttributedString(attributedText) + setNeedsDisplay() + return + } + + setRangesForDetectors(in: checkingResults) + + let textWithDetectorAttributes = addDetectorAttributes(to: attributedText, for: checkingResults) textStorage.setAttributedString(textWithDetectorAttributes) @@ -227,14 +235,10 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { } - private func addDetectorAttributes(to text: NSAttributedString) -> NSAttributedString { + private func addDetectorAttributes(to text: NSAttributedString, for checkingResults: [NSTextCheckingResult]) -> NSAttributedString { let mutableAttributedString = NSMutableAttributedString(attributedString: text) - guard enabledDetectors.isEmpty == false else { return mutableAttributedString } - guard let checkingResults = parse(text: text, for: enabledDetectors) else { return mutableAttributedString } - guard checkingResults.isEmpty == false else { return mutableAttributedString } - checkingResults.forEach { result in let attributes = detectorAttributes(for: result.resultType) mutableAttributedString.addAttributes(attributes, range: result.range) @@ -292,7 +296,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { // MARK: - Parsing Text private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? { - + guard detectorTypes.isEmpty == false else { return nil } let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue } let detector = try? NSDataDetector(types: checkingTypes) @@ -353,6 +357,8 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { private func setupGestureRecognizers() { + print("Setting up gesture recognizers") + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleGesture(_:))) addGestureRecognizer(tapGesture) tapGesture.delegate = self @@ -366,6 +372,8 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { func handleGesture(_ gesture: UIGestureRecognizer) { + print("Handling gesture") + let touchLocation = gesture.location(ofTouch: 0, in: self) guard let index = stringIndex(at: touchLocation) else { return } @@ -382,6 +390,8 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { } private func handleGesture(for detectorType: DetectorType, text: NSAttributedString) { + + print("Detector routing") switch detectorType { case .address: handleAddress(address: text) @@ -395,6 +405,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { } private func handleAddress(address: NSAttributedString) { + print("Ok") delegate?.didSelectAddress(address.string) } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 1c8f4f8a..f846c103 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -166,6 +166,7 @@ extension MessagesViewController: UICollectionViewDataSource { if let messageLabelDelegate = messagesCollectionView.messageLabelDelegate { cell.messageLabel.delegate = messageLabelDelegate + print("Setting delegate") } guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } From cc7fcbeb7d1c30503d24a32bf4078019de743286 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 24 Aug 2017 00:02:24 -0500 Subject: [PATCH 23/27] Remove print statements --- Sources/MessageLabel.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index d83684e4..500f2ac5 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -357,8 +357,6 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { private func setupGestureRecognizers() { - print("Setting up gesture recognizers") - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleGesture(_:))) addGestureRecognizer(tapGesture) tapGesture.delegate = self @@ -372,8 +370,6 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { func handleGesture(_ gesture: UIGestureRecognizer) { - print("Handling gesture") - let touchLocation = gesture.location(ofTouch: 0, in: self) guard let index = stringIndex(at: touchLocation) else { return } @@ -391,7 +387,6 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { private func handleGesture(for detectorType: DetectorType, text: NSAttributedString) { - print("Detector routing") switch detectorType { case .address: handleAddress(address: text) @@ -405,7 +400,6 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { } private func handleAddress(address: NSAttributedString) { - print("Ok") delegate?.didSelectAddress(address.string) } From 1a1ecd447d64b8da97f787faa0a256373ba9d303 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 24 Aug 2017 04:11:43 -0500 Subject: [PATCH 24/27] Set attributedText after textColor has been set and reduce delegate assignments --- Sources/MessagesViewController.swift | 39 +++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index f846c103..f1fba2fd 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -155,33 +155,36 @@ extension MessagesViewController: UICollectionViewDataSource { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MessageCell", for: indexPath) as? MessageCollectionViewCell ?? MessageCollectionViewCell() guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return cell } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return cell } - - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - cell.configure(with: message) if let cellDelegate = messagesCollectionView.messageCellDelegate { - cell.delegate = cellDelegate + if cell.delegate == nil { cell.delegate = cellDelegate } } if let messageLabelDelegate = messagesCollectionView.messageLabelDelegate { - cell.messageLabel.delegate = messageLabelDelegate - print("Setting delegate") + if cell.messageLabel.delegate == nil { cell.messageLabel.delegate = messageLabelDelegate } } - guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return cell } - let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) - let textColor = displayDataSource.textColor(for: message, at: indexPath, in: messagesCollectionView) - let avatar = displayDataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) - let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) - let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - cell.cellTopLabel.attributedText = topLabelText - cell.cellBottomLabel.attributedText = bottomLabelText - cell.avatarView.set(avatar: avatar) - cell.messageLabel.textColor = textColor - cell.messageContainerView.backgroundColor = messageColor + if let displayDataSource = messagesDataSource as? MessagesDisplayDataSource { + + let messageColor = displayDataSource.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) + let textColor = displayDataSource.textColor(for: message, at: indexPath, in: messagesCollectionView) + let avatar = displayDataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) + let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + + cell.avatarView.set(avatar: avatar) + cell.messageLabel.textColor = textColor + cell.messageContainerView.backgroundColor = messageColor + cell.cellTopLabel.attributedText = topLabelText + cell.cellBottomLabel.attributedText = bottomLabelText + + } + + cell.configure(with: message) return cell From 4fcbb998c4c54d42f00c2cea11fcc9fa0b437246 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 24 Aug 2017 04:29:34 -0500 Subject: [PATCH 25/27] Disable type_body_length and file_length rules --- .swiftlint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.swiftlint.yml b/.swiftlint.yml index c94593c5..d7b29322 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,3 +3,5 @@ disabled_rules: - identifier_name - trailing_whitespace - line_length +- type_body_length +- file_length From aaa70b8dade16a753429ae54838c688ef8b6cbc2 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 24 Aug 2017 06:11:48 -0500 Subject: [PATCH 26/27] Fix conflicting gesture recognizers --- Sources/MessageCollectionViewCell.swift | 1 + Sources/MessageLabel.swift | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index f2307b44..5bf2bff5 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -204,6 +204,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { let messageTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapMessage)) messageContainerView.addGestureRecognizer(messageTapGesture) + messageTapGesture.delegate = messageLabel let topLabelTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTopLabel)) cellTopLabel.addGestureRecognizer(topLabelTapGesture) diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index 500f2ac5..ca4fbeee 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -193,16 +193,19 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { let touchLocation = touch.location(in: self) - guard let index = stringIndex(at: touchLocation) else { return false } - for (_, ranges) in rangesForDetectors { - for nsRange in ranges { - guard let range = nsRange.toRange() else { return false } - if range.contains(index) { return true } + if let index = stringIndex(at: touchLocation) { + for (_, ranges) in rangesForDetectors { + for nsRange in ranges { + guard let range = nsRange.toRange() else { return false } + if range.contains(index) { + return gestureRecognizer.view == self + } + } } } - return false + return gestureRecognizer.view != self } From f55807ab9abe2b41bfc95d68d9ef136088f5f89c Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 24 Aug 2017 07:22:40 -0500 Subject: [PATCH 27/27] Pass more useful delegate data and resolve more gesture conflicts --- .../Sources/ConversationViewController.swift | 8 +- Sources/MessageLabel.swift | 102 +++++++++++------- Sources/MessageLabelDelegate.swift | 12 +-- 3 files changed, 71 insertions(+), 51 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index bdb2ae0d..de9d5bb3 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -134,11 +134,11 @@ extension ConversationViewController: MessageCellDelegate { extension ConversationViewController: MessageLabelDelegate { - func didSelectAddress(_ address: String) { - print("Address Selected: \(address)") + func didSelectAddress(_ addressComponents: [String : String]) { + print("Address Selected: \(addressComponents)") } - func didSelectDate(_ date: String) { + func didSelectDate(_ date: Date) { print("Date Selected: \(date)") } @@ -146,7 +146,7 @@ extension ConversationViewController: MessageLabelDelegate { print("Phone Number Selected: \(phoneNumber)") } - func didSelectURL(_ url: String) { + func didSelectURL(_ url: URL) { print("URL Selected: \(url)") } diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift index ca4fbeee..e8e35596 100644 --- a/Sources/MessageLabel.swift +++ b/Sources/MessageLabel.swift @@ -49,7 +49,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { return textStorage }() - private lazy var rangesForDetectors: [DetectorType: [NSRange]] = [:] + private lazy var rangesForDetectors: [DetectorType: [(NSRange, Any?)]] = [:] // MARK: - Public Properties @@ -190,23 +190,37 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { return true } + //swiftlint:disable cyclomatic_complexity + // Yeah we're disabling this because the whole file is a mess :D public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { let touchLocation = touch.location(in: self) - if let index = stringIndex(at: touchLocation) { + switch true { + case gestureRecognizer.view != self.superview && gestureRecognizer.view != self: + return true + case gestureRecognizer.view == self.superview: + guard let index = stringIndex(at: touchLocation) else { return true } for (_, ranges) in rangesForDetectors { - for nsRange in ranges { - guard let range = nsRange.toRange() else { return false } - if range.contains(index) { - return gestureRecognizer.view == self - } + for (nsRange, _) in ranges { + guard let range = nsRange.toRange() else { return true } + if range.contains(index) { return false } } } + return true + case gestureRecognizer.view == self: + guard let index = stringIndex(at: touchLocation) else { return false } + for (_, ranges) in rangesForDetectors { + for (nsRange, _) in ranges { + guard let range = nsRange.toRange() else { return false } + if range.contains(index) { return true } + } + } + return false + default: + return true } - return gestureRecognizer.view != self - } // MARK: - Private Methods @@ -233,9 +247,9 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { let textWithDetectorAttributes = addDetectorAttributes(to: attributedText, for: checkingResults) textStorage.setAttributedString(textWithDetectorAttributes) - + setNeedsDisplay() - + } private func addDetectorAttributes(to text: NSAttributedString, for checkingResults: [NSTextCheckingResult]) -> NSAttributedString { @@ -257,13 +271,13 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { guard let ranges = rangesForDetectors[detectorType] else { return } - ranges.forEach { range in + ranges.forEach { (range, _) in let attributes = detectorAttributes(for: detectorType) mutableAttributedString.addAttributes(attributes, range: range) } textStorage.setAttributedString(mutableAttributedString) - + } private func detectorAttributes(for detectorType: DetectorType) -> [String: Any] { @@ -313,26 +327,30 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { switch result.resultType { case NSTextCheckingResult.CheckingType.address: var ranges = rangesForDetectors[.address] ?? [] - ranges.append(result.range) + let tuple = (result.range, result.addressComponents) as (NSRange, Any?) + ranges.append(tuple) rangesForDetectors.updateValue(ranges, forKey: .address) case NSTextCheckingResult.CheckingType.date: var ranges = rangesForDetectors[.date] ?? [] - ranges.append(result.range) + let tuple = (result.range, result.date) as (NSRange, Any?) + ranges.append(tuple) rangesForDetectors.updateValue(ranges, forKey: .date) case NSTextCheckingResult.CheckingType.phoneNumber: var ranges = rangesForDetectors[.phoneNumber] ?? [] - ranges.append(result.range) + let tuple = (result.range, result.phoneNumber) as (NSRange, Any?) + ranges.append(tuple) rangesForDetectors.updateValue(ranges, forKey: .phoneNumber) case NSTextCheckingResult.CheckingType.link: var ranges = rangesForDetectors[.url] ?? [] - ranges.append(result.range) + let tuple = (result.range, result.url) as (NSRange, Any?) + ranges.append(tuple) rangesForDetectors.updateValue(ranges, forKey: .url) default: fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") } } - + } // MARK: - Gesture Handling @@ -355,7 +373,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { } else { return nil } - + } private func setupGestureRecognizers() { @@ -377,45 +395,47 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate { guard let index = stringIndex(at: touchLocation) else { return } for (detectorType, ranges) in rangesForDetectors { - for nsRange in ranges { + for (nsRange, value) in ranges { guard let range = nsRange.toRange() else { return } if range.contains(index) { - guard let text = attributedText?.attributedSubstring(from: nsRange) else { return } - handleGesture(for: detectorType, text: text) - + handleGesture(for: detectorType, value: value) } } } } - private func handleGesture(for detectorType: DetectorType, text: NSAttributedString) { + private func handleGesture(for detectorType: DetectorType, value: Any?) { switch detectorType { case .address: - handleAddress(address: text) + guard let addressComponents = value as? [String: String] else { return } + handleAddress(addressComponents) case .phoneNumber: - handlePhoneNumber(phoneNumber: text) + guard let phoneNumber = value as? String else { return } + handlePhoneNumber(phoneNumber) case .date: - handleDate(date: text) + guard let date = value as? Date else { return } + handleDate(date) case .url: - handleURL(url: text) + guard let url = value as? URL else { return } + handleURL(url) } } - - private func handleAddress(address: NSAttributedString) { - delegate?.didSelectAddress(address.string) + + private func handleAddress(_ addressComponents: [String: String]) { + delegate?.didSelectAddress(addressComponents) } - - private func handleDate(date: NSAttributedString) { - delegate?.didSelectDate(date.string) + + private func handleDate(_ date: Date) { + delegate?.didSelectDate(date) } - - private func handleURL(url: NSAttributedString) { - delegate?.didSelectURL(url.string) + + private func handleURL(_ url: URL) { + delegate?.didSelectURL(url) } - - private func handlePhoneNumber(phoneNumber: NSAttributedString) { - delegate?.didSelectPhoneNumber(phoneNumber.string) + + private func handlePhoneNumber(_ phoneNumber: String) { + delegate?.didSelectPhoneNumber(phoneNumber) } - + } diff --git a/Sources/MessageLabelDelegate.swift b/Sources/MessageLabelDelegate.swift index 2104d091..02957158 100644 --- a/Sources/MessageLabelDelegate.swift +++ b/Sources/MessageLabelDelegate.swift @@ -26,24 +26,24 @@ import Foundation public protocol MessageLabelDelegate: class { - func didSelectAddress(_ address: String) + func didSelectAddress(_ addressComponents: [String: String]) - func didSelectDate(_ date: String) + func didSelectDate(_ date: Date) func didSelectPhoneNumber(_ phoneNumber: String) - func didSelectURL(_ url: String) // Maybe convert to URL here + func didSelectURL(_ url: URL) } extension MessageLabelDelegate { - func didSelectAddress(_ address: String) {} + func didSelectAddress(_ addressComponents: [String: String]) {} - func didSelectDate(_ date: String) {} + func didSelectDate(_ date: Date) {} func didSelectPhoneNumber(_ phoneNumber: String) {} - func didSelectURL(_ url: String) {} + func didSelectURL(_ url: URL) {} }