こんにちは!
Diverse Advent Calendar 2018の23日目を担当するiOSエンジニアの中川です。
普段は株式会社DiverseでYYCの開発を担当しながら個人でもiOSアプリ開発の仕事をしています。
さて、今回は「PringとMuniを使ってリアルタイムチャットを爆速で実装する」というテーマで記事を書かせて頂きます。
チャットの実装ってかなり工数重そうですよね、、、。
Firebaseを使えばサーバーの開発がいらないので工数は抑えられると思いますが、データの持たせ方を考えたり、チャット画面のUIを作ったりとやることはそれなりに多い気がしてます。
この記事を元に実装すれば工数のかかるリアルタイムチャットを簡単かつ爆速で実装することができます。
チャットの実装に困っている方、これから実装しようとしている方の手助けになれれば幸いです。
##注意
この記事ではFirebaseの導入とユーザー認証が終わっている前提で説明させて頂きます。
Firebaseの導入についてはこちら
ユーザー認証についてはこちら
を参考にしてください。
またDBはCloudFireStoreを使用します。
##準備するもの
今回チャットを爆速で実装するにあたって肝になるのが下記2つのライブラリです。
PringはFirebaseをかなり扱いやすくしてくれる神ライブラリです。
Pringについてはこちらをご覧ください。
Muniについてはこの記事で解説していきます。
###開発環境
- macOS Mojave
- Xcode 10.1
- Swift4
##実装しよう
##まずCocoapodsで必要なライブラリをインストール
pod 'Pring'
pod 'Muni'
##必要なモデルを実装
今回の主な登場人物はこちらです。
- User (ユーザー情報)
- Room (自分と相手がいるRoomの情報)
- Transcript (メッセージの情報)
###User
UserにはUserProtocolを準拠させます。
import Pring
import Muni
extension Firebase {
@objcMembers
class User: Object, UserProtocol {
dynamic var name: String?
dynamic var thumbnailImage: File?
}
}
###Room
RoomにはRoomProtocolを準拠させます。
import Pring
import Muni
import Firebase
extension Firebase {
@objcMembers
class Room: Object, RoomProtocol {
typealias TranscriptType = Firebase.Transcript
dynamic var name: String?
dynamic var thumbnailImage: File?
dynamic var members: [String] = []
dynamic var viewers: [String] = []
dynamic var lastViewedTimestamps: [String : Timestamp] = [:]
dynamic var recentTranscript: [String : Any] = [:]
dynamic var transcripts: NestedCollection<Firebase.Transcript> = []
dynamic var isMessagingEnabled: Bool = false
dynamic var isHidden: Bool = false
dynamic var config: [String : Any] = [:]
}
}
###Transcript
TranscriptにはTranscriptProtocolを準拠させましょう。
import Pring
import Muni
import FirebaseFirestore
extension Firebase {
@objcMembers
class Transcript: Object, TranscriptProtocol {
dynamic var to: Relation<Firebase.Room> = .init()
dynamic var from: Relation<Firebase.User> = .init()
dynamic var text: String?
dynamic var image: File?
dynamic var video: File?
dynamic var audio: File?
dynamic var location: GeoPoint?
dynamic var sticker: String?
dynamic var imageMap: [File] = []
}
}
いずれもMuniのProtocolを準拠させると補完で必要なプロパティが出てくるので、出てきたプロパティにdynamicをつけて初期値を設定してください。(※dynamicをつけないとFirebaseに保存されません。)
TimestampとGeoPointという型がFirebaseとFirebaseFirestoreが持っている型なので、それぞれimportが必要です。
以上で必要なモデルの実装は終わりです!
##メッセージ一覧画面を作る
次はメッセージの一覧を表示するViewControllerを作ります。
こちらもMuniが必要なものを提供しているのでそれを使って実装します。
Muni<Firebase.User, Firebase.Room, Firebase.Transcript>.InboxViewController
を継承したサブクラスを作ります。
import Muni
import Pring
class BoxViewController: Muni<Firebase.User, Firebase.Room, Firebase.Transcript>.InboxViewController {
override func loadView() {
super.loadView()
self.title = "メッセージ"
self.tableView.tableFooterView = UIView()
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource.onCompleted { [weak self] (_, room) in
self?.view.setNeedsDisplay()
self?.tableView.reloadData()
}
// ※親クラスのlisten()を呼び出さないとデータが更新されないので注意
self.listen()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
// MuniのMessagesViewControllerを継承したMessageViewControllerを返す
override func messageViewController(with room: Firebase.Room) -> Muni<Firebase.User, Firebase.Room, Firebase.Transcript>.MessagesViewController {
let messageViewController = MessageViewController(room: room)
messageViewController.hidesBottomBarWhenPushed = true
return messageViewController
}
注意点はコメントで記載しているのでご確認ください!
Muniが提供しているデフォルトのCellを使う場合の実装はこれだけです。
ただこの場合だと表示されるCellに相手のユーザー名とサムネイルが入らないので下記のように
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
をoverrideして下記のように自分でユーザ名とサムネイルを入れて上げる必要があります。
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// DataSourceからRoomを取得
let room: Firebase.Room = self.dataSource[indexPath.item]
// 特にカスタムせずデフォルトのCellを使う場合はIdentifierを"InboxViewCell"を指定
let cell: InboxViewCell = tableView.dequeueReusableCell(withIdentifier: "InboxViewCell", for: indexPath) as! InboxViewCell
if let meID: String = UserManager.default.me?.id, let to: [String : Any] = room.config[meID] as? [String : Any] {
Firebase.User.get(to["id"] as! String) { (user, error) in
if let error = error {
print(error)
return
}
if let user = user {
cell.nameLabel.text = user.name
if let ref = user.thumbnailImage?.ref {
// ここで使っているsd_setImageはFirebaseが提供しているpod 'FirebaseUI/Storage'のものです
// SDWebImageのものではないのでご注意ください
cell.thumbnailImageView.sd_setImage(with: ref)
}
}
}
}
cell.dateLabel.text = self.dateFormatter.string(from: room.updatedAt.dateValue())
if let name: String = room.name {
cell.nameLabel.text = name
} else if let config: [String: Any] = room.config[self.userID] as? [String: Any] {
if let nameKey: String = Firebase.Room.configNameKey {
cell.nameLabel.text = config[nameKey] as? String
}
}
if let text: String = room.recentTranscript["text"] as? String {
cell.messageLabel?.text = text
}
if room.viewers.contains(self.userID) {
cell.format = .normal
} else {
cell.format = .bold
}
return cell
}
今回はべた書きしてますが、サブクラスにしたい場合はInboxViewCellを継承すればOKです。
##メッセージ画面を作る
続いてメッセージ画面を実装します。
こちらに関してもMuniのクラスを継承することで簡単に実装できます。
Muni<Firebase.User, Firebase.Room, Firebase.Transcript>.MessagesViewController
を継承したサブクラスを作ります。
import Muni
import Toolbar
import FirebaseAuth
import Firebase
class MessageViewController: Muni<Firebase.User, Firebase.Room, Firebase.Transcript>.MessagesViewController {
// メッセージを送信するボタン
var sendBarItem: ToolbarItem!
// senderIDをoverrideして下記のように自分のuidを返すようにします
// これを実装しないとエラーになるので注意
override var senderID: String? {
return Auth.auth().currentUser!.uid
}
override func viewDidLoad() {
super.viewDidLoad()
// titleに入れる相手のユーザー名とサムネイルを取得してViewに反映します
// 相手のユーザーはroom.memberから自分のIDじゃないものを取って相手のIDとしています
guard let user = UserManager.default.me else { return }
let partnerID: String = room.members.filter { $0 != user.id }.first!
Firebase.User.get(partnerID) { [weak self] (user, error) in
guard let user = user else { return }
self?.titleView?.nameLabel.text = user.name
guard let thumbnailImageView = self?.titleView?.thumbnailImageView else { return }
thumbnailImageView.clipsToBounds = true
thumbnailImageView.layer.cornerRadius = thumbnailImageView.frame.width/2
if let ref = user.thumbnailImage?.ref {
thumbnailImageView.sd_setImage(with: ref)
}
}
// 送信ボタンのtitleとボタンを押したときのアクションを設定
// アクションは親クラスのsendを呼び出しましょう
self.sendBarItem = ToolbarItem(title: "送信", target: self, action: #selector(send))
// Toolbarの設定
// 下記のようにsetItemsにメッセージを入力するtextViewと送信するボタンを設定します。
self.toolBar.setItems([ToolbarItem(customView: self.textView), self.sendBarItem], animated: false)
// こちらもlistenしないと値が更新されないので注意
self.listen()
}
// textViewのテキストがnil、空じゃないときに送信できるようにします。
override func transcript(_ transcript: Firebase.Transcript, shouldSendTo room: Firebase.Room) -> Bool {
if let text: String = textView.text, text.isEmpty {
return false
} else {
return true
}
}
// メッセージが送信されるときにtranscript.textに入力されたテキストをいれてtextView.textをnilにします。
override func transcript(_ transcript: Firebase.Transcript, willSendTo room: Firebase.Room, with batch: WriteBatch) {
if self.textView.text.isEmpty { return }
transcript.text = self.textView.text
self.textView.text = nil
}
}
複雑そうなメッセージ画面もたったこれだけで実装完了です。
##メッセージする相手とのRoomのデータを保存する
今回は下記のようにUser(メッセージする相手)を渡してRoomを作成するようにしました。
private func roomCreate(to: Firebase.User) {
let room: Firebase.Room = Firebase.Room()
let from: Firebase.User = UserManager.default.me!
room.members = [from.id, to.id]
room.config[to.id] = ["id": from.id]
room.config[from.id] = ["id": to.id]
room.save()
}
##保存されたデータを見てみよう
FireStoreに保存されたデータはこのようになっています。
###Transcript
Transcriptは新規メッセージが保存されるとTranscripsに追加されていきます。
##カスタマイズについて
###テキスト以外の送信
MuniのTranscriptProtocolにはimage, video, Audioなどといったテキスト以外を保存するプロパティがありますので画像や動画の送信も可能です。
その場合は
相手が送信したメッセージ: MessageViewLeftCell
自分が送信したメッセージ: MessageViewRightCell
を継承したサブクラスを作って画像や動画を表示するUIを実装してください。
###Toolbar
Toolbarのカスタムについては下記のUIViewを渡すイニシャライザが用意されているのでこちらを使いましょう。
public convenience init(customView: UIView) {
self.init(frame: .zero)
self.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
self.customView = customView
}
#終わりに
いかがだったでしょうか?
本来ならかなり工数のかかるチャットですが、FirebaseとPring, Muniを使うことでたったこれだけの実装でリアルタイムチャットを作ることができます!
ユーザー登録までできている状態であれば爆速で実装できるかと思います!!
今回作ったサンプルはこちら
さて、明日24日は@seripaiさんの**「ツール尽くして彼氏を作る!」**です!
とても気になるタイトルですね!笑
ぜひ楽しみに!!