イノベーション エンジニアブログ


株式会社イノベーションのエンジニアたちの技術系ブログです。ITトレンド・List Finderの開発をベースに、業務外での技術研究などもブログとして発信していってます!


このエントリーをはてなブックマークに追加

【Swift3】ランダムマッチチャットアプリを作ったお話。

こんばんは、こへです。

そろそろ、Swift3とFirebaseを使って、完全匿名ランダムマッチングチャットを作りたい時期が来ましたので作成。

やりたいこと

  • 1対1のチャット

  • 匿名

  • ランダムにマッチング

  • iPhoneアプリ

  • 確認はシミュレータと実機でチャットをする

以上!!

画面イメージ設計

randomChat.002.png

ロード中(マッチング相手を探し中)くるくる回すインテグレーションだけにはこだわりたい。

DB設計

Firebase を使うためこんな感じにカキカキ。

randomChat.001.png

枠から文字がはみ出ているのはご愛嬌。

Firebase

GoogleアカウントでFirebaseに登録。 ※ココらへんの設定はググれば大量に出てくるため割愛。

書き込み読み込みのルールもパブリックにしてしまってはいかんので認証を必要にする。

{
  "rules": {
    ".read": "auth != null",
    ".write":"auth != null"
  }
}

そしてまずは、ルーム作成の際に与える数値を持つ keyとvalueを作成。

swiftchat10.png

初期値は適当。
今回はなんとなく「20」を選択。

ランダムチャットがマッチングするたびに、この値をインクリメントし、その数値を名前としてroomを作っていくよーん。

ライブラリ

  • Firebase

  • Firebase/Database

  • Firebase/Core

  • Firebase/Auth

    • ↑4つはFirebaseを使うために必須のやつ

  • JSQMessagesViewController

    • チャットをするための画面、(吹き出しなど)を簡単に実装できそうなやつ

  • SVProgressHUD

    • インジケータをいい感じに出してくれる

  • ChameleonFramework/Swift

    • いい感じの配色を楽に出してくれる

上記のこやつらをpodの野郎で取り込んでいく。
ライブラリって素晴らしい… :0

上が終わったらやっとこさXcodeさ!

画面設計の通り、色々配していく。

swiftchat2.png

チャットスタートボタンはひよこの絵を、絵がうまい同期の子に依頼。

後はナビゲーションコントローラーをつけて楽をする。

ストーリーボードの設定はこれで終わり!

Code

最初に開かれる画面

import UIKit
import SVProgressHUD

class StartViewController: UIViewController {

    //AppDelegateのインスタンスを作り、AppDelegateの変数を使えるようにする。
    let app:AppDelegate =
        (UIApplication.shared.delegate as! AppDelegate)

    override func viewDidLoad() {
        super.viewDidLoad()

        self.app.roomId = "0"
    }

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

}

AppDelegate

    var window: UIWindow?
    var deiceToken:String?
    var roomId:String?
    var newRoomId:String?
    var targetId:String?
    var chatStartFlg:Bool?

スーパグローバル変数的なものを定義。

チャット部分の画面

import UIKit
import Firebase
import JSQMessagesViewController
import SVProgressHUD

class ViewController: JSQMessagesViewController {

    var messages: [JSQMessage]?

    var incomingBubble: JSQMessagesBubbleImage!
    var outgoingBubble: JSQMessagesBubbleImage!
    var exitcomingBubble:JSQMessagesBubbleImage!


    var incomingAvatar: JSQMessagesAvatarImage!
    var outgoingAvatar: JSQMessagesAvatarImage!

    var uid:String = UUID().uuidString

    let userRef  = FIRDatabase.database().reference().child("users")
    let rootRef  = FIRDatabase.database().reference()

    let userDefaults = UserDefaults.standard

    //AppDelegateのインスタンスを作り、AppDelegateの変数を使えるようにする。
    let app:AppDelegate =
        (UIApplication.shared.delegate as! AppDelegate)


    override func viewDidLoad() {
        super.viewDidLoad()


        //初回時のみ自分のIDを保存しておく、それ移行は使い回し
        if(userDefaults.string(forKey: "uid") != nil){
            uid = userDefaults.string(forKey: "uid")!
        }else{
            userDefaults.set(uid, forKey: "uid")
        }

        print("自分のuid→\(self.uid)")

        //Indicatorを回す
        SVProgressHUD.setDefaultStyle(SVProgressHUDStyle.dark)
        //↓の処理を入れるとIndicatorが回っている間は背後のuiが非活性になる。
   //     SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
        SVProgressHUD.show(withStatus: "チャット相手をさがしています")

        self.app.chatStartFlg = false

        //表示される名前(未実装)
        self.senderDisplayName = "hoge"
        setupFirebase()
        getRoom()
        setupChatUi()

        self.messages = []
    }

    func setupFirebase() {

        //匿名アカウントを認証する
        FIRAuth.auth()?.signInAnonymously() { (user, error) in
            if error != nil {
                //エラー時の処理
                print("失敗")
                return
            }
            //成功時の処理
            print("成功")

        }
    }

    func getRoom(){

        let user = ["name": senderDisplayName,
                        "inRoom": "0",
                        "waitingFlg": "0"]

        userRef.child(self.uid
            ).setValue(user)


        //一回だけwaitingFlgが1のユーザを取得
        userRef.queryOrdered(byChild: "waitingFlg").queryEqual(toValue: "1").observeSingleEvent(of: .value, with: { (snapshot) in

            let value = snapshot.value as? NSDictionary

            if(value != nil){
                if (value!.count >= 1) {
                    //ルームを作る側の処理
                    print(value!.count);
                    print("value \(value!)")
                    print("↑初回ボタン押下時に、waitingFlgが1のユーザ")

                    self.createRoom(value: value as! Dictionary<AnyHashable, Any>)
                }
            } else {
                //ルームを作られるのを待つ側の処理
                self.userRef.child(self.uid).updateChildValues(["waitingFlg":"1"])
                self.checkMyWaitingFlg();
            }
        });


    }

    //他のユーザが自分とマッチするまで待機する。
    func checkMyWaitingFlg(){
        userRef.child(self.uid).observe(FIRDataEventType.childChanged, with: { (snapshot) in
            print(snapshot)
            let snapshotVal = snapshot.value as! String
            let snapshotKey = snapshot.key

            if (snapshotVal == "0" && snapshotKey == "waitingFlg"){
                self.getJoinRoom()
            }
        })
    }


    //自身のjoinする(している)roomIdを取得
    func getJoinRoom(){
        userRef.child(self.uid).child("inRoom").observeSingleEvent(of: .value, with: { (snapshot) in
            //返ってくる値が1つしか無いからstr型になる
            let snapshotValue = snapshot.value as! String
            self.app.roomId   = snapshotValue

            if(self.app.roomId != "0"){
                print("roomId→ \(self.app.roomId!)")
                print("チャットを開始します")
                self.getMessages()
            }

        })

    }

    //roomIdからそのroomのmessage情報を取得する
    //chatが始まる際必ず呼ばれるmethod
    func getMessages(){

        //Indicatorを止める
        SVProgressHUD.dismiss()

        SVProgressHUD.showSuccess(withStatus: "マッチングしました!")

        self.app.chatStartFlg = true


        //【非同期】子要素が増えるたびにmessageに値を追加する。
        rootRef.child("rooms").child(self.app.roomId!).queryLimited(toLast: 100).observe(FIRDataEventType.childAdded, with: { (snapshot) in
            let snapshotValue = snapshot.value as! NSDictionary
            let text          = snapshotValue["text"] as! String
            let sender        = snapshotValue["from"] as! String
            let name          = snapshotValue["name"] as! String

            print("display名前→\(name)")
            let message       = JSQMessage(senderId: sender, displayName: name, text: text)
            self.messages?.append(message!)
            self.finishReceivingMessage()
        })

    }


    func createRoom(value:Dictionary<AnyHashable, Any>){

        //chatを始めるユーザを取得。

        for (key,val) in value {

            //自分のidと違うユーザを取得
            if (key as! String != self.uid){

                //待機中のユーザがいた場合(必ずこの処理で居るが)の処理
                print("待機中のユーザId\(key)")
                self.app.targetId = key as? String

            }
        }


        print("チャット開始するユーザId\(self.app.targetId!)")

        //新規のRoomを作るための数値を取得
        getNewRoomId()


    }

    var count:Int = 1

    //新しくルームを作る際の数値を取得、そしてDBも更新
    func getNewRoomId(){

        FIRDatabase.database().reference().child("roomKeyNum").observeSingleEvent(of: .value, with: { (snapshot) in

            if !(snapshot.value is NSNull){
                self.count = (snapshot.value as! Int) + 1
            }
            FIRDatabase.database().reference()
                .child("roomKeyNum")
                .setValue(self.count)

            self.app.newRoomId = String(self.count)
            self.updateEachUserInfo()

        }) { (error) in
            print(error.localizedDescription)
        }

    }

    //1to1でマッチした場合、お互いの情報を更新する。
    func updateEachUserInfo(){

        self.app.roomId = self.app.newRoomId

        print (self.app.roomId!)
        print (self.app.newRoomId!)
        //ユーザ情報を書き換えていく。
        userRef.child(self.app.targetId!).updateChildValues(["inRoom":self.app.roomId!])
        userRef.child(self.app.targetId!).updateChildValues(["waitingFlg":"0"])
        userRef.child(self.uid).updateChildValues(["inRoom":self.app.roomId!])
        userRef.child(self.uid).updateChildValues(["waitingFlg":"0"])

        //新しく作ったルームのidの情報を取ってくる処理【非同期】
        getMessages()
    }

    func setupChatUi() {
        inputToolbar!.contentView!.leftBarButtonItem = nil
        automaticallyScrollsToMostRecentMessage = true

        //firebaseのfromカラムに入る値
        self.senderId = self.uid


        self.collectionView.collectionViewLayout.incomingAvatarViewSize = CGSize.zero

        self.collectionView.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero


        let bubbleFactory = JSQMessagesBubbleImageFactory()
        self.incomingBubble = bubbleFactory?.incomingMessagesBubbleImage(
            with: UIColor.gray)
        self.outgoingBubble = bubbleFactory?.outgoingMessagesBubbleImage(
            with: UIColor.jsq_messageBubbleGreen())
        self.exitcomingBubble = bubbleFactory?.incomingMessagesBubbleImage(
            with: UIColor.jsq_messageBubbleRed())
    }



    //別の画面に遷移する直前に実行される処理
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("ViewController/viewWillDisappear/別の画面に遷移する直前")

    }


    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        print("ViewController/viewDidDisappear/別の画面に遷移した直後")

        userRef.child(self.uid).updateChildValues(["waitingFlg":"0"])

        //Indicatorを止める
        SVProgressHUD.dismiss()

        if(self.app.roomId != "0"){
            //相手に退室のメッセージを送るようにする。
            let endMsg = "~相手が退出したよ!!~"
            sendTextToDb(text: endMsg,exitFlg: true)
            self.app.roomId = "0"
        }
    }



    //====================↓JSQMessageの色々======================//


    //メッセージの送信
    override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {

        self.finishSendingMessage(animated: true)

        if(self.app.chatStartFlg! == true){
            sendTextToDb(text: text)
        }else{
            print("チャット相手検索中です。")
        }

    }

    func sendTextToDb(text: String,exitFlg:Bool = false) {
        //データベースへの送信(後述)

        let rootRef = FIRDatabase.database().reference()

        var tmpSenderId = senderId as String
        if(exitFlg){
            tmpSenderId = "exit"
        }

        let post:Dictionary<String, Any>? = ["from": tmpSenderId,
                                             "name": senderDisplayName,
                                             "text": text]

        let postRef = rootRef.child("rooms").child(self.app.roomId!).childByAutoId()

        postRef.setValue(post)


    }

    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
        return self.messages?[indexPath.item]
    }

    //バブルの色を返す。
    override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
        let message = self.messages?[indexPath.item]
        if message?.senderId == self.senderId {
            return self.outgoingBubble
        } else if message?.senderId == "exit"{
            return self.exitcomingBubble
        }
        return self.incomingBubble
    }

    //アバター(サムネを返す)
    override func collectionView(_ collectionView: JSQMessagesCollectionView!,
                                 avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
        let message = self.messages?[indexPath.item]
        if message?.senderId == self.senderId {
            return self.outgoingAvatar
        }
        return self.incomingAvatar
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return (self.messages?.count)!
    }
}

動き

swiftchat.gif

これに色々と機能をつけてリリースしますんでそのときはおなしゃす:)