Files
MessageKit/Example/Sources/View Controllers/AdvancedExampleViewController.swift
T
2018-08-31 11:04:42 -07:00

377 lines
18 KiB
Swift

/*
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
import MessageKit
import MapKit
final class AdvancedExampleViewController: ChatViewController {
let outgoingAvatarOverlap: CGFloat = 17.5
override func viewDidLoad() {
messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout())
messagesCollectionView.register(CustomCell.self)
super.viewDidLoad()
updateTitleView(title: "MessageKit", subtitle: "2 Online")
// Customize the typing bubble! These are the default values
// typingBubbleBackgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
// typingBubbleDotColor = .lightGray
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu])
.onTypingStatus { [weak self] in
self?.setTypingIndicatorHidden(false)
}.onNewMessage { [weak self] message in
self?.setTypingIndicatorHidden(true, performUpdates: {
// self?.insertMessage(message)
})
self?.insertMessage(message)
}
}
override func loadFirstMessages() {
DispatchQueue.global(qos: .userInitiated).async {
let count = UserDefaults.standard.mockMessagesCount()
SampleData.shared.getAdvancedMessages(count: count) { messages in
DispatchQueue.main.async {
self.messageList = messages
self.messagesCollectionView.reloadData()
self.messagesCollectionView.scrollToBottom()
}
}
}
}
override func loadMoreMessages() {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
SampleData.shared.getAdvancedMessages(count: 20) { messages in
DispatchQueue.main.async {
self.messageList.insert(contentsOf: messages, at: 0)
self.messagesCollectionView.reloadDataAndKeepOffset()
self.refreshControl.endRefreshing()
}
}
}
}
override func configureMessageCollectionView() {
super.configureMessageCollectionView()
let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)
// Hide the outgoing avatar and adjust the label alignment to line up with the messages
layout?.setMessageOutgoingAvatarSize(.zero)
layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
// Set outgoing avatar to overlap with the message bubble
layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0)))
layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18))
layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
layout?.setMessageIncomingAccessoryViewPadding(UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0))
layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
layout?.setMessageOutgoingAccessoryViewPadding(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
}
override func configureMessageInputBar() {
super.configureMessageInputBar()
messageInputBar.isTranslucent = true
messageInputBar.separatorLine.isHidden = true
messageInputBar.inputTextView.tintColor = .primaryColor
messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1)
messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36)
messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36)
messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor
messageInputBar.inputTextView.layer.borderWidth = 1.0
messageInputBar.inputTextView.layer.cornerRadius = 16.0
messageInputBar.inputTextView.layer.masksToBounds = true
messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false)
messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false)
messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up")
messageInputBar.sendButton.title = nil
messageInputBar.sendButton.imageView?.layer.cornerRadius = 16
messageInputBar.textViewPadding.right = -38
let charCountButton = InputBarButtonItem()
.configure {
$0.title = "0/140"
$0.contentHorizontalAlignment = .right
$0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal)
$0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold)
$0.setSize(CGSize(width: 50, height: 25), animated: false)
}.onTextViewDidChange { (item, textView) in
item.title = "\(textView.text.count)/140"
let isOverLimit = textView.text.count > 140
item.messageInputBar?.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit
item.messageInputBar?.sendButton.isEnabled = !isOverLimit
let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1)
item.setTitleColor(color, for: .normal)
}
let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), makeButton(named: "ic_library"), .flexibleSpace, charCountButton]
messageInputBar.textViewPadding.bottom = 8
messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)
// This just adds some more flare
messageInputBar.sendButton
.onEnabled { item in
UIView.animate(withDuration: 0.3, animations: {
item.imageView?.backgroundColor = .primaryColor
})
}.onDisabled { item in
UIView.animate(withDuration: 0.3, animations: {
item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
})
}
}
// MARK: - Helpers
func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
}
func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
guard indexPath.section - 1 >= 0 else { return false }
return messageList[indexPath.section].sender == messageList[indexPath.section - 1].sender
}
func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
guard indexPath.section + 1 < messageList.count else { return false }
return messageList[indexPath.section].sender == messageList[indexPath.section + 1].sender
}
func setTypingIndicatorHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) {
updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...")
// setTypingBubbleHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in
// if self?.isLastSectionVisible() == true {
// self?.messagesCollectionView.scrollToBottom(animated: true)
// }
// }
messagesCollectionView.scrollToBottom(animated: true)
}
private func makeButton(named: String) -> InputBarButtonItem {
return InputBarButtonItem()
.configure {
$0.spacing = .fixed(10)
$0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
$0.setSize(CGSize(width: 25, height: 25), animated: false)
$0.tintColor = UIColor(white: 0.8, alpha: 1)
}.onSelected {
$0.tintColor = .primaryColor
}.onDeselected {
$0.tintColor = UIColor(white: 0.8, alpha: 1)
}.onTouchUpInside { _ in
print("Item Tapped")
}
}
// MARK: - UICollectionViewDataSource
open override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
fatalError("Ouch. nil data source for messages")
}
// guard !isSectionReservedForTypingBubble(indexPath.section) else {
// return super.collectionView(collectionView, cellForItemAt: indexPath)
// }
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
if case .custom = message.kind {
let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath)
cell.configure(with: message, at: indexPath, and: messagesCollectionView)
return cell
}
return super.collectionView(collectionView, cellForItemAt: indexPath)
}
// MARK: - MessagesDataSource
override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
if isTimeLabelVisible(at: indexPath) {
return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedStringKey.foregroundColor: UIColor.darkGray])
}
return nil
}
override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
if !isPreviousMessageSameSender(at: indexPath) {
let name = message.sender.displayName
return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
}
return nil
}
override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) {
return NSAttributedString(string: "Delivered", attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
}
return nil
}
}
// MARK: - MessagesDisplayDelegate
extension AdvancedExampleViewController: MessagesDisplayDelegate {
// MARK: - Text Messages
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? .white : .darkText
}
func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] {
return MessageLabel.defaultAttributes
}
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
return [.url, .address, .phoneNumber, .date, .transitInformation]
}
// MARK: - All Messages
func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
}
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
var corners: UIRectCorner = []
if isFromCurrentSender(message: message) {
corners.formUnion(.topLeft)
corners.formUnion(.bottomLeft)
if !isPreviousMessageSameSender(at: indexPath) {
corners.formUnion(.topRight)
}
if !isNextMessageSameSender(at: indexPath) {
corners.formUnion(.bottomRight)
}
} else {
corners.formUnion(.topRight)
corners.formUnion(.bottomRight)
if !isPreviousMessageSameSender(at: indexPath) {
corners.formUnion(.topLeft)
}
if !isNextMessageSameSender(at: indexPath) {
corners.formUnion(.bottomLeft)
}
}
return .custom { view in
let radius: CGFloat = 16
let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
view.layer.mask = mask
}
}
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)
avatarView.isHidden = isNextMessageSameSender(at: indexPath)
avatarView.layer.borderWidth = 2
avatarView.layer.borderColor = UIColor.primaryColor.cgColor
}
func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
let button = UIButton(type: .infoLight)
button.tintColor = .primaryColor
accessoryView.addSubview(button)
button.frame = accessoryView.bounds
button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
}
// MARK: - Location Messages
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
let pinImage = #imageLiteral(resourceName: "ic_map_marker")
annotationView.image = pinImage
annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
return annotationView
}
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
return { view in
view.layer.transform = CATransform3DMakeScale(2, 2, 2)
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
view.layer.transform = CATransform3DIdentity
}, completion: nil)
}
}
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
}
}
// MARK: - MessagesLayoutDelegate
extension AdvancedExampleViewController: MessagesLayoutDelegate {
func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
if isTimeLabelVisible(at: indexPath) {
return 18
}
return 0
}
func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
if isFromCurrentSender(message: message) {
return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
} else {
return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0
}
}
func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
}
}