Merge pull request #25 from MessageKit/v0.2.0

v0.2.0
This commit is contained in:
Steven Deutsch
2017-08-05 00:14:31 -05:00
committed by GitHub
28 changed files with 790 additions and 339 deletions
+6
View File
@@ -0,0 +1,6 @@
line_length:
warning: 150
ignores_comments: true
disabled_rules:
identifier_name
+21 -1
View File
@@ -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:
@@ -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()
}
}
+2 -4
View File
@@ -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 {
}
}
+3 -4
View File
@@ -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()
}
}
+1 -2
View File
@@ -25,11 +25,10 @@
import UIKit
import MessageKit
final class SettingsViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
}
+4 -3
View File
@@ -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.
}
}
+8 -5
View File
@@ -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 its important to set the initial state - such as interface orientation - required for your tests before they run.
// In UI tests its 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.
}
}
+1 -1
View File
@@ -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.'
+33
View File
@@ -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 = "<group>"; };
B015E8181F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = "<group>"; };
B015E81E1F259D8E007EDFB6 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = "<group>"; };
B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = "<group>"; };
B0655A251F23D6C500542A83 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
B0655A271F23D71400542A83 /* MessageDirection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDirection.swift; sourceTree = "<group>"; };
B0655A291F23D77200542A83 /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = "<group>"; };
@@ -58,6 +63,9 @@
B0655A371F23EE8B00542A83 /* MessageInputBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = "<group>"; };
B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = "<group>"; };
B0655A4E1F245C5A00542A83 /* MessagesCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewFlowLayout.swift; sourceTree = "<group>"; };
B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = "<group>"; };
B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = "<group>"; };
B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = "<group>"; };
B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = "<group>"; };
B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
@@ -149,6 +157,8 @@
B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */,
B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */,
B0655A371F23EE8B00542A83 /* MessageInputBar.swift */,
B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */,
B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */,
);
name = Views;
sourceTree = "<group>";
@@ -160,6 +170,8 @@
88916B461CF0DFE600469F91 /* MessageType.swift */,
882D75831DE507320033F95F /* MessagesDataSource.swift */,
B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */,
B03FF9AE1F31BB1200754FE5 /* MessageCellDelegate.swift */,
B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */,
);
name = Protocols;
sourceTree = "<group>";
@@ -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 */,
);
+6 -6
View File
@@ -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
+41
View File
@@ -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) {}
}
+58 -28
View File
@@ -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)
}
}
+3 -3
View File
@@ -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)
+37
View File
@@ -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")
}
}
+37
View File
@@ -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")
}
}
+68 -26
View File
@@ -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)
}
+36 -15
View File
@@ -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()
}
}
+65 -59
View File
@@ -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)
}
}
@@ -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
}
}
+9 -9
View File
@@ -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()
}
}
+43 -14
View File
@@ -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
}
}
+45
View File
@@ -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
}
}
+146 -94
View File
@@ -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)
}
}
+4 -4
View File
@@ -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
}
}
+8 -8
View File
@@ -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
}
}
+4 -4
View File
@@ -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)
}
+4 -6
View File
@@ -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.
}
}