mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
@@ -6,11 +6,45 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa
|
||||
|
||||
## Upcoming release
|
||||
|
||||
## [[Prerelease] 0.13.0](https://github.com/MessageKit/MessageKit/releases/tag/0.13.0)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed message rendering when `MessagesViewController` sliding back.
|
||||
[#454](https://github.com/MessageKit/MessageKit/pull/454) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- Fixed `iPhoneX` `MessageInputBar` transparent bottom area when `keyboardDismissMode` is `interactive`.
|
||||
[#425](https://github.com/MessageKit/MessageKit/pull/425) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- Fixed wrong `contentInset` calculation when `navigationBar` is `hidden`
|
||||
[#444](https://github.com/MessageKit/MessageKit/pull/444) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
### Added
|
||||
|
||||
- Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`.
|
||||
[#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- Added copy support for image, text, and emoji messages.
|
||||
[#418](https://github.com/MessageKit/MessageKit/pull/418) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- Added `UIImage` paste support to the `InputTextView`. Images can easily be accessed using the `InputTextView.images` property.
|
||||
See the example project for an updated use case.
|
||||
[#423](https://github.com/MessageKit/MessageKit/pull/423) by [@nathantannar4](https://github.com/nathantannar4).
|
||||
|
||||
### Removed
|
||||
|
||||
- **Breaking Change** Removed `avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method of `MessagesDataSource`, use `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` instead.
|
||||
[#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking Change** Moved the `handleTapGesture(_ gesture: UIGestureRecognizer)` method from `MessagesCollectionViewCell` to `MessagesCollectionView`.
|
||||
[#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- **Breaking Change** Changed `AvatarView` from type `UIView` to type `UIImageView`.
|
||||
[#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
|
||||
## [[Prerelease] 0.12.1](https://github.com/MessageKit/MessageKit/releases/tag/0.12.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
B096438B1F288D47004D0129 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438A1F288D47004D0129 /* MockMessage.swift */; };
|
||||
C1DF6DF39F66906000EC76CF /* Pods_ChatExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */; };
|
||||
C7CA53A1B85256A5097E7DC7 /* Pods_ChatExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */; };
|
||||
CAB36EA12007A573009995ED /* TableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB36EA02007A573009995ED /* TableViewCells.swift */; };
|
||||
CAB36EA32007B1B7009995ED /* Settings+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -79,6 +81,8 @@
|
||||
B0DD3C951C9D064B5E6D6644 /* Pods-ChatExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B2F1C412A96DE613A0AC31F8 /* Pods-ChatExampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
BFE5859D088A740A7D43E1B1 /* Pods-ChatExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
CAB36EA02007A573009995ED /* TableViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCells.swift; sourceTree = "<group>"; };
|
||||
CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+UserDefaults.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -157,6 +161,8 @@
|
||||
882B5E791CF7D53600B6E160 /* Assets.xcassets */,
|
||||
882B5E7F1CF7D53600B6E160 /* Info.plist */,
|
||||
882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */,
|
||||
CAB36EA02007A573009995ED /* TableViewCells.swift */,
|
||||
CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@@ -485,6 +491,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CAB36EA12007A573009995ED /* TableViewCells.swift in Sources */,
|
||||
CAB36EA32007B1B7009995ED /* Settings+UserDefaults.swift in Sources */,
|
||||
882B5E871CF7D53600B6E160 /* SettingsViewController.swift in Sources */,
|
||||
37D3EAC41F390E5F00DD6A55 /* SampleData.swift in Sources */,
|
||||
B096438B1F288D47004D0129 /* MockMessage.swift in Sources */,
|
||||
|
||||
@@ -37,8 +37,10 @@ class ConversationViewController: MessagesViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let messagesToFetch = UserDefaults.standard.mockMessagesCount()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
SampleData.shared.getMessages(count: 10) { messages in
|
||||
SampleData.shared.getMessages(count: messagesToFetch) { messages in
|
||||
DispatchQueue.main.async {
|
||||
self.messageList = messages
|
||||
self.messagesCollectionView.reloadData()
|
||||
@@ -58,17 +60,17 @@ class ConversationViewController: MessagesViewController {
|
||||
maintainPositionOnKeyboardFrameChanged = true // default false
|
||||
|
||||
messagesCollectionView.addSubview(refreshControl)
|
||||
refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
|
||||
refreshControl.addTarget(self, action: #selector(ConversationViewController.loadMoreMessages), for: .valueChanged)
|
||||
|
||||
navigationItem.rightBarButtonItems = [
|
||||
UIBarButtonItem(image: UIImage(named: "ic_keyboard"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(handleKeyboardButton)),
|
||||
action: #selector(ConversationViewController.handleKeyboardButton)),
|
||||
UIBarButtonItem(image: UIImage(named: "ic_typing"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(handleTyping))
|
||||
action: #selector(ConversationViewController.handleTyping))
|
||||
]
|
||||
}
|
||||
|
||||
@@ -267,10 +269,6 @@ extension ConversationViewController: MessagesDataSource {
|
||||
return messageList[indexPath.section]
|
||||
}
|
||||
|
||||
func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar {
|
||||
return SampleData.shared.getAvatarFor(sender: message.sender)
|
||||
}
|
||||
|
||||
func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
|
||||
let name = message.sender.displayName
|
||||
return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
|
||||
@@ -322,6 +320,11 @@ extension ConversationViewController: MessagesDisplayDelegate {
|
||||
// let configurationClosure = { (view: MessageContainerView) in}
|
||||
// return .custom(configurationClosure)
|
||||
}
|
||||
|
||||
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
|
||||
let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
|
||||
avatarView.set(avatar: avatar)
|
||||
}
|
||||
|
||||
// MARK: - Location Messages
|
||||
|
||||
@@ -343,6 +346,11 @@ extension ConversationViewController: MessagesDisplayDelegate {
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MessagesLayoutDelegate
|
||||
@@ -439,12 +447,29 @@ extension ConversationViewController: MessageLabelDelegate {
|
||||
extension ConversationViewController: MessageInputBarDelegate {
|
||||
|
||||
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
|
||||
let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 8), .foregroundColor: UIColor.blue])
|
||||
let id = UUID().uuidString
|
||||
let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: id, date: Date())
|
||||
messageList.append(message)
|
||||
|
||||
// Each NSTextAttachment that contains an image will count as one empty character in the text: String
|
||||
|
||||
for component in inputBar.inputTextView.components {
|
||||
|
||||
if let image = component as? UIImage {
|
||||
|
||||
let imageMessage = MockMessage(image: image, sender: currentSender(), messageId: UUID().uuidString, date: Date())
|
||||
messageList.append(imageMessage)
|
||||
messagesCollectionView.insertSections([messageList.count - 1])
|
||||
|
||||
} else if let text = component as? String {
|
||||
|
||||
let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 15), .foregroundColor: UIColor.blue])
|
||||
|
||||
let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: UUID().uuidString, date: Date())
|
||||
messageList.append(message)
|
||||
messagesCollectionView.insertSections([messageList.count - 1])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inputBar.inputTextView.text = String()
|
||||
messagesCollectionView.insertSections([messageList.count - 1])
|
||||
messagesCollectionView.scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
static let messagesKey = "mockMessages"
|
||||
|
||||
// MARK: - Mock Messages
|
||||
|
||||
func setMockMessages(count: Int) {
|
||||
set(count, forKey: "mockMessages")
|
||||
synchronize()
|
||||
}
|
||||
|
||||
func mockMessagesCount() -> Int {
|
||||
if let value = object(forKey: "mockMessages") as? Int {
|
||||
return value
|
||||
}
|
||||
return 20
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,108 @@ import MessageKit
|
||||
|
||||
final class SettingsViewController: UITableViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var selectedMockMessagesCount: Int = 20
|
||||
|
||||
// MARK: - Picker
|
||||
|
||||
var messagesPicker = UIPickerView()
|
||||
|
||||
@objc func onDoneWithPickerView() {
|
||||
let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0)
|
||||
UserDefaults.standard.setMockMessages(count: selectedMessagesCount)
|
||||
view.endEditing(false)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func dismissPickerView() {
|
||||
view.endEditing(false)
|
||||
}
|
||||
|
||||
private func configurePickerView() {
|
||||
messagesPicker.dataSource = self
|
||||
messagesPicker.delegate = self
|
||||
messagesPicker.backgroundColor = .white
|
||||
}
|
||||
|
||||
// MARK: - Toolbar
|
||||
|
||||
var messagesToolbar = UIToolbar()
|
||||
|
||||
private func configureToolbar() {
|
||||
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView))
|
||||
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView))
|
||||
messagesToolbar.items = [cancelButton, spaceButton, doneButton]
|
||||
messagesToolbar.sizeToFit()
|
||||
}
|
||||
|
||||
// MARK: - View lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier)
|
||||
|
||||
configurePickerView()
|
||||
configureToolbar()
|
||||
}
|
||||
|
||||
// MARK: - TableViewDelegate & TableViewDataSource
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
return indexPath.row == 0 ? configureTextFieldTableViewCell(at: indexPath) : UITableViewCell()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let cell = tableView.cellForRow(at: indexPath)
|
||||
|
||||
cell?.contentView.subviews.forEach {
|
||||
if $0 is UITextField {
|
||||
$0.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as! TextFieldTableViewCell
|
||||
cell.mainLabel.text = "Mock messages count:"
|
||||
|
||||
let messagesCount = UserDefaults.standard.mockMessagesCount()
|
||||
cell.textField.text = "\(messagesCount)"
|
||||
|
||||
cell.textField.inputView = messagesPicker
|
||||
cell.textField.inputAccessoryView = messagesToolbar
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIPickerViewDelegate, UIPickerViewDataSource
|
||||
extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource {
|
||||
|
||||
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
return 100
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
return "\(row)"
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
selectedMockMessagesCount = row
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class TextFieldTableViewCell: UITableViewCell {
|
||||
|
||||
static let identifier = "TextFieldTableViewCellIdentifier"
|
||||
|
||||
var mainLabel = UILabel()
|
||||
var textField = UITextField()
|
||||
|
||||
// MARK: - View lifecycle
|
||||
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
mainLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
contentView.addSubview(mainLabel)
|
||||
contentView.addSubview(textField)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
mainLabel.widthAnchor.constraint(equalToConstant: 200),
|
||||
mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
||||
textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
||||
textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
|
||||
textField.widthAnchor.constraint(equalToConstant: 50)
|
||||
])
|
||||
|
||||
textField.textAlignment = .right
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'MessageKit'
|
||||
s.version = '0.12.1'
|
||||
s.version = '0.13.0'
|
||||
s.license = { :type => "MIT", :file => "LICENSE.md" }
|
||||
|
||||
s.summary = 'An elegant messages UI library for iOS.'
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; };
|
||||
1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; };
|
||||
1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; };
|
||||
1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; };
|
||||
1FF377A620087D20004FD648 /* MessagesViewController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */; };
|
||||
1FF377A820087D56004FD648 /* MessagesViewController+Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A720087D56004FD648 /* MessagesViewController+Delegate.swift */; };
|
||||
1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; };
|
||||
1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; };
|
||||
38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */; };
|
||||
38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */; };
|
||||
88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; };
|
||||
@@ -115,6 +120,11 @@
|
||||
1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = "<group>"; };
|
||||
1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = "<group>"; };
|
||||
1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = "<group>"; };
|
||||
1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = "<group>"; };
|
||||
1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+DataSource.swift"; sourceTree = "<group>"; };
|
||||
1FF377A720087D56004FD648 /* MessagesViewController+Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Delegate.swift"; sourceTree = "<group>"; };
|
||||
1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = "<group>"; };
|
||||
1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = "<group>"; };
|
||||
38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorLine.swift; sourceTree = "<group>"; };
|
||||
38C57C7B1F9AE4870043CC03 /* InputStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputStackView.swift; sourceTree = "<group>"; };
|
||||
88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -373,6 +383,7 @@
|
||||
B7A03F221F866895006AEF79 /* LabelAlignment.swift */,
|
||||
B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */,
|
||||
B7A03F231F866895006AEF79 /* MessageData.swift */,
|
||||
1FF377A320087C82004FD648 /* MessageKitError.swift */,
|
||||
B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */,
|
||||
B7A03F1F1F866895006AEF79 /* MessageStyle.swift */,
|
||||
B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */,
|
||||
@@ -418,6 +429,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */,
|
||||
1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */,
|
||||
1FF377A720087D56004FD648 /* MessagesViewController+Delegate.swift */,
|
||||
1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */,
|
||||
1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@@ -571,12 +586,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */,
|
||||
1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */,
|
||||
38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */,
|
||||
B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */,
|
||||
B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */,
|
||||
B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */,
|
||||
B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */,
|
||||
B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */,
|
||||
1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */,
|
||||
B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */,
|
||||
B006FA021F99DE2100509C46 /* MessageIntermediateLayoutAttributes.swift in Sources */,
|
||||
B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */,
|
||||
@@ -610,14 +627,17 @@
|
||||
0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */,
|
||||
B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */,
|
||||
B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */,
|
||||
1FF377A620087D20004FD648 /* MessagesViewController+DataSource.swift in Sources */,
|
||||
B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */,
|
||||
B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */,
|
||||
B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */,
|
||||
B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */,
|
||||
38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */,
|
||||
B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */,
|
||||
1FF377A820087D56004FD648 /* MessagesViewController+Delegate.swift in Sources */,
|
||||
B7A03F271F866895006AEF79 /* Avatar.swift in Sources */,
|
||||
1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */,
|
||||
1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -627,7 +647,6 @@
|
||||
files = (
|
||||
8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */,
|
||||
1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */,
|
||||
8962AC971F87AB860030B058 /* DetectorTypeTests.swift in Sources */,
|
||||
8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */,
|
||||
8962AC8E1F87AB7D0030B058 /* InputTextViewTests.swift in Sources */,
|
||||
8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */,
|
||||
@@ -637,7 +656,6 @@
|
||||
8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */,
|
||||
1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */,
|
||||
1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */,
|
||||
1F7FC8CC1FD2700B006CC979 /* MessagesViewControllerSpec.swift in Sources */,
|
||||
8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */,
|
||||
8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */,
|
||||
8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 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
|
||||
|
||||
extension MessagesViewController: UICollectionViewDataSource {
|
||||
|
||||
open func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
guard let collectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
// Each message is its own section
|
||||
return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
guard let collectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0
|
||||
// There will only ever be 1 message per section
|
||||
return messageCount > 0 ? 1 : 0
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch message.data {
|
||||
case .text, .attributedText, .emoji:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
case .photo, .video:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
case .location:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError(MessageKitError.nilMessagesDisplayDelegate)
|
||||
}
|
||||
|
||||
let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch kind {
|
||||
case UICollectionElementKindSectionHeader:
|
||||
return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView)
|
||||
case UICollectionElementKindSectionFooter:
|
||||
return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView)
|
||||
default:
|
||||
fatalError(MessageKitError.unrecognizedSectionKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 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
|
||||
|
||||
extension MessagesViewController: UICollectionViewDelegateFlowLayout {
|
||||
|
||||
open 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)
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
|
||||
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
|
||||
fatalError(MessageKitError.nilMessagesLayoutDeleagte)
|
||||
}
|
||||
// Could pose a problem if subclass behaviors allows more than one item per section
|
||||
let indexPath = IndexPath(item: 0, section: section)
|
||||
let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
return layoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError(MessageKitError.notMessagesCollectionView)
|
||||
}
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
|
||||
fatalError(MessageKitError.nilMessagesLayoutDeleagte)
|
||||
}
|
||||
// Could pose a problem if subclass behaviors allows more than one item per section
|
||||
let indexPath = IndexPath(item: 0, section: section)
|
||||
let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
return layoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false }
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch message.data {
|
||||
case .text, .attributedText, .emoji, .photo:
|
||||
selectedIndexPathForMenu = indexPath
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
|
||||
return (action == NSSelectorFromString("copy:"))
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
let pasteBoard = UIPasteboard.general
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch message.data {
|
||||
case .text(let text), .emoji(let text):
|
||||
pasteBoard.string = text
|
||||
case .attributedText(let attributedText):
|
||||
pasteBoard.string = attributedText.string
|
||||
case .photo(let image):
|
||||
pasteBoard.image = image
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MessagesViewController {
|
||||
|
||||
// MARK: - Register / Unregister Observers
|
||||
|
||||
func addKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: .UIKeyboardWillChangeFrame, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: .UITextViewTextDidBeginEditing, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil)
|
||||
}
|
||||
|
||||
func removeKeyboardObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidBeginEditing, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .UIDeviceOrientationDidChange, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Notification Handlers
|
||||
|
||||
@objc
|
||||
private func handleTextViewDidBeginEditing(_ notification: Notification) {
|
||||
if scrollsToBottomOnKeybordBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return }
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func handleKeyboardDidChangeState(_ notification: Notification) {
|
||||
guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
|
||||
if (keyboardEndFrame.origin.y + keyboardEndFrame.size.height) > UIScreen.main.bounds.height {
|
||||
// Hardware keyboard is found
|
||||
messageCollectionViewBottomInset = view.frame.size.height - keyboardEndFrame.origin.y - iPhoneXBottomInset
|
||||
} else {
|
||||
//Software keyboard is found
|
||||
let afterBottomInset = keyboardEndFrame.height > keyboardOffsetFrame.height ? (keyboardEndFrame.height - iPhoneXBottomInset) : keyboardOffsetFrame.height
|
||||
let differenceOfBottomInset = afterBottomInset - messageCollectionViewBottomInset
|
||||
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
|
||||
|
||||
if maintainPositionOnKeyboardFrameChanged {
|
||||
messagesCollectionView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
messageCollectionViewBottomInset = afterBottomInset
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func adjustScrollViewInset() {
|
||||
if #available(iOS 11.0, *) {
|
||||
// No need to add to the top contentInset
|
||||
} else {
|
||||
let navigationBarInset = navigationController?.navigationBar.frame.height ?? 0
|
||||
let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20
|
||||
let topInset = navigationBarInset + statusBarInset
|
||||
messagesCollectionView.contentInset.top = topInset
|
||||
messagesCollectionView.scrollIndicatorInsets.top = topInset
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
var keyboardOffsetFrame: CGRect {
|
||||
guard let inputFrame = inputAccessoryView?.frame else { return .zero }
|
||||
return CGRect(origin: inputFrame.origin, size: CGSize(width: inputFrame.width, height: inputFrame.height - iPhoneXBottomInset))
|
||||
}
|
||||
|
||||
/// On the iPhone X the inputAccessoryView is anchored to the layoutMarginesGuide.bottom anchor
|
||||
/// so the frame of the inputAccessoryView is larger than the required offset
|
||||
/// for the MessagesCollectionView.
|
||||
///
|
||||
/// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0
|
||||
private var iPhoneXBottomInset: CGFloat {
|
||||
if #available(iOS 11.0, *) {
|
||||
guard UIScreen.main.nativeBounds.height == 2436 else { return 0 }
|
||||
return view.safeAreaInsets.bottom
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MessagesViewController {
|
||||
|
||||
// MARK: - Register / Unregister Observers
|
||||
|
||||
/// Add observer for `UIMenuControllerWillShowMenu` notification
|
||||
func addMenuControllerObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil)
|
||||
}
|
||||
|
||||
/// Remove observer for `UIMenuControllerWillShowMenu` notification
|
||||
func removeMenuControllerObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Notification Handlers
|
||||
|
||||
/// Show menuController and set target rect to selected bubble
|
||||
@objc
|
||||
private func menuControllerWillShow(_ notification: Notification) {
|
||||
|
||||
guard let currentMenuController = notification.object as? UIMenuController,
|
||||
let selectedIndexPath = selectedIndexPathForMenu else { return }
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil)
|
||||
defer {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(MessagesViewController.menuControllerWillShow(_:)),
|
||||
name: .UIMenuControllerWillShowMenu, object: nil)
|
||||
selectedIndexPathForMenu = nil
|
||||
}
|
||||
|
||||
currentMenuController.setMenuVisible(false, animated: false)
|
||||
|
||||
guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return }
|
||||
let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view)
|
||||
|
||||
var messageInputBarFrame: CGRect = .zero
|
||||
if let messageInputBarSuperview = messageInputBar.superview {
|
||||
messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview)
|
||||
}
|
||||
|
||||
var topNavigationBarFrame: CGRect = navigationBarFrame
|
||||
if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview {
|
||||
topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview)
|
||||
}
|
||||
|
||||
let menuHeight = currentMenuController.menuFrame.height
|
||||
|
||||
let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight)
|
||||
|
||||
var targetRect: CGRect = selectedCellMessageBubbleFrame
|
||||
currentMenuController.arrowDirection = .default
|
||||
|
||||
/// Message bubble intersects with navigationBar and keyboard
|
||||
if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) {
|
||||
let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2
|
||||
targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1)
|
||||
} /// Message bubble only intersects with navigationBar
|
||||
else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) {
|
||||
currentMenuController.arrowDirection = .up
|
||||
}
|
||||
|
||||
currentMenuController.setTargetRect(targetRect, in: view)
|
||||
currentMenuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private var navigationBarFrame: CGRect {
|
||||
guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else {
|
||||
return .zero
|
||||
}
|
||||
return navigationController.navigationBar.frame
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/*
|
||||
MIT License
|
||||
|
||||
@@ -58,11 +57,14 @@ open class MessagesViewController: UIViewController {
|
||||
open override var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
/// A Boolean value used to determine if `viewDidLayoutSubviews()` has been called.
|
||||
private var isFirstLayout: Bool = true
|
||||
|
||||
private var messageCollectionViewBottomInset: CGFloat = 0 {
|
||||
/// Indicated selected indexPath when handle menu action
|
||||
var selectedIndexPathForMenu: IndexPath?
|
||||
|
||||
var messageCollectionViewBottomInset: CGFloat = 0 {
|
||||
didSet {
|
||||
messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset
|
||||
messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset
|
||||
@@ -73,18 +75,12 @@ open class MessagesViewController: UIViewController {
|
||||
|
||||
open override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
view.backgroundColor = .white
|
||||
messagesCollectionView.keyboardDismissMode = .interactive
|
||||
messagesCollectionView.alwaysBounceVertical = true
|
||||
|
||||
setupDefaults()
|
||||
setupSubviews()
|
||||
setupConstraints()
|
||||
registerReusableViews()
|
||||
setupDelegates()
|
||||
|
||||
addMenuControllerObservers()
|
||||
}
|
||||
|
||||
open override func viewDidLayoutSubviews() {
|
||||
@@ -101,19 +97,33 @@ open class MessagesViewController: UIViewController {
|
||||
|
||||
deinit {
|
||||
removeKeyboardObservers()
|
||||
removeMenuControllerObservers()
|
||||
}
|
||||
|
||||
// MARK: - Methods [Private]
|
||||
|
||||
/// Sets the default values for the MessagesViewController
|
||||
private func setupDefaults() {
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
automaticallyAdjustsScrollViewInsets = false
|
||||
view.backgroundColor = .white
|
||||
messagesCollectionView.keyboardDismissMode = .interactive
|
||||
messagesCollectionView.alwaysBounceVertical = true
|
||||
}
|
||||
|
||||
/// Sets the delegate and dataSource of the messagesCollectionView property.
|
||||
private func setupDelegates() {
|
||||
messagesCollectionView.delegate = self
|
||||
messagesCollectionView.dataSource = self
|
||||
}
|
||||
|
||||
/// Adds the messagesCollectionView to the controllers root view.
|
||||
private func setupSubviews() {
|
||||
view.addSubview(messagesCollectionView)
|
||||
}
|
||||
|
||||
/// Registers all cells and supplementary views of the messagesCollectionView property.
|
||||
private func registerReusableViews() {
|
||||
|
||||
messagesCollectionView.register(TextMessageCell.self)
|
||||
messagesCollectionView.register(MediaMessageCell.self)
|
||||
messagesCollectionView.register(LocationMessageCell.self)
|
||||
@@ -121,12 +131,6 @@ open class MessagesViewController: UIViewController {
|
||||
messagesCollectionView.register(MessageFooterView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter)
|
||||
messagesCollectionView.register(MessageHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader)
|
||||
messagesCollectionView.register(MessageDateHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader)
|
||||
|
||||
}
|
||||
|
||||
/// Adds the messagesCollectionView to the controllers root view.
|
||||
private func setupSubviews() {
|
||||
view.addSubview(messagesCollectionView)
|
||||
}
|
||||
|
||||
/// Sets the constraints of the `MessagesCollectionView`.
|
||||
@@ -145,187 +149,4 @@ open class MessagesViewController: UIViewController {
|
||||
NSLayoutConstraint.activate([top, bottom, trailing, leading])
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func adjustScrollViewInset() {
|
||||
if #available(iOS 11.0, *) {
|
||||
// No need to add to the top contentInset
|
||||
} else {
|
||||
var navigationBarInset: CGFloat = 0
|
||||
if let navigationController = navigationController, !navigationController.isNavigationBarHidden {
|
||||
navigationBarInset = navigationController.navigationBar.frame.height
|
||||
}
|
||||
let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20
|
||||
let topInset = navigationBarInset + statusBarInset
|
||||
messagesCollectionView.contentInset.top = topInset
|
||||
messagesCollectionView.scrollIndicatorInsets.top = topInset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegate & UICollectionViewDelegateFlowLayout Conformance
|
||||
|
||||
extension MessagesViewController: UICollectionViewDelegateFlowLayout {
|
||||
|
||||
open 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)
|
||||
}
|
||||
|
||||
open 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.headerViewSize(for: message, at: indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
open 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.footerViewSize(for: message, at: indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource Conformance
|
||||
|
||||
extension MessagesViewController: UICollectionViewDataSource {
|
||||
|
||||
open 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
|
||||
}
|
||||
|
||||
open 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
|
||||
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.")
|
||||
}
|
||||
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError("MessagesDataSource has not been set.")
|
||||
}
|
||||
|
||||
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch message.data {
|
||||
case .text, .attributedText, .emoji:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
case .photo, .video:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
case .location:
|
||||
let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath)
|
||||
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
|
||||
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.")
|
||||
}
|
||||
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError("MessagesDataSource has not been set.")
|
||||
}
|
||||
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError("MessagesDisplayDelegate has not been set.")
|
||||
}
|
||||
|
||||
let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView)
|
||||
|
||||
switch kind {
|
||||
case UICollectionElementKindSectionHeader:
|
||||
return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView)
|
||||
case UICollectionElementKindSectionFooter:
|
||||
return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView)
|
||||
default:
|
||||
fatalError("Unrecognized element of kind: \(kind)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keyboard Handling
|
||||
|
||||
fileprivate extension MessagesViewController {
|
||||
|
||||
func addKeyboardObservers() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidChangeState), name: .UIKeyboardWillChangeFrame, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleTextViewDidBeginEditing), name: .UITextViewTextDidBeginEditing, object: nil)
|
||||
}
|
||||
|
||||
func removeKeyboardObservers() {
|
||||
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidBeginEditing, object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
func handleTextViewDidBeginEditing(_ notification: Notification) {
|
||||
if scrollsToBottomOnKeybordBeginsEditing {
|
||||
guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return }
|
||||
messagesCollectionView.scrollToBottom(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func handleKeyboardDidChangeState(_ notification: Notification) {
|
||||
guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return }
|
||||
|
||||
if (keyboardEndFrame.origin.y + keyboardEndFrame.size.height) > UIScreen.main.bounds.height {
|
||||
// Hardware keyboard is found
|
||||
messageCollectionViewBottomInset = view.frame.size.height - keyboardEndFrame.origin.y - iPhoneXBottomInset
|
||||
} else {
|
||||
//Software keyboard is found
|
||||
let afterBottomInset = keyboardEndFrame.height > keyboardOffsetFrame.height ? (keyboardEndFrame.height - iPhoneXBottomInset) : keyboardOffsetFrame.height
|
||||
let differenceOfBottomInset = afterBottomInset - messageCollectionViewBottomInset
|
||||
let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset)
|
||||
|
||||
if maintainPositionOnKeyboardFrameChanged {
|
||||
messagesCollectionView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
messageCollectionViewBottomInset = afterBottomInset
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var keyboardOffsetFrame: CGRect {
|
||||
guard let inputFrame = inputAccessoryView?.frame else { return .zero }
|
||||
return CGRect(origin: inputFrame.origin, size: CGSize(width: inputFrame.width, height: inputFrame.height - iPhoneXBottomInset))
|
||||
}
|
||||
|
||||
/// On the iPhone X the inputAccessoryView is anchored to the layoutMarginesGuide.bottom anchor so the frame of the inputAccessoryView
|
||||
/// is larger than the required offset for the MessagesCollectionView
|
||||
///
|
||||
/// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0
|
||||
fileprivate var iPhoneXBottomInset: CGFloat {
|
||||
if #available(iOS 11.0, *) {
|
||||
guard UIScreen.main.nativeBounds.height == 2436 else { return 0 }
|
||||
return view.safeAreaInsets.bottom
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ extension Bundle {
|
||||
let podBundle = Bundle(for: MessagesViewController.self)
|
||||
|
||||
guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else {
|
||||
fatalError("MessageKit: Could not create path to the assets bundle")
|
||||
fatalError(MessageKitError.couldNotCreateAssetsPath)
|
||||
}
|
||||
|
||||
guard let resourceBundle = Bundle(url: resourceBundleUrl) else {
|
||||
fatalError("MessageKit: Could not load the assets bundle")
|
||||
fatalError(MessageKitError.couldNotLoadAssetsBundle)
|
||||
}
|
||||
|
||||
return resourceBundle
|
||||
|
||||
@@ -31,4 +31,3 @@ extension CGRect {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ final class MessageIntermediateLayoutAttributes {
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - avatarSize.width
|
||||
case .natural:
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
fatalError(MessageKitError.avatarPositionUnresolved)
|
||||
}
|
||||
|
||||
switch avatarPosition.vertical {
|
||||
@@ -91,7 +91,7 @@ final class MessageIntermediateLayoutAttributes {
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - avatarSize.width - messageContainerSize.width - messageContainerPadding.right
|
||||
case .natural:
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
fatalError(MessageKitError.avatarPositionUnresolved)
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: messageContainerSize)
|
||||
|
||||
@@ -66,7 +66,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
/// Convenience property for accessing the layout object's `MessagesCollectionView`.
|
||||
fileprivate var messagesCollectionView: MessagesCollectionView {
|
||||
guard let messagesCollectionView = collectionView as? MessagesCollectionView else {
|
||||
fatalError("MessagesCollectionViewFlowLayout is being used on a foreign type.")
|
||||
fatalError(MessageKitError.layoutUsedOnForeignType)
|
||||
}
|
||||
return messagesCollectionView
|
||||
}
|
||||
@@ -74,7 +74,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
/// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesDataSource`.
|
||||
fileprivate var messagesDataSource: MessagesDataSource {
|
||||
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError("MessagesDataSource has not been set.")
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
return messagesDataSource
|
||||
}
|
||||
@@ -82,7 +82,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
/// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesLayoutDelegate`.
|
||||
fileprivate var messagesLayoutDelegate: MessagesLayoutDelegate {
|
||||
guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else {
|
||||
fatalError("MessagesLayoutDeleagte has not been set.")
|
||||
fatalError(MessageKitError.nilMessagesLayoutDeleagte)
|
||||
}
|
||||
return messagesLayoutDelegate
|
||||
}
|
||||
@@ -104,7 +104,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
|
||||
sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleOrientationChange), name: .UIDeviceOrientationDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: .UIDeviceOrientationDidChange, object: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
@@ -502,7 +502,7 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.bottomLabelHorizontalPadding
|
||||
|
||||
case (_, .natural):
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
fatalError(MessageKitError.avatarPositionUnresolved)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -575,7 +575,7 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.topLabelHorizontalPadding
|
||||
|
||||
case (_, .natural):
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
fatalError(MessageKitError.avatarPositionUnresolved)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,24 +27,38 @@ import MapKit
|
||||
/// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`.
|
||||
public struct LocationMessageSnapshotOptions {
|
||||
|
||||
/// Initialize LocationMessageSnapshotOptions with given parameters
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings.
|
||||
/// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest.
|
||||
/// - span: The span of the snapshot.
|
||||
/// - scale: The scale of the snapshot.
|
||||
public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), scale: CGFloat = UIScreen.main.scale) {
|
||||
self.showsBuildings = showsBuildings
|
||||
self.showsPointsOfInterest = showsPointsOfInterest
|
||||
self.span = span
|
||||
self.scale = scale
|
||||
}
|
||||
|
||||
/// A Boolean value indicating whether the snapshot image should display buildings.
|
||||
///
|
||||
/// The default value of this property is `false`.
|
||||
var showsBuildings = false
|
||||
|
||||
public var showsBuildings: Bool
|
||||
|
||||
/// A Boolean value indicating whether the snapshot image should display points of interest.
|
||||
///
|
||||
/// The default value of this property is `false`.
|
||||
var showsPointsOfInterest = false
|
||||
|
||||
public var showsPointsOfInterest: Bool
|
||||
|
||||
/// The span of the snapshot.
|
||||
///
|
||||
/// The default value of this property uses a width of `0` and height of `0`.
|
||||
var span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0)
|
||||
|
||||
public var span: MKCoordinateSpan
|
||||
|
||||
/// The scale of the snapshot.
|
||||
///
|
||||
/// The default value of this property uses the `UIScreen.main.scale`.
|
||||
var scale: CGFloat = UIScreen.main.scale
|
||||
public var scale: CGFloat
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
enum MessageKitError {
|
||||
static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved."
|
||||
static let nilMessagesDataSource = "MessagesDataSource has not been set."
|
||||
static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set."
|
||||
static let nilMessagesLayoutDeleagte = "MessagesLayoutDelegate has not been set."
|
||||
static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView."
|
||||
static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type."
|
||||
static let unrecognizedSectionKind = "Received unrecognized element kind:"
|
||||
static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType"
|
||||
static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle"
|
||||
static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle."
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public struct Sender {
|
||||
extension Sender: Equatable {
|
||||
|
||||
/// Two senders are considered equal if they have the same id.
|
||||
static public func == (left: Sender, right: Sender) -> Bool {
|
||||
public static func == (left: Sender, right: Sender) -> Bool {
|
||||
return left.id == right.id
|
||||
}
|
||||
|
||||
|
||||
@@ -51,16 +51,6 @@ public protocol MessagesDataSource: AnyObject {
|
||||
/// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed.
|
||||
func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int
|
||||
|
||||
/// The `Avatar` information to be used by the `AvatarView`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: The `MessageType` that will be displayed by this cell.
|
||||
/// - indexPath: The `IndexPath` of the cell.
|
||||
/// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
|
||||
///
|
||||
/// The default value returned by this method is `Avatar()`.
|
||||
func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar
|
||||
|
||||
/// The attributed text to be used for cell's top label.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -89,10 +79,6 @@ public extension MessagesDataSource {
|
||||
return message.sender == currentSender()
|
||||
}
|
||||
|
||||
func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar {
|
||||
return Avatar()
|
||||
}
|
||||
|
||||
func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ public protocol MessagesDisplayDelegate: AnyObject {
|
||||
///
|
||||
/// The default value returned by this method is a `MessageFooterView`.
|
||||
func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView
|
||||
|
||||
/// Configure `AvatarView`‘s image.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - avatarView: The `AvatarView` of the cell.
|
||||
/// - message: The `MessageType` that will be displayed by this cell.
|
||||
/// - indexPath: The `IndexPath` of the cell.
|
||||
/// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
|
||||
///
|
||||
/// The default image configured by this method is `?`.
|
||||
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)
|
||||
|
||||
// MARK: - Text Messages
|
||||
|
||||
@@ -193,11 +204,17 @@ public extension MessagesDisplayDelegate {
|
||||
func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView {
|
||||
return messagesCollectionView.dequeueReusableFooterView(MessageFooterView.self, for: indexPath)
|
||||
}
|
||||
|
||||
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
|
||||
avatarView.initials = "?"
|
||||
}
|
||||
|
||||
// MARK: - Text Messages Defaults
|
||||
|
||||
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .darkText }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
return dataSource.isFromCurrentSender(message: message) ? .white : .darkText
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,9 @@ public extension MessagesLayoutDelegate {
|
||||
// MARK: - All Messages Defaults
|
||||
|
||||
func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
if dataSource.isFromCurrentSender(message: message) {
|
||||
return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)
|
||||
} else {
|
||||
@@ -201,12 +203,16 @@ public extension MessagesLayoutDelegate {
|
||||
}
|
||||
|
||||
func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .cellCenter(.zero) }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
return dataSource.isFromCurrentSender(message: message) ? .messageTrailing(.zero) : .messageLeading(.zero)
|
||||
}
|
||||
|
||||
func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .cellCenter(.zero) }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
return dataSource.isFromCurrentSender(message: message) ? .messageLeading(.zero) : .messageTrailing(.zero)
|
||||
}
|
||||
|
||||
@@ -219,7 +225,9 @@ public extension MessagesLayoutDelegate {
|
||||
}
|
||||
|
||||
func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { return .zero }
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError(MessageKitError.nilMessagesDisplayDelegate)
|
||||
}
|
||||
let shouldDisplay = displayDelegate.shouldDisplayHeader(for: message, at: indexPath, in: messagesCollectionView)
|
||||
return shouldDisplay ? CGSize(width: messagesCollectionView.bounds.width, height: 12) : .zero
|
||||
}
|
||||
@@ -235,7 +243,9 @@ public extension MessagesLayoutDelegate {
|
||||
// MARK: - Text Messages Defaults
|
||||
|
||||
func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero }
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
if dataSource.isFromCurrentSender(message: message) {
|
||||
return UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)
|
||||
} else {
|
||||
@@ -264,5 +274,4 @@ public extension MessagesLayoutDelegate {
|
||||
func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ extension MessagesLayoutDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesCollectionViewFlowLayout
|
||||
|
||||
extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
/// A Boolean value that determines if the `AvatarView` is always on the leading
|
||||
@@ -88,6 +90,25 @@ extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesDataSource
|
||||
|
||||
extension MessagesDataSource {
|
||||
/// The `Avatar` information to be used by the `AvatarView`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: The `MessageType` that will be displayed by this cell.
|
||||
/// - indexPath: The `IndexPath` of the cell.
|
||||
/// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed.
|
||||
///
|
||||
/// The default value returned by this method is `Avatar()`.
|
||||
@available(*, deprecated: 0.12.1, message: "Removed in MessageKit 0.12.1.")
|
||||
func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar {
|
||||
fatalError("Fatal Error: avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) is no longer supported")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MessagesViewController
|
||||
|
||||
extension MessagesViewController {
|
||||
/// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the
|
||||
/// bottom on the view's first layout.
|
||||
|
||||
@@ -24,27 +24,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
open class AvatarView: UIView {
|
||||
open class AvatarView: UIImageView {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
open var avatar: Avatar = Avatar()
|
||||
|
||||
open var imageView = UIImageView()
|
||||
|
||||
public var image: UIImage? {
|
||||
return imageView.image
|
||||
|
||||
open var initials: String? {
|
||||
didSet {
|
||||
setImageFrom(initials: initials)
|
||||
}
|
||||
}
|
||||
|
||||
open var placeholderFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) {
|
||||
didSet {
|
||||
set(avatar: avatar)
|
||||
setImageFrom(initials: initials)
|
||||
}
|
||||
}
|
||||
|
||||
open var placeholderTextColor: UIColor = .white {
|
||||
didSet {
|
||||
set(avatar: avatar)
|
||||
setImageFrom(initials: initials)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,14 +59,12 @@ open class AvatarView: UIView {
|
||||
// MARK: - Overridden Properties
|
||||
open override var frame: CGRect {
|
||||
didSet {
|
||||
imageView.frame = bounds
|
||||
setCorner(radius: self.radius)
|
||||
}
|
||||
}
|
||||
|
||||
open override var bounds: CGRect {
|
||||
didSet {
|
||||
imageView.frame = bounds
|
||||
setCorner(radius: self.radius)
|
||||
}
|
||||
}
|
||||
@@ -82,6 +78,11 @@ open class AvatarView: UIView {
|
||||
convenience public init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
private func setImageFrom(initials: String?) {
|
||||
guard let initials = initials else { return }
|
||||
image = getImageFrom(initials: initials)
|
||||
}
|
||||
|
||||
private func getImageFrom(initials: String) -> UIImage {
|
||||
let width = frame.width
|
||||
@@ -160,17 +161,17 @@ open class AvatarView: UIView {
|
||||
contentMode = .scaleAspectFill
|
||||
layer.masksToBounds = true
|
||||
clipsToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.frame = frame
|
||||
addSubview(imageView)
|
||||
imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials)
|
||||
setCorner(radius: nil)
|
||||
}
|
||||
|
||||
// MARK: - Open setters
|
||||
|
||||
|
||||
open func set(avatar: Avatar) {
|
||||
imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials)
|
||||
if let image = avatar.image {
|
||||
self.image = image
|
||||
} else {
|
||||
initials = avatar.initials
|
||||
}
|
||||
}
|
||||
|
||||
open func setCorner(radius: CGFloat?) {
|
||||
|
||||
@@ -58,7 +58,7 @@ open class LocationMessageCell: MessageCollectionViewCell {
|
||||
open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
|
||||
super.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError("MessagesDisplayDelegate is not set.")
|
||||
fatalError(MessageKitError.nilMessagesDisplayDelegate)
|
||||
}
|
||||
let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
|
||||
let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView)
|
||||
@@ -107,7 +107,5 @@ open class LocationMessageCell: MessageCollectionViewCell {
|
||||
self.imageView.image = composedImage
|
||||
animationBlock?(self.imageView)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,10 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab
|
||||
|
||||
open weak var delegate: MessageCellDelegate?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
setupSubviews()
|
||||
setupGestureRecognizers()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
@@ -93,39 +92,31 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab
|
||||
|
||||
open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else {
|
||||
fatalError("MessagesDataSource is not set.")
|
||||
fatalError(MessageKitError.nilMessagesDataSource)
|
||||
}
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError("MessagesDisplayDelegate is not set.")
|
||||
fatalError(MessageKitError.nilMessagesDisplayDelegate)
|
||||
}
|
||||
|
||||
delegate = messagesCollectionView.messageCellDelegate
|
||||
|
||||
let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
|
||||
let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
|
||||
|
||||
displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
|
||||
|
||||
messageContainerView.backgroundColor = messageColor
|
||||
messageContainerView.style = messageStyle
|
||||
|
||||
let avatar = dataSource.avatar(for: message, at: indexPath, in: messagesCollectionView)
|
||||
let topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
|
||||
let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
|
||||
|
||||
avatarView.set(avatar: avatar)
|
||||
cellTopLabel.attributedText = topText
|
||||
cellBottomLabel.attributedText = bottomText
|
||||
}
|
||||
|
||||
func setupGestureRecognizers() {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
||||
contentView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
/// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView ....
|
||||
@objc
|
||||
open func handleTapGesture(_ gesture: UIGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
|
||||
let touchLocation = gesture.location(in: self)
|
||||
|
||||
switch true {
|
||||
@@ -141,6 +132,13 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
|
||||
open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let touchPoint = gestureRecognizer.location(in: self)
|
||||
guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
|
||||
return messageContainerView.frame.contains(touchPoint)
|
||||
}
|
||||
|
||||
/// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
|
||||
open func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
|
||||
|
||||
@@ -45,6 +45,7 @@ open class TextMessageCell: MessageCollectionViewCell {
|
||||
if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes {
|
||||
messageLabel.textInsets = attributes.messageLabelInsets
|
||||
messageLabel.font = attributes.messageLabelFont
|
||||
messageLabel.frame = messageContainerView.bounds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,18 +58,13 @@ open class TextMessageCell: MessageCollectionViewCell {
|
||||
open override func setupSubviews() {
|
||||
super.setupSubviews()
|
||||
messageContainerView.addSubview(messageLabel)
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
open func setupConstraints() {
|
||||
messageLabel.fillSuperview()
|
||||
}
|
||||
|
||||
open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
|
||||
super.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
|
||||
fatalError("MessagesDisplayDelegate not set.")
|
||||
fatalError(MessageKitError.nilMessagesDisplayDelegate)
|
||||
}
|
||||
|
||||
let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
|
||||
|
||||
@@ -49,6 +49,17 @@ open class InputTextView: UITextView {
|
||||
}
|
||||
}
|
||||
|
||||
/// The images that are currently stored as NSTextAttachment's
|
||||
open var images: [UIImage] {
|
||||
return parseForAttachedImages()
|
||||
}
|
||||
|
||||
open var components: [Any] {
|
||||
return parseForComponents()
|
||||
}
|
||||
|
||||
open var isImagePasteEnabled: Bool = true
|
||||
|
||||
/// A UILabel that holds the InputTextView's placeholder text
|
||||
open let placeholderLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
@@ -148,7 +159,9 @@ open class InputTextView: UITextView {
|
||||
layer.cornerRadius = 5.0
|
||||
layer.borderWidth = 1.25
|
||||
layer.borderColor = UIColor.lightGray.cgColor
|
||||
allowsEditingTextAttributes = false
|
||||
setupPlaceholderLabel()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
/// Adds the placeholderLabel to the view and sets up its initial constraints
|
||||
@@ -168,6 +181,14 @@ open class InputTextView: UITextView {
|
||||
placeholderLabelConstraintSet?.activate()
|
||||
}
|
||||
|
||||
/// Adds the required notification observers
|
||||
private func setupObservers() {
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(InputTextView.redrawTextAttachments),
|
||||
name: .UIDeviceOrientationDidChange, object: nil)
|
||||
}
|
||||
|
||||
/// Updates the placeholderLabels constraint constants to match the placeholderLabelInsets
|
||||
private func updateConstraintsForPlaceholderLabel() {
|
||||
|
||||
@@ -176,4 +197,143 @@ open class InputTextView: UITextView {
|
||||
placeholderLabelConstraintSet?.left?.constant = placeholderLabelInsets.left
|
||||
placeholderLabelConstraintSet?.right?.constant = -placeholderLabelInsets.right
|
||||
}
|
||||
|
||||
// MARK: - Image Paste Support
|
||||
|
||||
open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
|
||||
if action == NSSelectorFromString("paste:") && UIPasteboard.general.image != nil {
|
||||
return isImagePasteEnabled
|
||||
}
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
open override func paste(_ sender: Any?) {
|
||||
|
||||
guard let image = UIPasteboard.general.image else {
|
||||
return super.paste(sender)
|
||||
}
|
||||
pasteImageInTextContainer(with: image)
|
||||
}
|
||||
|
||||
/// Addes a new UIImage to the NSTextContainer as an NSTextAttachment
|
||||
///
|
||||
/// - Parameter image: The image to add
|
||||
private func pasteImageInTextContainer(with image: UIImage) {
|
||||
|
||||
// Add the new image as an NSTextAttachment
|
||||
let attributedImageString = NSAttributedString(attachment: textAttachment(using: image))
|
||||
|
||||
let isEmpty = attributedText.length == 0
|
||||
|
||||
// Add a new line character before the image, this is what iMessage does
|
||||
let newAttributedStingComponent = isEmpty ? NSMutableAttributedString(string: "") : NSMutableAttributedString(string: "\n")
|
||||
newAttributedStingComponent.append(attributedImageString)
|
||||
|
||||
// Add a new line character after the image, this is what iMessage does
|
||||
newAttributedStingComponent.append(NSAttributedString(string: "\n"))
|
||||
|
||||
// The attributes that should be applied to the new NSAttributedString to match the current attributes
|
||||
let attributes: [NSAttributedStringKey: Any] = [
|
||||
NSAttributedStringKey.font: font ?? UIFont.preferredFont(forTextStyle: .body),
|
||||
NSAttributedStringKey.foregroundColor: textColor ?? .black
|
||||
]
|
||||
newAttributedStingComponent.addAttributes(attributes, range: NSRange(location: 0, length: newAttributedStingComponent.length))
|
||||
|
||||
textStorage.beginEditing()
|
||||
// Paste over selected text
|
||||
textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent)
|
||||
textStorage.endEditing()
|
||||
|
||||
// Advance the range to the selected range plus the number of characters added
|
||||
let location = selectedRange.location + (isEmpty ? 2 : 3)
|
||||
selectedRange = NSRange(location: location, length: 0)
|
||||
|
||||
// Broadcast a notification to recievers such as the MessageInputBar which will handle resizing
|
||||
NotificationCenter.default.post(name: .UITextViewTextDidChange, object: self)
|
||||
}
|
||||
|
||||
/// Returns an NSTextAttachment the provided image that will fit inside the NSTextContainer
|
||||
///
|
||||
/// - Parameter image: The image to create an attachment with
|
||||
/// - Returns: The formatted NSTextAttachment
|
||||
private func textAttachment(using image: UIImage) -> NSTextAttachment {
|
||||
|
||||
guard let cgImage = image.cgImage else { return NSTextAttachment() }
|
||||
let scale = image.size.width / (frame.width - 2 * (textContainerInset.left + textContainerInset.right))
|
||||
let textAttachment = NSTextAttachment()
|
||||
textAttachment.image = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
|
||||
return textAttachment
|
||||
}
|
||||
|
||||
/// Returns all images that exist as NSTextAttachment's
|
||||
///
|
||||
/// - Returns: An array of type UIImage
|
||||
private func parseForAttachedImages() -> [UIImage] {
|
||||
|
||||
var images = [UIImage]()
|
||||
let range = NSRange(location: 0, length: attributedText.length)
|
||||
attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, _ -> Void in
|
||||
|
||||
if let attachment = value as? NSTextAttachment {
|
||||
if let image = attachment.image {
|
||||
images.append(image)
|
||||
} else if let image = attachment.image(forBounds: attachment.bounds,
|
||||
textContainer: nil,
|
||||
characterIndex: range.location) {
|
||||
images.append(image)
|
||||
}
|
||||
}
|
||||
})
|
||||
return images
|
||||
}
|
||||
|
||||
/// Returns an array of components (either a String or UIImage) that makes up the textContainer in
|
||||
/// the order that they were typed
|
||||
///
|
||||
/// - Returns: An array of objects guaranteed to be of UIImage or String
|
||||
private func parseForComponents() -> [Any] {
|
||||
|
||||
var components = [Any]()
|
||||
let range = NSRange(location: 0, length: attributedText.length)
|
||||
attributedText.enumerateAttributes(in: range, options: []) { (object, range, _) in
|
||||
|
||||
if object.keys.contains(.attachment) {
|
||||
if let attachment = object[.attachment] as? NSTextAttachment {
|
||||
if let image = attachment.image {
|
||||
components.append(image)
|
||||
} else if let image = attachment.image(forBounds: attachment.bounds,
|
||||
textContainer: nil,
|
||||
characterIndex: range.location) {
|
||||
components.append(image)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let stringValue = attributedText.attributedSubstring(from: range).string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !stringValue.isEmpty {
|
||||
components.append(stringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
/// Redraws the NSTextAttachments in the NSTextContainer to fit the current bounds
|
||||
@objc
|
||||
private func redrawTextAttachments() {
|
||||
|
||||
guard images.count > 0 else { return }
|
||||
let range = NSRange(location: 0, length: attributedText.length)
|
||||
attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, _, _ -> Void in
|
||||
if let attachment = value as? NSTextAttachment, let image = attachment.image {
|
||||
|
||||
// Calculates a new width/height ratio to fit the image in the current frame
|
||||
let newWidth = frame.width - 2 * (textContainerInset.left + textContainerInset.right)
|
||||
let ratio = image.size.height / image.size.width
|
||||
attachment.bounds.size = CGSize(width: newWidth, height: ratio * newWidth)
|
||||
}
|
||||
})
|
||||
layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ open class MessageInputBar: UIView {
|
||||
private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var contentViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var windowAnchor: NSLayoutConstraint?
|
||||
private var backgroundViewBottomAnchor: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -336,7 +337,9 @@ open class MessageInputBar: UIView {
|
||||
|
||||
// The constraints within the MessageInputBar
|
||||
separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: 1)
|
||||
backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor)
|
||||
backgroundViewBottomAnchor = backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
backgroundViewBottomAnchor?.isActive = true
|
||||
backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, right: rightAnchor)
|
||||
|
||||
topStackViewLayoutSet = NSLayoutConstraintSet(
|
||||
top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
|
||||
@@ -408,6 +411,7 @@ open class MessageInputBar: UIView {
|
||||
windowAnchor?.constant = -padding.bottom
|
||||
windowAnchor?.priority = UILayoutPriority(rawValue: 750)
|
||||
windowAnchor?.isActive = true
|
||||
backgroundViewBottomAnchor?.constant = 34
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -666,7 +670,7 @@ open class MessageInputBar: UIView {
|
||||
open func textViewDidChange() {
|
||||
let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
sendButton.isEnabled = !trimmedText.isEmpty
|
||||
sendButton.isEnabled = !trimmedText.isEmpty || inputTextView.images.count > 0
|
||||
inputTextView.placeholderLabel.isHidden = !inputTextView.text.isEmpty
|
||||
|
||||
items.forEach { $0.textViewDidChangeAction(with: inputTextView) }
|
||||
|
||||
@@ -283,7 +283,7 @@ open class MessageLabel: UILabel {
|
||||
case .link:
|
||||
return urlAttributes
|
||||
default:
|
||||
fatalError("Received an unrecognized NSTextCheckingResult.CheckingType")
|
||||
fatalError(MessageKitError.unrecognizedCheckingResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ open class MessageLabel: UILabel {
|
||||
|
||||
}
|
||||
|
||||
fileprivate enum MessageTextCheckingType {
|
||||
private enum MessageTextCheckingType {
|
||||
case addressComponents([NSTextCheckingKey: String]?)
|
||||
case date(Date?)
|
||||
case phoneNumber(String?)
|
||||
|
||||
@@ -51,6 +51,7 @@ open class MessagesCollectionView: UICollectionView {
|
||||
public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
|
||||
super.init(frame: frame, collectionViewLayout: layout)
|
||||
backgroundColor = .white
|
||||
setupGestureRecognizers()
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
@@ -62,6 +63,23 @@ open class MessagesCollectionView: UICollectionView {
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func setupGestureRecognizers() {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
|
||||
tapGesture.delaysTouchesBegan = true
|
||||
addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@objc
|
||||
open func handleTapGesture(_ gesture: UIGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
|
||||
let touchLocation = gesture.location(in: self)
|
||||
guard let indexPath = indexPathForItem(at: touchLocation) else { return }
|
||||
|
||||
let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell
|
||||
cell?.handleTapGesture(gesture)
|
||||
}
|
||||
|
||||
public func scrollToBottom(animated: Bool = false) {
|
||||
let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height
|
||||
|
||||
@@ -205,4 +205,8 @@ private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegat
|
||||
func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
|
||||
return 10.0
|
||||
}
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +85,6 @@ class MessagesDisplayDelegateTests: XCTestCase {
|
||||
XCTAssertEqual(backgroundColor, .clear)
|
||||
}
|
||||
|
||||
func testAvatarDefaultState() {
|
||||
XCTAssertNotNil(sut.dataProvider.avatar(for: sut.dataProvider.messages[0],
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView).initials)
|
||||
}
|
||||
|
||||
func testCellTopLabelDefaultState() {
|
||||
XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(for: sut.dataProvider.messages[0],
|
||||
at: IndexPath(item: 0, section: 0)))
|
||||
@@ -218,7 +212,8 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testTextColorWithoutDataSource_returnsDarkTextForDefault() {
|
||||
sut.messagesCollectionView.messagesDataSource = nil
|
||||
let dataSource = sut.makeDataSource()
|
||||
sut.messagesCollectionView.messagesDataSource = dataSource
|
||||
let textColor = sut.textColor(for: sut.dataProvider.messages[1],
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
@@ -255,7 +250,7 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla
|
||||
|
||||
}
|
||||
|
||||
private func makeDataSource() -> MockMessagesDataSource {
|
||||
fileprivate func makeDataSource() -> MockMessagesDataSource {
|
||||
let dataSource = MockMessagesDataSource()
|
||||
dataSource.messages.append(MockMessage(text: "Text 1",
|
||||
sender: dataSource.senders[0],
|
||||
@@ -267,4 +262,7 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ class AvatarViewTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testNoParams() {
|
||||
XCTAssertEqual(avatarView.avatar.initials, "?")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
}
|
||||
@@ -57,6 +56,7 @@ class AvatarViewTests: XCTestCase {
|
||||
func testInitialsOnly() {
|
||||
let avatar = Avatar(initials: "DL")
|
||||
avatarView.set(avatar: avatar)
|
||||
XCTAssertEqual(avatarView.initials, avatar.initials)
|
||||
XCTAssertEqual(avatar.initials, "DL")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
|
||||
@@ -76,6 +76,11 @@ class MessageCollectionViewCellTests: XCTestCase {
|
||||
|
||||
extension MessageCollectionViewCellTests {
|
||||
|
||||
fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate { }
|
||||
fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate {
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user