Merge pull request #464 from MessageKit/development

Version 0.13.0
This commit is contained in:
Wu Zhong
2018-01-17 14:22:27 +08:00
committed by GitHub
36 changed files with 1097 additions and 313 deletions
+34
View File
@@ -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
}
}
+101 -1
View File
@@ -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
}
}
+62
View File
@@ -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
View File
@@ -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.'
+20 -2
View File
@@ -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
}
}
+22 -201
View File
@@ -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
}
}
+2 -2
View File
@@ -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
}
+36
View File
@@ -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."
}
+1 -1
View File
@@ -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
}
+15 -6
View File
@@ -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.
+19 -18
View File
@@ -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 {
+2 -6
View File
@@ -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)
+160
View File
@@ -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)
}
}
+6 -2
View File
@@ -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) }
+2 -2
View File
@@ -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()
}
}
+1 -1
View File
@@ -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()
}
}
}