Edited at

MessageKitの使いかた(初歩)

More than 1 year has passed since last update.

LINEのようなメッセージをやり取りする画面を作成するとき、今まではJSQMessagesViewControllerを使用していたが、久々に使おうとしたところDEPRECATEDとなっていた、、、

代替としてMessageKitを見つけたが、使いかたがピンとこなかったのでメモがてら書きました。

間違い等ありましたらコメントくださいmm


環境


  • macOS High Sierra 10.13.5

  • XCode 9.4.1

  • iOS 11.4

  • Swift 4.1.2

  • MessageKit 1.0.0


導入

CocoaPodsを使用してインストール

# バージョン指定がない場合、0.13.5とかがインストールされる(僕だけ?

pod 'MessageKit', '>= 1.0.0'

$ pod install


実装

画像のViewControllerに実装する。

(NavigationControllerが接続されているだけ。Custom ClassにViewControllerを設定)

スクリーンショット.png


ViewController.swift


import UIKit
import MessageKit

class ViewController: MessagesViewController {

var messageList: [MockMessage] = []

lazy var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()

override func viewDidLoad() {
super.viewDidLoad()

DispatchQueue.main.async {
// messageListにメッセージの配列をいれて
self.messageList = self.getMessages()
// messagesCollectionViewをリロードして
self.messagesCollectionView.reloadData()
// 一番下までスクロールする
self.messagesCollectionView.scrollToBottom()
}

messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
messagesCollectionView.messageCellDelegate = self

messageInputBar.delegate = self
messageInputBar.sendButton.tintColor = UIColor.lightGray

// // メッセージ入力欄の左に画像選択ボタンを追加
// // 画像選択とかしたいときに
// let items = [
// makeButton(named: "clip.png").onTextViewDidChange { button, textView in
// button.tintColor = UIColor.lightGray
// button.isEnabled = textView.text.isEmpty
// }
// ]
// items.forEach { $0.tintColor = .lightGray }
// messageInputBar.setStackViewItems(items, forStack: .left, animated: false)
// messageInputBar.setLeftStackViewWidthConstant(to: 45, animated: false)

// メッセージ入力時に一番下までスクロール
scrollsToBottomOnKeybordBeginsEditing = true // default false
maintainPositionOnKeyboardFrameChanged = true // default false
}

// // ボタンの作成
// func makeButton(named: String) -> InputBarButtonItem {
// return InputBarButtonItem()
// .configure {
// $0.spacing = .fixed(10)
// $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
// $0.setSize(CGSize(width: 30, height: 30), animated: true)
// }.onSelected {
// $0.tintColor = UIColor.gray
// }.onDeselected {
// $0.tintColor = UIColor.lightGray
// }.onTouchUpInside { _ in
// print("Item Tapped")
// }
// }

// サンプル用に適当なメッセージ
func getMessages() -> [MockMessage] {
return [
createMessage(text: "あ"),
createMessage(text: "い"),
createMessage(text: "う"),
createMessage(text: "え"),
createMessage(text: "お"),
createMessage(text: "か"),
createMessage(text: "き"),
createMessage(text: "く"),
createMessage(text: "け"),
createMessage(text: "こ"),
createMessage(text: "さ"),
createMessage(text: "し"),
createMessage(text: "すせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん"),
]
}

func createMessage(text: String) -> MockMessage {
let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 15),
.foregroundColor: UIColor.black])
return MockMessage(attributedText: attributedText, sender: otherSender(), messageId: UUID().uuidString, date: Date())
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: MessagesDataSource {

func currentSender() -> Sender {
return Sender(id: "123", displayName: "自分")
}

func otherSender() -> Sender {
return Sender(id: "456", displayName: "知らない人")
}

func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
return messageList.count
}

func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
return messageList[indexPath.section]
}

// メッセージの上に文字を表示
func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
if indexPath.section % 3 == 0 {
return NSAttributedString(
string: MessageKitDateFormatter.shared.string(from: message.sentDate),
attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10),
NSAttributedStringKey.foregroundColor: UIColor.darkGray]
)
}
return nil
}

// メッセージの上に文字を表示(名前)
func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
let name = message.sender.displayName
return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
}

// メッセージの下に文字を表示(日付)
func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
let dateString = formatter.string(from: message.sentDate)
return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
}
}

// メッセージのdelegate
extension ViewController: MessagesDisplayDelegate {

// メッセージの色を変更(デフォルトは自分:白、相手:黒)
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ? .white : .darkText
}

// メッセージの背景色を変更している(デフォルトは自分:緑、相手:グレー)
func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
return isFromCurrentSender(message: message) ?
UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) :
UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
}

// メッセージの枠にしっぽを付ける
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
return .bubbleTail(corner, .curved)
}

// アイコンをセット
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
// message.sender.displayNameとかで送信者の名前を取得できるので
// そこからイニシャルを生成するとよい
let avatar = Avatar(initials: "人")
avatarView.set(avatar: avatar)
}
}

// 各ラベルの高さを設定(デフォルト0なので必須)
extension ViewController: MessagesLayoutDelegate {

func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
if indexPath.section % 3 == 0 { return 10 }
return 0
}

func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 16
}

func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
return 16
}
}

extension ViewController: MessageCellDelegate {
// メッセージをタップした時の挙動
func didTapMessage(in cell: MessageCollectionViewCell) {
print("Message tapped")
}
}

extension ViewController: MessageInputBarDelegate {
// メッセージ送信ボタンをタップした時の挙動
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith 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.white])
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.scrollToBottom()
}
}


メッセージのモデルとしてMockMessageを追加


MockMessage.swift

import Foundation

import CoreLocation
import MessageKit

private struct MockLocationItem: LocationItem {

var location: CLLocation
var size: CGSize

init(location: CLLocation) {
self.location = location
self.size = CGSize(width: 240, height: 240)
}
}

private struct MockMediaItem: MediaItem {

var url: URL?
var image: UIImage?
var placeholderImage: UIImage
var size: CGSize

init(image: UIImage) {
self.image = image
self.size = CGSize(width: 240, height: 240)
self.placeholderImage = UIImage()
}
}

internal struct MockMessage: MessageType {

var messageId: String
var sender: Sender
var sentDate: Date
var kind: MessageKind

private init(kind: MessageKind, sender: Sender, messageId: String, date: Date) {
self.kind = kind
self.sender = sender
self.messageId = messageId
self.sentDate = date
}

init(text: String, sender: Sender, messageId: String, date: Date) {
self.init(kind: .text(text), sender: sender, messageId: messageId, date: date)
}

init(attributedText: NSAttributedString, sender: Sender, messageId: String, date: Date) {
self.init(kind: .attributedText(attributedText), sender: sender, messageId: messageId, date: date)
}

init(image: UIImage, sender: Sender, messageId: String, date: Date) {
let mediaItem = MockMediaItem(image: image)
self.init(kind: .photo(mediaItem), sender: sender, messageId: messageId, date: date)
}

init(thumbnail: UIImage, sender: Sender, messageId: String, date: Date) {
let mediaItem = MockMediaItem(image: thumbnail)
self.init(kind: .video(mediaItem), sender: sender, messageId: messageId, date: date)
}

init(location: CLLocation, sender: Sender, messageId: String, date: Date) {
let locationItem = MockLocationItem(location: location)
self.init(kind: .location(locationItem), sender: sender, messageId: messageId, date: date)
}

init(emoji: String, sender: Sender, messageId: String, date: Date) {
self.init(kind: .emoji(emoji), sender: sender, messageId: messageId, date: date)
}
}


基本的なのはこれで動くと思います。

スクショ1.pngスクショ2.pngスクショ3.png


カスタマイズ

アイコンを非表示にしたかったが、かなり手こずったので自戒の念を込めて

(ライブラリをめちゃくちゃ読み込んだのに、最終的にはQAにかかれていました、、)


ViewController.swift

class ViewController: MessagesViewController {

override func viewDidLoad() {
super.viewDidLoad()

///////// 追記 ////////
// アイコンの表示を消し、その分ラベルを移動させる
if let layout = self.messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout {
layout.setMessageIncomingAvatarSize(.zero)
layout.setMessageOutgoingAvatarSize(.zero)
layout.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 10)))
layout.setMessageIncomingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 10)))
layout.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 10)))
layout.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 10)))
}
///////// 追記 ////////

// 省略
}
}

///////// 追記 ////////
fileprivate extension UIEdgeInsets {
init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) {
self.init(top: top, left: left, bottom: bottom, right: right)
}
}
///////// 追記 ////////


追記部分を入れることで、アイコンを非表示にできます。

非表示.png

extension ViewController: MessagesDisplayDelegate {}内で下記のように書いてもアイコンは非表示にできる

// アイコンをセット

func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
avatarView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
}

しかし、名前ラベル、メッセージ、日付ラベルがアイコン分ずれたままになるので、set系を使うほうがよい

ずれる.png


まとめ

公式のドキュメントが50%くらいしか書かれていないので、ソースをかなり読み込む必要があった。(2018/7/5 現在)

MessageKitのリポジトリをDLしてMessageKit/Exampleディレクトリ内にサンプルのプロジェクトがあるので

そこをいじりながらやりたいことを確認するとやりやすかった。