diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..edf24697 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,6 @@ +line_length: + warning: 150 + ignores_comments: true + +disabled_rules: + identifier_name diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e1cdcf..3b9cfba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,29 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa -------------------------------------- -Upcoming release +## Upcoming release ---------------- +## [Prerelease] 0.2.0 + +This release closes the [0.2 milestone](https://github.com/MessageKit/MessageKit/milestone/2?closed=1). + +### API Breaking + +- `MessagesDataSource` & `MessagesDisplayDataSource` collectionView params are now typed as `MessagesCollectionView`. + +### Enahancements + +- Resizing `UITextView` in `MessageInputBar`. +- Adds basic support for `MessageHeaderView` and `MessageFooterView`. +- Adds `MessageCellDelegate` to handle tap events on message container or avatar. + +### Bugfixes + +- Fixes layout for `Landscape` orientation. + +## [Prerelease] 0.1.0 + This release closes the [0.1 milestone](https://github.com/MessageKit/MessageKit/milestone/1?closed=1). Initial release. :tada: diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index c8eb2113..af1a3441 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -25,43 +25,44 @@ import UIKit import MessageKit -class ConversationViewController: MessagesViewController, MessagesDataSource, MessagesDisplayDataSource { - +class ConversationViewController: MessagesViewController { + var messages: [MessageType] = [] - + override func viewDidLoad() { super.viewDidLoad() - + addSampleData() - + messagesCollectionView.messagesDataSource = self - messagesCollectionView.messagesDisplayDataSource = self + messagesCollectionView.messageCellDelegate = self + messagesCollectionView.messagesLayoutDelegate = self messageInputBar.delegate = self - + tabBarController?.tabBar.isHidden = true } - + func addSampleData() { - + let sender1 = Sender(id: "123456", displayName: "Bobby") let sender2 = Sender(id: "654321", displayName: "Steven") let sender3 = Sender(id: "777999", displayName: "Omar") - + let msg1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." - + "Pellentesque venenatis, ante et hendrerit rutrum" + + "Quam erat vehicula metus, et condimentum ante tellus augue." + let msg2 = "Cras efficitur bibendum mauris sed ultrices." + - "Phasellus tellus nisl, ullamcorper quis erat." - + "Phasellus tellus nisl, ullamcorper quis erat." + let msg3 = "Maecenas." - + let msg4 = "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." - + "Quam erat vehicula metus, et condimentum ante tellus augue." + let msg5 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." + "Pellentesque venenatis, ante et hendrerit rutrum" + + "Quam erat vehicula metus, et condimentum ante tellus augue." messages.append(MockMessage(text: msg2, sender: sender2, id: NSUUID().uuidString)) messages.append(MockMessage(text: msg4, sender: currentSender(), id: NSUUID().uuidString)) @@ -83,39 +84,88 @@ class ConversationViewController: MessagesViewController, MessagesDataSource, Me messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) messages.append(MockMessage(text: msg3, sender: sender1, id: NSUUID().uuidString)) + } - + +} + +// MARK: - MessagesDataSource + +extension ConversationViewController: MessagesDataSource { + func currentSender() -> Sender { return Sender(id: "123", displayName: "Steven") } - - func numberOfMessages(in collectionView: UICollectionView) -> Int { + + func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { return messages.count } - - func messageForItem(at indexPath: IndexPath, in collectionView: UICollectionView) -> MessageType { + + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { return messages[indexPath.section] } - - func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in collectionView: UICollectionView) -> Avatar { + +} + +// MARK: - MessagesDisplayDataSource + +extension ConversationViewController: MessagesDisplayDataSource { + + func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { let image = isFromCurrentSender(message: message) ? #imageLiteral(resourceName: "Steve-Jobs") : #imageLiteral(resourceName: "Tim-Cook") return Avatar(placeholderImage: image) } - + + func headerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { + return messagesCollectionView.dequeueMessageHeaderView(for: indexPath) + } + + func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? { + return messagesCollectionView.dequeueMessageFooterView(for: indexPath) + } + } +// MARK: - MessagesLayoutDelegate + +extension ConversationViewController: MessagesLayoutDelegate { + + func headerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return CGSize(width: messagesCollectionView.bounds.width, height: 4) + } + + func footerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return CGSize(width: messagesCollectionView.bounds.width, height: 4) + } + +} + +// MARK: - MessageCellDelegate + +extension ConversationViewController: MessageCellDelegate { + + func didTapAvatar(in cell: MessageCollectionViewCell) { + print("Avatar tapped") + } + + func didTapMessage(in cell: MessageCollectionViewCell) { + print("Message tapped") + } + +} + +// MARK: - MessageInputBarDelegate + extension ConversationViewController: MessageInputBarDelegate { - + func sendButtonPressed(sender: UIButton, textView: UITextView) { guard let message = textView.text else { return } messages.append(MockMessage(text: message, sender: currentSender(), id: NSUUID().uuidString)) - + messagesCollectionView.reloadData() - + } } - - diff --git a/Example/Sources/InboxViewController.swift b/Example/Sources/InboxViewController.swift index 761b28f6..31ede66e 100644 --- a/Example/Sources/InboxViewController.swift +++ b/Example/Sources/InboxViewController.swift @@ -25,17 +25,16 @@ import UIKit import MessageKit - final class InboxViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() cell.textLabel?.text = "Test" @@ -43,4 +42,3 @@ final class InboxViewController: UITableViewController { } } - diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index 83603a00..95e7bbfd 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -26,18 +26,17 @@ import Foundation import MessageKit struct MockMessage: MessageType { - + var messageId: String var sender: Sender var sentDate: Date var data: MessageData - + init(text: String, sender: Sender, id: String) { data = .text(text) self.sender = sender self.messageId = id self.sentDate = Date() } - - + } diff --git a/Example/Sources/SettingsViewController.swift b/Example/Sources/SettingsViewController.swift index 3109e595..9dc9a7a8 100644 --- a/Example/Sources/SettingsViewController.swift +++ b/Example/Sources/SettingsViewController.swift @@ -25,11 +25,10 @@ import UIKit import MessageKit - final class SettingsViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() } -} +} diff --git a/Example/Tests/ChatExampleTests.swift b/Example/Tests/ChatExampleTests.swift index 128a25e2..f29a4705 100644 --- a/Example/Tests/ChatExampleTests.swift +++ b/Example/Tests/ChatExampleTests.swift @@ -20,17 +20,18 @@ import XCTest @testable import ChatExample final class ChatExampleTests: XCTestCase { - + override func setUp() { super.setUp() } - + override func tearDown() { super.tearDown() } - + func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } + } diff --git a/Example/UITests/ChatExampleUITests.swift b/Example/UITests/ChatExampleUITests.swift index 3fa22105..28393690 100644 --- a/Example/UITests/ChatExampleUITests.swift +++ b/Example/UITests/ChatExampleUITests.swift @@ -20,29 +20,32 @@ import XCTest @testable import ChatExample final class ChatExampleUITests: XCTestCase { - + override func setUp() { super.setUp() // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false - // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + // UI tests must launch the application that they test. + // Doing this in setup will make sure it happens for each test method. if #available(iOS 9.0, *) { XCUIApplication().launch() } else { // Fallback on earlier versions } - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. + // In UI tests it’s important to set the initial state + // - such as interface orientation - required for your tests before they run. // The setUp method is a good place to do this. } - + override func tearDown() { super.tearDown() } - + func testExample() { // Use recording to get started writing UI tests. // Use XCTAssert and related functions to verify your tests produce the correct results. } + } diff --git a/MessageKit.podspec b/MessageKit.podspec index eac269c0..11882e6f 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '0.1.0' + s.version = '0.2.0' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.' diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 4ae9d5ac..5fd8f5d0 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88916B461CF0DFE600469F91 /* MessageType.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 */; }; B0655A261F23D6C500542A83 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A251F23D6C500542A83 /* Avatar.swift */; }; B0655A281F23D71400542A83 /* MessageDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A271F23D71400542A83 /* MessageDirection.swift */; }; B0655A2A1F23D77200542A83 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A291F23D77200542A83 /* Sender.swift */; }; @@ -23,6 +24,9 @@ B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A371F23EE8B00542A83 /* MessageInputBar.swift */; }; B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */; }; B0655A4F1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */; }; + B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */; }; + B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */; }; + B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.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 */; }; @@ -50,6 +54,7 @@ 88916B461CF0DFE600469F91 /* MessageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageType.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 = ""; }; B0655A251F23D6C500542A83 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.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 = ""; }; @@ -58,6 +63,9 @@ B0655A371F23EE8B00542A83 /* MessageInputBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = ""; }; B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewFlowLayout.swift; sourceTree = ""; }; + B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -149,6 +157,8 @@ B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */, B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */, B0655A371F23EE8B00542A83 /* MessageInputBar.swift */, + B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, + B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, ); name = Views; sourceTree = ""; @@ -160,6 +170,8 @@ 88916B461CF0DFE600469F91 /* MessageType.swift */, 882D75831DE507320033F95F /* MessagesDataSource.swift */, B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */, + B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */, + B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */, ); name = Protocols; sourceTree = ""; @@ -212,6 +224,7 @@ 88916B1E1CF0DF2F00469F91 /* Frameworks */, 88916B1F1CF0DF2F00469F91 /* Headers */, 88916B201CF0DF2F00469F91 /* Resources */, + B03FF9A51F30398900754FE5 /* ShellScript */, ); buildRules = ( ); @@ -294,11 +307,28 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + B03FF9A51F30398900754FE5 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 88916B1D1CF0DF2F00469F91 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */, 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */, 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */, B0655A4F1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift in Sources */, @@ -308,11 +338,14 @@ B0655A261F23D6C500542A83 /* Avatar.swift in Sources */, B015E81F1F259D8E007EDFB6 /* MessageInputBarDelegate.swift in Sources */, B0655A2A1F23D77200542A83 /* Sender.swift in Sources */, + B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */, B0655A4D1F244C0600542A83 /* MessageCollectionViewCell.swift in Sources */, B0655A2E1F23D8BC00542A83 /* MessagesCollectionView.swift in Sources */, + B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */, B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */, B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */, 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, + B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */, B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, ); diff --git a/Sources/Avatar.swift b/Sources/Avatar.swift index fd393ad8..ecc8eaf6 100644 --- a/Sources/Avatar.swift +++ b/Sources/Avatar.swift @@ -25,21 +25,21 @@ import Foundation public struct Avatar { - + public let image: UIImage? - + public let highlightedImage: UIImage? - + public let placeholderImage: UIImage - + public init(image: UIImage? = nil, highlightedImage: UIImage? = nil, placeholderImage: UIImage) { self.image = image self.highlightedImage = highlightedImage self.placeholderImage = placeholderImage } - + public func image(highlighted: Bool) -> UIImage { - + switch highlighted { case true: return highlightedImage ?? image ?? placeholderImage diff --git a/Sources/MessageCellDelegate.swift b/Sources/MessageCellDelegate.swift new file mode 100644 index 00000000..3b8a56d9 --- /dev/null +++ b/Sources/MessageCellDelegate.swift @@ -0,0 +1,41 @@ +/* + 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 MessageCellDelegate: class { + + func didTapMessage(in cell: MessageCollectionViewCell) + + func didTapAvatar(in cell: MessageCollectionViewCell) + +} + +extension MessageCellDelegate { + + func didTapMessage(in cell: MessageCollectionViewCell) {} + + func didTapAvatar(in cell: MessageCollectionViewCell) {} + +} diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 96db33ee..baf2ac95 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -25,19 +25,19 @@ import UIKit open class MessageCollectionViewCell: UICollectionViewCell { - + // MARK: - Properties - + open let messageContainerView: UIView = { - + let messageContainerView = UIView() messageContainerView.layer.cornerRadius = 12.0 messageContainerView.layer.masksToBounds = true return messageContainerView }() - + open let avatarImageView: UIImageView = { - + let avatarImageView = UIImageView() avatarImageView.contentMode = .scaleAspectFill avatarImageView.backgroundColor = .lightGray @@ -45,7 +45,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { avatarImageView.clipsToBounds = true return avatarImageView }() - + open let messageLabel: UILabel = { let messageLabel = UILabel() @@ -54,57 +54,66 @@ open class MessageCollectionViewCell: UICollectionViewCell { messageLabel.isOpaque = false return messageLabel }() - + + open weak var delegate: MessageCellDelegate? + // MARK: - Initializer - - + override public init(frame: CGRect) { super.init(frame: frame) setupSubviews() + setupGestureRecognizers() + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Methods - + private func setupSubviews() { - + contentView.addSubview(messageContainerView) contentView.addSubview(avatarImageView) messageContainerView.addSubview(messageLabel) } - + override open func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) - + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - + messageLabel.font = attributes.messageFont - + setAvatarFrameFor(attributes: attributes) setMessageContainerFrameFor(attributes: attributes) setMessageLabelFor(attributes: attributes) } - + private func setMessageContainerFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { - + switch attributes.direction { case .incoming: let x = attributes.avatarSize.width + attributes.avatarContainerSpacing - messageContainerView.frame = CGRect(x: x, y: 0, width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height) + messageContainerView.frame = CGRect(x: x, + y: 0, + width: attributes.messageContainerSize.width, + height: attributes.messageContainerSize.height) case .outgoing: let x = contentView.frame.width - attributes.avatarSize.width - attributes.avatarContainerSpacing - attributes.messageContainerSize.width - messageContainerView.frame = CGRect(x: x, y: 0, width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height) + messageContainerView.frame = CGRect(x: x, + y: 0, + width: attributes.messageContainerSize.width, + height: attributes.messageContainerSize.height) } } - + private func setAvatarFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { - + switch attributes.direction { case .incoming: let y = frame.height - attributes.avatarSize.height - attributes.avatarBottomSpacing @@ -114,25 +123,46 @@ open class MessageCollectionViewCell: UICollectionViewCell { let x = contentView.frame.width - attributes.avatarSize.width avatarImageView.frame = CGRect(x: x, y: y, width: attributes.avatarSize.width, height: attributes.avatarSize.height) } - + avatarImageView.layer.cornerRadius = avatarImageView.frame.width / 2 } - + private func setMessageLabelFor(attributes: MessagesCollectionViewLayoutAttributes) { - + let frame = CGRect(x: 0, y: 0, width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height) let insetFrame = UIEdgeInsetsInsetRect(frame, attributes.messageContainerInsets) messageLabel.frame = insetFrame } - + func configure(with message: MessageType) { - + switch message.data { case .text(let text): messageLabel.text = text } } + + func setupGestureRecognizers() { + + let avatarTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAvatar)) + avatarImageView.addGestureRecognizer(avatarTapGesture) + avatarImageView.isUserInteractionEnabled = true + + let messageTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapMessage)) + messageContainerView.addGestureRecognizer(messageTapGesture) + + } + + // MARK: - Delegate Methods + + func didTapAvatar() { + delegate?.didTapAvatar(in: self) + } + + func didTapMessage() { + delegate?.didTapMessage(in: self) + } } diff --git a/Sources/MessageData.swift b/Sources/MessageData.swift index a0106c7d..86130944 100644 --- a/Sources/MessageData.swift +++ b/Sources/MessageData.swift @@ -26,11 +26,11 @@ import Foundation //import class CoreLocation.CLLocation public enum MessageData { - + case text(String) - + // MARK: - Not supported yet - + // case attributedText(NSAttributedString) // // case audio(Data) diff --git a/Sources/MessageFooterView.swift b/Sources/MessageFooterView.swift new file mode 100644 index 00000000..cff8fe1c --- /dev/null +++ b/Sources/MessageFooterView.swift @@ -0,0 +1,37 @@ +/* + 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 UIKit + +open class MessageFooterView: UICollectionReusableView { + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Sources/MessageHeaderView.swift b/Sources/MessageHeaderView.swift new file mode 100644 index 00000000..c2070517 --- /dev/null +++ b/Sources/MessageHeaderView.swift @@ -0,0 +1,37 @@ +/* + 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 UIKit + +open class MessageHeaderView: UICollectionReusableView { + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Sources/MessageInputBar.swift b/Sources/MessageInputBar.swift index 670d2bf7..fd89376e 100644 --- a/Sources/MessageInputBar.swift +++ b/Sources/MessageInputBar.swift @@ -24,12 +24,12 @@ import UIKit -open class MessageInputBar: UIView { - +open class MessageInputBar: UIView, UITextViewDelegate { + // MARK: - Properties - + open let inputTextView: UITextView = { - + let inputTextView = UITextView(frame: .zero) inputTextView.font = UIFont.preferredFont(forTextStyle: .body) inputTextView.text = "New Message" @@ -39,68 +39,110 @@ open class MessageInputBar: UIView { inputTextView.layer.borderWidth = 1.0 inputTextView.layer.cornerRadius = 3.0 inputTextView.layer.masksToBounds = true + inputTextView.isScrollEnabled = false return inputTextView }() - + open let sendButton: UIButton = { - + let sendButton = UIButton() sendButton.setTitle("Send", for: .normal) sendButton.setTitleColor(.lightGray, for: .normal) return sendButton }() - + open weak var delegate: MessageInputBarDelegate? - + // MARK: - Initializers override public init(frame: CGRect) { super.init(frame: frame) - + setupSubviews() setupConstraints() registerSelector() - + + inputTextView.delegate = self + backgroundColor = .inputBarGray + autoresizingMask = .flexibleHeight + + NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange), name: .UIDeviceOrientationDidChange, object: nil) + } - + convenience public init() { self.init(frame: .zero) } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + deinit { + NotificationCenter.default.removeObserver(self) + } + // MARK: - Methods - + + func orientationDidChange(_ notification: Notification) { + invalidateIntrinsicContentSize() + } + + public func textViewDidChange(_ textView: UITextView) { + invalidateIntrinsicContentSize() + } + + override open var intrinsicContentSize: CGSize { + let sizeToFit = inputTextView.sizeThatFits(CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)) + let heightToFit = sizeToFit.height.rounded() + return CGSize(width: bounds.width, height: heightToFit + 8) + } + private func setupSubviews() { - + addSubview(inputTextView) addSubview(sendButton) - + } - + private func setupConstraints() { inputTextView.translatesAutoresizingMaskIntoConstraints = false - addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 4)) - addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: -4)) - addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 4)) - addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .trailing, relatedBy: .equal, toItem: sendButton, attribute: .leading, multiplier: 1, constant: -4)) + + addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .top, relatedBy: .equal, + toItem: self, attribute: .top, multiplier: 1, constant: 4)) + + addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .bottom, relatedBy: .equal, + toItem: self, attribute: .bottom, multiplier: 1, constant: -4)) + + addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .leading, relatedBy: .equal, + toItem: self, attribute: .leading, multiplier: 1, constant: 4)) + + addConstraint(NSLayoutConstraint(item: inputTextView, attribute: .trailing, relatedBy: .equal, + toItem: sendButton, attribute: .leading, multiplier: 1, constant: -4)) sendButton.translatesAutoresizingMaskIntoConstraints = false - addConstraint(NSLayoutConstraint(item: sendButton, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)) - addConstraint(NSLayoutConstraint(item: sendButton, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -4)) - addConstraint(NSLayoutConstraint(item: sendButton, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)) + + addConstraint(NSLayoutConstraint(item: sendButton, attribute: .bottom, relatedBy: .equal, + toItem: inputTextView, attribute: .bottom, multiplier: 1, constant: 0)) + + addConstraint(NSLayoutConstraint(item: sendButton, attribute: .trailing, relatedBy: .equal, + toItem: self, attribute: .trailing, multiplier: 1, constant: -4)) + + addConstraint(NSLayoutConstraint(item: sendButton, attribute: .top, relatedBy: .greaterThanOrEqual, + toItem: self, attribute: .top, multiplier: 1, constant: 0)) + + addConstraint(NSLayoutConstraint(item: sendButton, attribute: .width, relatedBy: .equal, + toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 60)) } - + private func registerSelector() { sendButton.addTarget(self, action: #selector(MessageInputBar.sendButtonPressed), for: .touchUpInside) } - + func sendButtonPressed() { delegate?.sendButtonPressed(sender: sendButton, textView: inputTextView) } diff --git a/Sources/MessagesCollectionView.swift b/Sources/MessagesCollectionView.swift index e7bbefe8..e420a8f7 100644 --- a/Sources/MessagesCollectionView.swift +++ b/Sources/MessagesCollectionView.swift @@ -25,30 +25,51 @@ import UIKit open class MessagesCollectionView: UICollectionView { - + // MARK: - Properties - + open weak var messagesDataSource: MessagesDataSource? - - open weak var messagesDisplayDataSource: MessagesDisplayDataSource? - + + open weak var messagesLayoutDelegate: MessagesLayoutDelegate? + + open weak var messageCellDelegate: MessageCellDelegate? + + //open weak var messagesDisplayDataSource: MessagesDisplayDataSource? + + var indexPathForLastItem: IndexPath? { + + let lastSection = numberOfSections > 0 ? numberOfSections - 1 : 0 + guard numberOfItems(inSection: lastSection) > 0 else { return nil } + return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) + + } + // MARK: - Initializers - + override public init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) backgroundColor = .white } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - var indexPathForLastItem: IndexPath? { - - let lastSection = numberOfSections > 0 ? numberOfSections - 1 : 0 - guard numberOfItems(inSection: lastSection) > 0 else { return nil } - return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) - - } + // MARK: - Methods + + func scrollToBottom(animated: Bool = false) { + guard let indexPath = indexPathForLastItem else { return } + scrollToItem(at: indexPath, at: .bottom, animated: animated) + } + + open func dequeueMessageHeaderView(withReuseIdentifier identifier: String = "MessageHeader", for indexPath: IndexPath) -> MessageHeaderView { + let header = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: identifier, for: indexPath) + return header as? MessageHeaderView ?? MessageHeaderView() + } + + open func dequeueMessageFooterView(withReuseIdentifier identifier: String = "MessageFooter", for indexPath: IndexPath) -> MessageFooterView { + let footer = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: identifier, for: indexPath) + return footer as? MessageFooterView ?? MessageFooterView() + } + } diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index f0b2fc12..9315ce5e 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -25,32 +25,32 @@ import UIKit open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { - + // MARK: - Properties - + open var messageFont: UIFont - + open var incomingAvatarSize: CGSize - + open var outgoingAvatarSize: CGSize - + open var messageContainerInsets: UIEdgeInsets - + fileprivate let avatarBottomSpacing: CGFloat = 4 - + fileprivate let avatarContainerSpacing: CGFloat = 4 - + fileprivate var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.frame.width - sectionInset.left - sectionInset.right } - + override open class var layoutAttributesClass: AnyClass { return MessagesCollectionViewLayoutAttributes.self } - + // MARK: - Initializers - + override public init() { messageFont = UIFont.preferredFont(forTextStyle: .body) incomingAvatarSize = CGSize(width: 30, height: 30) @@ -59,49 +59,49 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { super.init() sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Methods - + override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } - + attributesArray.forEach { attributes in if attributes.representedElementCategory == UICollectionElementCategory.cell { configure(attributes: attributes) } } - + return attributesArray } - + override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - + guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } - + if attributes.representedElementCategory == UICollectionElementCategory.cell { configure(attributes: attributes) } - + return attributes } - + private func configure(attributes: MessagesCollectionViewLayoutAttributes) { - + guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return } - + let indexPath = attributes.indexPath let message = dataSource.messageForItem(at: indexPath, in: collectionView) - + let direction: MessageDirection = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming let avatarSize = avatarSizeFor(message: message) let messageContainerSize = containerSizeFor(message: message) - + attributes.direction = direction attributes.messageFont = messageFont attributes.messageContainerSize = messageContainerSize @@ -111,31 +111,38 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { attributes.avatarContainerSpacing = avatarContainerSpacing } - + override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - - return collectionView?.bounds.width != newBounds.width - + + return collectionView?.bounds.width != newBounds.width || collectionView?.bounds.height != newBounds.height + } - + + open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { + let context = super.invalidationContext(forBoundsChange: newBounds) + guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } + flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) + return flowLayoutContext + } + } extension MessagesCollectionViewFlowLayout { - + func avatarSizeFor(message: MessageType) -> CGSize { - + guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return .zero } - + return dataSource.isFromCurrentSender(message: message) ? outgoingAvatarSize : incomingAvatarSize } - + func minimumCellHeightFor(message: MessageType) -> CGFloat { - + guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return 0 } - + let messageDirection: MessageDirection = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming - + switch messageDirection { case .incoming: return incomingAvatarSize.height + avatarBottomSpacing @@ -144,13 +151,13 @@ extension MessagesCollectionViewFlowLayout { } } - + func containerHeightForMessage(message: MessageType) -> CGFloat { - + let avatarSize = avatarSizeFor(message: message) let insets = messageContainerInsets.left + messageContainerInsets.right let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets - + // This is a switch because support for more messages are to come switch message.data { case .text(let text): @@ -160,15 +167,15 @@ extension MessagesCollectionViewFlowLayout { } } - + func containerWidthForMessage(message: MessageType) -> CGFloat { - + let containerHeight = containerHeightForMessage(message: message) - + let avatarSize = avatarSizeFor(message: message) let insets = messageContainerInsets.left + messageContainerInsets.right let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets - + // This is a switch because support for more messages are to come switch message.data { case .text(let text): @@ -179,36 +186,35 @@ extension MessagesCollectionViewFlowLayout { } } - + func estimatedCellHeightForMessage(message: MessageType) -> CGFloat { - + let messageContainerHeight = containerHeightForMessage(message: message) return messageContainerHeight } - + func containerSizeFor(message: MessageType) -> CGSize { - + let containerHeight = containerHeightForMessage(message: message) let containerWidth = containerWidthForMessage(message: message) - + return CGSize(width: containerWidth, height: containerHeight) - + } - + func sizeForItem(at indexPath: IndexPath) -> CGSize { - + guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return .zero } - + let message = dataSource.messageForItem(at: indexPath, in: collectionView) - + let minHeight = minimumCellHeightFor(message: message) let estimatedHeight = estimatedCellHeightForMessage(message: message) let actualHeight = estimatedHeight < minHeight ? minHeight : estimatedHeight - + return CGSize(width: itemWidth, height: actualHeight) - + } + } - - diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 4c84a25b..4cbfa6b4 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -25,26 +25,27 @@ import UIKit final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { - + // MARK: - Properties - + var direction: MessageDirection = .outgoing var messageFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - + var messageContainerSize: CGSize = .zero - + var messageContainerInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) - + var avatarSize: CGSize = CGSize(width: 30, height: 30) - + var avatarBottomSpacing: CGFloat = 4 - + var avatarContainerSpacing: CGFloat = 4 - + // MARK: - Methods - + override func copy(with zone: NSZone? = nil) -> Any { + // swiftlint:disable force_cast let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes copy.direction = direction copy.messageFont = messageFont @@ -54,15 +55,18 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib copy.avatarBottomSpacing = avatarBottomSpacing copy.avatarContainerSpacing = avatarContainerSpacing return copy + // swiftlint:enable force_cast } override func isEqual(_ object: Any?) -> Bool { - + // MARK: - LEAVE this as is + // swiftlint:disable unused_optional_binding if let _ = object as? MessagesCollectionViewLayoutAttributes { return super.isEqual(object) } else { return false } + // swiftlint:enable unused_optional_binding } } diff --git a/Sources/MessagesDataSource.swift b/Sources/MessagesDataSource.swift index b306d23b..ac275cd8 100644 --- a/Sources/MessagesDataSource.swift +++ b/Sources/MessagesDataSource.swift @@ -26,20 +26,20 @@ import UIKit public protocol MessagesDataSource: class { - func currentSender() -> Sender // if this is a function we can conform via extension + func currentSender() -> Sender + + func isFromCurrentSender(message: MessageType) -> Bool + + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType + + func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int - func messageForItem(at indexPath: IndexPath, in collectionView: UICollectionView) -> MessageType - - func numberOfMessages(in collectionView: UICollectionView) -> Int - } public extension MessagesDataSource { - - // Pros and cons of not having this defined in the protocol? No idea yet. + func isFromCurrentSender(message: MessageType) -> Bool { return message.sender == currentSender() } + } - - diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index a301b4c1..35c92b82 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -1,24 +1,53 @@ -// -// MessagesDisplayDataSource.swift -// MessageKit -// -// Created by Steven on 7/26/17. -// Copyright © 2017 Hexed Bits. 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 -// I don't want this to inherit from MessagesDataSource really but I need access to isFromCurrentSender(message:) for now public protocol MessagesDisplayDataSource: class, MessagesDataSource { - - func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in collectionView: UICollectionView) -> Avatar - + + func messageColorFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor + + func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar + + func headerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? + + func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? + } public extension MessagesDisplayDataSource { - - func messageColorFor(_ message: MessageType, at indexPath: IndexPath, in collectionView: UICollectionView) -> UIColor { + + func messageColorFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .outgoingGreen : .incomingGray } - + + func headerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { + return nil + } + + func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? { + return nil + } + } diff --git a/Sources/MessagesLayoutDelegate.swift b/Sources/MessagesLayoutDelegate.swift new file mode 100644 index 00000000..4ee3753a --- /dev/null +++ b/Sources/MessagesLayoutDelegate.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 protocol MessagesLayoutDelegate: class { + + func headerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + + func footerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + +} + +extension MessagesLayoutDelegate { + + func headerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return .zero + } + + func footerSizeFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return .zero + } + +} diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 65ed1136..1105322e 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -22,188 +22,240 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - import UIKit - open class MessagesViewController: UIViewController { - + // MARK: - Properties - + open var messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) - + open var messageInputBar = MessageInputBar() - + override open var canBecomeFirstResponder: Bool { return true } - + override open var inputAccessoryView: UIView? { - messageInputBar.bounds.size = CGSize(width: messagesCollectionView.frame.width, height: 48) return messageInputBar } - + + open override var shouldAutorotate: Bool { + return false + } + // MARK: - View Life Cycle - + open override func viewDidLoad() { super.viewDidLoad() - + automaticallyAdjustsScrollViewInsets = false - + setupSubviews() setupConstraints() registerReusableViews() setupDelegates() - + } - + + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + messagesCollectionView.scrollToBottom(animated: true) + } + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // depends on inputAccessoryView frame thus must be called here addKeyboardObservers() + messagesCollectionView.scrollToBottom(animated: false) } - + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) removeKeyboardObservers() } - + // MARK: - Methods - + private func setupDelegates() { messagesCollectionView.delegate = self messagesCollectionView.dataSource = self } - + private func registerReusableViews() { + messagesCollectionView.register(MessageCollectionViewCell.self, forCellWithReuseIdentifier: "MessageCell") + + messagesCollectionView.register(MessageHeaderView.self, + forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, + withReuseIdentifier: "MessageHeader") + + messagesCollectionView.register(MessageFooterView.self, + forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, + withReuseIdentifier: "MessageFooter") } - + private func setupSubviews() { messagesCollectionView.keyboardDismissMode = .interactive view.addSubview(messagesCollectionView) } - + private func setupConstraints() { - + messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false - view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)) - view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0)) - view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0)) - view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .bottom, relatedBy: .equal, toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -48)) - + + view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .top, relatedBy: .equal, + toItem: topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0)) + + view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .leading, relatedBy: .equal, + toItem: view, attribute: .leading, multiplier: 1, constant: 0)) + + view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .trailing, relatedBy: .equal, + toItem: view, attribute: .trailing, multiplier: 1, constant: 0)) + + view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .bottom, relatedBy: .equal, + toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -48)) + } - + } // MARK: - UICollectionViewDelegate & UICollectionViewDelegateFlowLayout Conformance +//swiftlint:disable line_length + extension MessagesViewController: UICollectionViewDelegateFlowLayout { - + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero } return messagesFlowLayout.sizeForItem(at: indexPath) } - + } +//swiftlint:enable line_length // MARK: - UICollectionViewDataSource Conformance extension MessagesViewController: UICollectionViewDataSource { - + public func numberOfSections(in collectionView: UICollectionView) -> Int { guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - + // Each message is its own section return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 } - + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - + let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 // There will only ever be 1 message per section return messageCount > 0 ? 1 : 0 - + } - + + //swiftlint:disable line_length + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MessageCell", for: indexPath) as! MessageCollectionViewCell - - if let messagesCollectionView = collectionView as? MessagesCollectionView, - let dataSource = messagesCollectionView.messagesDataSource, - let displayDataSource = messagesCollectionView.messagesDisplayDataSource { - - let message = dataSource.messageForItem(at: indexPath, in: collectionView) - let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: collectionView) - let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: collectionView) - - cell.avatarImageView.image = avatar.image(highlighted: false) - cell.avatarImageView.highlightedImage = avatar.image(highlighted: true) - cell.messageContainerView.backgroundColor = messageColor - cell.configure(with: message) - - } - + + 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 } + guard let displayDataSource = messagesDataSource as? MessagesDisplayDataSource else { return cell } + + let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: messagesCollectionView) + let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: messagesCollectionView) + + cell.avatarImageView.image = avatar.image(highlighted: false) + cell.avatarImageView.highlightedImage = avatar.image(highlighted: true) + cell.messageContainerView.backgroundColor = messageColor + cell.configure(with: message) + return cell - + } + + public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return UICollectionReusableView() } + guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return UICollectionReusableView() } + + let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch kind { + case UICollectionElementKindSectionHeader: + return displayDataSource.headerForMessage(message, at: indexPath, in: messagesCollectionView) ?? MessageHeaderView() + case UICollectionElementKindSectionFooter: + return displayDataSource.footerForMessage(message, at: indexPath, in: messagesCollectionView) ?? MessageFooterView() + default: + fatalError("Unrecognized element of kind: \(kind)") + } + + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + // Could pose a problem if subclass behaviors allows more than one item per section + let indexPath = IndexPath(item: 0, section: section) + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return messagesLayoutDelegate.headerSizeFor(message, at: indexPath, in: messagesCollectionView) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + // Could pose a problem if subclass behaviors allows more than one item per section + let indexPath = IndexPath(item: 0, section: section) + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return messagesLayoutDelegate.footerSizeFor(message, at: indexPath, in: messagesCollectionView) + } + + //swiftlint:enable line_length + } - -// MARK: - Keyboard methods +// MARK: - Keyboard Handling extension MessagesViewController { - + fileprivate func addKeyboardObservers() { - - NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: .UIKeyboardDidShow, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: .UIKeyboardWillHide, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil) - - } - - fileprivate func removeKeyboardObservers() { - - NotificationCenter.default.removeObserver(self, name: .UIKeyboardDidShow, object: nil) - NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil) - NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) - } - - func handleKeyboardDidShow(_ notification: Notification) { - - guard let indexPath = messagesCollectionView.indexPathForLastItem else { return } - - if messageInputBar.inputTextView.isFirstResponder { - messagesCollectionView.scrollToItem(at: indexPath, at: .bottom, animated: true) - } - } - + + } + + fileprivate func removeKeyboardObservers() { + + NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil) + NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, 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 messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) + } - + } - - - - - - - - - - diff --git a/Sources/Sender.swift b/Sources/Sender.swift index 8e753ed6..b8f4baec 100644 --- a/Sources/Sender.swift +++ b/Sources/Sender.swift @@ -25,11 +25,11 @@ import Foundation public struct Sender { - + public let id: String - + public let displayName: String - + public init(id: String, displayName: String) { self.id = id self.displayName = displayName @@ -39,7 +39,7 @@ public struct Sender { // MARK: - Equatable Conformance extension Sender: Equatable { - static public func ==(left: Sender, right: Sender) -> Bool { + static public func == (left: Sender, right: Sender) -> Bool { return left.id == right.id } } diff --git a/Sources/String+Extensions.swift b/Sources/String+Extensions.swift index dfdf557a..e3930408 100644 --- a/Sources/String+Extensions.swift +++ b/Sources/String+Extensions.swift @@ -25,20 +25,20 @@ import Foundation extension String { - + func height(considering width: CGFloat, and font: UIFont) -> CGFloat { - + let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) - let boundRect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil) - return boundRect.height + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil) + return rect.height } - + func width(considering height: CGFloat, and font: UIFont) -> CGFloat { - + let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let boundRect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil) - return boundRect.width + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil) + return rect.width } } diff --git a/Sources/UIColor+Extensions.swift b/Sources/UIColor+Extensions.swift index e19069bc..04bc89f7 100644 --- a/Sources/UIColor+Extensions.swift +++ b/Sources/UIColor+Extensions.swift @@ -25,11 +25,11 @@ import Foundation extension UIColor { - + static let incomingGray = UIColor(colorLiteralRed: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) - + static let outgoingGreen = UIColor(colorLiteralRed: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) - + static let inputBarGray = UIColor(colorLiteralRed: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) - + } diff --git a/Tests/MessageKitTests.swift b/Tests/MessageKitTests.swift index babf355f..f4c98757 100644 --- a/Tests/MessageKitTests.swift +++ b/Tests/MessageKitTests.swift @@ -17,22 +17,20 @@ // import XCTest -@testable import MessageKit - final class MessageKitTests: XCTestCase { - + override func setUp() { super.setUp() } - + override func tearDown() { super.tearDown() } - + func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - + }