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
を設定)
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
を追加
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)
}
}
基本的なのはこれで動くと思います。
カスタマイズ
アイコンを非表示にしたかったが、かなり手こずったので自戒の念を込めて
(ライブラリをめちゃくちゃ読み込んだのに、最終的にはQAにかかれていました、、)
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)
}
}
///////// 追記 ////////
追記部分を入れることで、アイコンを非表示にできます。
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系を使うほうがよい
まとめ
公式のドキュメントが50%くらいしか書かれていないので、ソースをかなり読み込む必要があった。(2018/7/5 現在)
MessageKitのリポジトリをDLしてMessageKit/Example
ディレクトリ内にサンプルのプロジェクトがあるので
そこをいじりながらやりたいことを確認するとやりやすかった。