昨今、多くのアプリでチャットUIが見慣れてきたかと思います。以外と見慣れているUIだけど一から自分で実装するのは以外と面倒ですよね....そこで今回はJSQMessagesViewControllerというライブラリを使ってチャットに必要な最低限の機能を実装する方法を説明します。
仕様について
JSQMessagesViewControllerには多くの機能が備わっているのですが、今回は以下の機能を実装していきます(とりあえずこれがあれば成り立つだろうという要素)。
- 1対1でユーザー同士がやりとりをできる
- テキストが送信できる
- 画像が送信できる
- 画像タップが検知できる
- 送信時刻を良い感じに表示する(ある一定の間隔が空いたら表示するなど)
- チャット相手のユーザーのアイコンが表示される
- 相手のユーザーのアイコンタップを検知することができる
初期導入方法
インストール
JSQMessagesViewControllerをCocoaPodsを使ってインストールします。
// Podfile pod 'JSQMessagesViewController' ~ pod install
はじめの設定
以下のファイルを作成してください。するとチャット画面ができ、最低限の実装ができているかと思います。 処理としては、メッセージを送ると自動で返信をするというものです。
import UIKit
import SwiftyJSON
import JSQMessagesViewController
class MessagesViewController: JSQMessagesViewController {
private var messages: [JSQMessage] = []
private var incomingBubble: JSQMessagesBubbleImage!
private var outgoingBubble: JSQMessagesBubbleImage!
private var incomingAvatar: JSQMessagesAvatarImage!
// テスト用
private let targetUser: JSON = ["senderId": "targetUser", "displayName": "passion"]
override func viewDidLoad() {
super.viewDidLoad()
initialSettings()
}
private func initialSettings() {
// 自分の情報入力
self.senderId = "self"
self.senderDisplayName = "自分の名前"
// 吹き出しの色設定
let bubbleFactory = JSQMessagesBubbleImageFactory()
self.incomingBubble = bubbleFactory.incomingMessagesBubbleImageWithColor(UIColor.jsq_messageBubbleLightGrayColor())
self.outgoingBubble = bubbleFactory.outgoingMessagesBubbleImageWithColor(UIColor.jsq_messageBubbleGreenColor())
// 相手の画像設定
self.incomingAvatar = JSQMessagesAvatarImageFactory.avatarImageWithImage(UIImage(named: "sample_user")!, diameter: 64)
// 自分の画像を表示しない
self.collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSizeZero
}
// 送信ボタンを押した時の挙動
override func didPressSendButton(button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: NSDate!) {
let message = JSQMessage(senderId: senderId, displayName: senderDisplayName, text: text)
messages.append(message)
// 更新
finishSendingMessageAnimated(true)
sendAutoMessage()
}
// 表示するメッセージの内容
override func collectionView(collectionView: JSQMessagesCollectionView!, messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
return self.messages[indexPath.item]
}
// 表示するメッセージの背景を指定
override func collectionView(collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
if messages[indexPath.item].senderId == senderId {
return self.outgoingBubble
}
return self.incomingBubble
}
// 表示するユーザーアイコンを指定。nilを指定すると画像がでない
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
if messages[indexPath.item].senderId != self.senderId {
return incomingAvatar
}
return nil
}
// メッセージの件数を指定
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
// テストでメッセージを送信するためのメソッド
private func sendAutoMessage() {
let message = JSQMessage(senderId: targetUser["senderId"].string, displayName: targetUser["displayName"].string, text: "返信するぞ")
messages.append(message)
finishReceivingMessageAnimated(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
機能実装
ここからはこの基本的なチャットを拡張していきます。
メッセージの上に時刻を表示
よくLINEなどで見かけるメッセージの上に送信時刻を表示する機能を実装していきます。 以下のリンクに書いてある内容が非常に良いので、今回はそちらを使わせて頂きました。Objective-Cで書かれていたのでSwift用に書き換えています。
Change a timestamp logic in JSQMessagesViewController
// 送信時刻を出すために高さを調整する
override func collectionView(collectionView: JSQMessagesCollectionView!, attributedTextForCellTopLabelAtIndexPath indexPath: NSIndexPath!) -> NSAttributedString! {
let message = messages[indexPath.item]
if indexPath.item == 0 {
return JSQMessagesTimestampFormatter.sharedFormatter().attributedTimestampForDate(message.date)
}
if indexPath.item - 1 > 0 {
let previousMessage = messages[indexPath.item - 1]
if message.date.timeIntervalSinceDate(previousMessage.date) / 60 > 1 {
return JSQMessagesTimestampFormatter.sharedFormatter().attributedTimestampForDate(message.date)
}
}
return nil
}
// 送信時刻を出すために高さを調整する
override func collectionView(collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
if indexPath.item == 0 {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
if indexPath.item - 1 > 0 {
let previousMessage = messages[indexPath.item - 1]
let message = messages[indexPath.item]
if message.date .timeIntervalSinceDate(previousMessage.date) / 60 > 1 {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
}
return 0.0
}
これでメッセージを送信してみてください。上に時刻が表示されていることでしょう。
ユーザーアイコンのタッチイベントを検知する
ユーザーのアイコンタップのイベントをハンドルするメソッドはデフォルトで備わっていないので、collectionViewのメソッドをオーバーライドして検知する方法をとります。
公式FAQを見るとcellのカスタマイズ方法が書いてあるので、それに習っていきます。
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
// ユーザーアイコンに対してジェスチャーをつける
let avatarImageTap = UITapGestureRecognizer(target: self, action: "tappedAvatar")
cell.avatarImageView?.userInteractionEnabled = true
cell.avatarImageView?.addGestureRecognizer(avatarImageTap)
// 文字色を変える
if messages[indexPath.item].senderId != senderId {
cell.textView?.textColor = UIColor.darkGrayColor()
} else {
cell.textView?.textColor = UIColor.whiteColor()
}
return cell
}
func tappedAvatar() {
print("tapped user avatar")
}
これでtappedAvatar()のメソッドでアイコンタッチが拾えるようになっているかと思います。
画像を投稿する
画像を投稿する仕組みを実装します。キーボードの上にクリップのアイコンがあるかと思います。あのアイコンをタップすることで投稿する画像を選べるようにします。
クリップのタッチイベントはdidPressAccessoryButton(sender: UIButton!)で受け取ることができるので、処理をこの中に書いていきます。
今回はUIImagePickerControllerを使用するので、こちらをみて、selectImage()メソッドが呼べる状態にしておいてください。
override func didPressAccessoryButton(sender: UIButton!) {
selectImage()
}
private func selectImage() {
let alertController = UIAlertController(title: "画像を選択", message: nil, preferredStyle: .ActionSheet)
let cameraAction = UIAlertAction(title: "カメラを起動", style: .Default) { (UIAlertAction) -> Void in
self.selectFromCamera()
}
let libraryAction = UIAlertAction(title: "カメラロールから選択", style: .Default) { (UIAlertAction) -> Void in
self.selectFromLibrary()
}
let cancelAction = UIAlertAction(title: "キャンセル", style: .Cancel) { (UIAlertAction) -> Void in
self.dismissViewControllerAnimated(true, completion: nil)
}
alertController.addAction(cameraAction)
alertController.addAction(libraryAction)
alertController.addAction(cancelAction)
}
private func selectFromCamera() {
if UIImagePickerController.isSourceTypeAvailable(.Camera) {
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.sourceType = UIImagePickerControllerSourceType.Camera
imagePickerController.allowsEditing = true
self.presentViewController(imagePickerController, animated: true, completion: nil)
} else {
print("カメラ許可をしていない時の処理")
}
}
private func selectFromLibrary() {
if UIImagePickerController.isSourceTypeAvailable(.PhotoLibrary) {
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
imagePickerController.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
imagePickerController.allowsEditing = true
self.presentViewController(imagePickerController, animated: true, completion: nil)
} else {
print("カメラロール許可をしていない時の処理")
}
}
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
if let image = info[UIImagePickerControllerEditedImage] {
sendImageMessage(image)
}
picker.dismissViewControllerAnimated(true, completion: nil)
}
private func sendImageMessage(image: UIImage) {
let photoItem = JSQPhotoMediaItem(image: image)
let imageMessage = JSQMessage(senderId: senderId, displayName: senderDisplayName, media: photoItem)
messages.append(imageMessage)
finishSendingMessageAnimated(true)
}
これで写真を投稿することができたかと思います。
メッセージで送った画像のタップイベントを受け取る
先ほど、メッセージで画像を送るところまではできているかと思います。続いてやるのは、よくチャットアプリである画像をタップして拡大を行うために、画像のタップを検知する処理を書くことです。
JSQMessagesViewControllerにはfunc collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!)というメソッドがあります。このメソッドでbubble(吹き出し)のタッチを検知し、さらに画像だった場合に別のメソッドを呼ぶという処理を書いていきます。
override func collectionView(collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAtIndexPath indexPath: NSIndexPath!) {
if messages[indexPath.item].isMediaMessage {
let media = messages[indexPath.item].media
if media .isKindOfClass(JSQPhotoMediaItem) {
print("tapped Image")
}
}
}
これで画像を送信してタップをすることでprintで書いた文章が呼ばれるかと思います。
最後に
以上が、SwiftでJSQMessagesViewControllerを使用した際のチャットUIの実装方法になります。今回紹介したのはあくまで最低限の実装方法なので、興味ある方は、他にも詳しく調べて是非実装してみてください。
間違え等ありましたらコメントなりで連絡を頂けると嬉しいです。