mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
@@ -0,0 +1,6 @@
|
||||
line_length:
|
||||
warning: 150
|
||||
ignores_comments: true
|
||||
|
||||
disabled_rules:
|
||||
identifier_name
|
||||
+21
-1
@@ -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()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -25,11 +25,10 @@
|
||||
import UIKit
|
||||
import MessageKit
|
||||
|
||||
|
||||
final class SettingsViewController: UITableViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.'
|
||||
|
||||
@@ -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 */,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user