Files
MessageKit/Example/Sources/View Controllers/AdvancedExampleViewController.swift

531 lines
20 KiB
Swift

// MIT License
//
// Copyright (c) 2017-2020 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 InputBarAccessoryView
import Kingfisher
import MapKit
import MessageKit
import UIKit
// MARK: - AdvancedExampleViewController
final class AdvancedExampleViewController: ChatViewController {
// MARK: Public
// MARK: - UICollectionViewDataSource
public override func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath)
-> UICollectionViewCell
{
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
fatalError("Ouch. nil data source for messages")
}
// Very important to check this when overriding `cellForItemAt`
// Super method will handle returning the typing indicator cell
guard !isSectionReservedForTypingIndicator(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: Internal
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")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
.onTypingStatus { [weak self] in
self?.setTypingIndicatorViewHidden(false, animated: true)
}.onNewMessage { [weak self] message in
self?.setTypingIndicatorViewHidden(true, animated: false, performUpdates: {
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.scrollToLastItem()
}
}
}
}
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(HorizontalEdgeInsets(left: 8, right: 0))
layout?.setMessageIncomingAccessoryViewPosition(.messageBottom)
layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
}
override func configureMessageInputBar() {
// super.configureMessageInputBar()
messageInputBar = CameraInputBarAccessoryView()
messageInputBar.delegate = self
messageInputBar.inputTextView.tintColor = .primaryColor
messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal)
messageInputBar.sendButton.setTitleColor(
UIColor.primaryColor.withAlphaComponent(0.3),
for: .highlighted)
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)
configureInputBarItems()
inputBarType = .custom(messageInputBar)
}
// MARK: - Helpers
func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
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].user == messageList[indexPath.section - 1].user
}
func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
guard indexPath.section + 1 < messageList.count else { return false }
return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
}
func setTypingIndicatorViewHidden(_ isHidden: Bool, animated: Bool, performUpdates updates: (() -> Void)? = nil) {
updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...")
setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates) { [weak self] success in
if success, self?.isLastSectionVisible() == true {
self?.messagesCollectionView.scrollToLastItem(animated: true)
}
}
}
// 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: [
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
NSAttributedString.Key.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: [NSAttributedString.Key.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: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
}
return nil
}
// MARK: Private
private func configureInputBarItems() {
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
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.inputBarAccessoryView?
.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit
if isOverLimit {
item.inputBarAccessoryView?.sendButton.isEnabled = false
}
let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1)
item.setTitleColor(color, for: .normal)
}
let bottomItems = [.flexibleSpace, charCountButton]
configureInputBarPadding()
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)
})
}
}
/// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if necessary
/// See the InputBar diagram here to visualize how each of these would take effect:
/// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png
private func configureInputBarPadding() {
// Entire InputBar padding
messageInputBar.padding.bottom = 8
// or MiddleContentView padding
messageInputBar.middleContentViewPadding.right = -38
// or InputTextView padding
messageInputBar.inputTextView.textContainerInset.bottom = 8
}
private func makeButton(named: String) -> InputBarButtonItem {
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 {
print("Item Tapped")
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
actionSheet.addAction(action)
if let popoverPresentationController = actionSheet.popoverPresentationController {
popoverPresentationController.sourceView = $0
popoverPresentationController.sourceRect = $0.frame
}
self.navigationController?.present(actionSheet, animated: true, completion: nil)
}
}
}
// MARK: MessagesDisplayDelegate
extension AdvancedExampleViewController: MessagesDisplayDelegate {
// MARK: - Text Messages
func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
isFromCurrentSender(message: message) ? .white : .darkText
}
func detectorAttributes(
for detector: DetectorType,
and message: MessageType,
at _: IndexPath) -> [NSAttributedString.Key: Any]
{
switch detector {
case .hashtag, .mention:
if isFromCurrentSender(message: message) {
return [.foregroundColor: UIColor.white]
} else {
return [.foregroundColor: UIColor.primaryColor]
}
default: return MessageLabel.defaultAttributes
}
}
func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
[.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
}
// MARK: - All Messages
func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
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) -> 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)
{
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 _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
// Cells are reused, so only add a button here once. For real use you would need to
// ensure any subviews are removed if not needed
accessoryView.subviews.forEach { $0.removeFromSuperview() }
accessoryView.backgroundColor = .clear
let shouldShow = Int.random(in: 0 ... 10) == 0
guard shouldShow else { return }
let button = UIButton(type: .infoLight)
button.tintColor = .primaryColor
accessoryView.addSubview(button)
button.frame = accessoryView.bounds
button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
}
func configureMediaMessageImageView(
_ imageView: UIImageView,
for message: MessageType,
at _: IndexPath,
in _: MessagesCollectionView)
{
if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
imageView.kf.setImage(with: imageURL)
} else {
imageView.kf.cancelDownloadTask()
}
}
// MARK: - Location Messages
func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: 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,
in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
{
{ 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,
in _: MessagesCollectionView)
-> LocationMessageSnapshotOptions
{
LocationMessageSnapshotOptions(
showsBuildings: true,
showsPointsOfInterest: true,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
}
// MARK: - Audio Messages
func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
isFromCurrentSender(message: message) ? .white : .primaryColor
}
func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
audioController
.configureAudioCell(
cell,
message: message) // this is needed especially when the cell is reconfigure while is playing sound
}
}
// MARK: MessagesLayoutDelegate
extension AdvancedExampleViewController: MessagesLayoutDelegate {
func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
if isTimeLabelVisible(at: indexPath) {
return 18
}
return 0
}
func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: 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) -> CGFloat {
(!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
}
}
// MARK: CameraInputBarAccessoryViewDelegate
extension AdvancedExampleViewController: CameraInputBarAccessoryViewDelegate {
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) {
for item in attachments {
if case .image(let image) = item {
self.sendImageMessage(photo: image)
}
}
inputBar.invalidatePlugins()
}
func sendImageMessage(photo: UIImage) {
let photoMessage = MockMessage(image: photo, user: currentSender as! MockUser, messageId: UUID().uuidString, date: Date())
insertMessage(photoMessage)
}
}