ホーム » Swift » 【iOS×Firebase】ListenerRegistrationを使ったFirestoreDatabaseのCRUDに関して

【iOS×Firebase】ListenerRegistrationを使ったFirestoreDatabaseのCRUDに関して

Swift

ListenerRegistrationとは

  • WebSocketを利用してデータ更新を行うリアルタイムリスナー
  • Firebaseに更新があれば非同期でその更新を受け取ることが可能
  • Firebaseはリアルタイムでネットワーク通信を行う時にその裏側を意識しなくても使える便利な機能(ポーリング不要、定期取得不要)
  • データベースのアップデートに使うと便利

例:Firebase databaseのCloud Fire Stormをチャットアプリを作る場合を想定

FirestoreDatabaseにデータベースを以下のような形で持つとする

Create:リスナーは使わない

データのパス

enum FCollectionReference:String{
    case User
    case Recent
    case Message
    case Typing
    case Channel
}

func FirebaseReference(_ collectionReference:FCollectionReference) -> CollectionReference{
    return Firestore.firestore().collection(collectionReference.rawValue)
}

Create

//MARK: - Add
func addMessage(_ message:LocalMessage,memberId:String){
    do{
        let _ = try Firestore.firestore().collection(FCollectionReference.Message.rawValue).document(memberId)
   .collection(message.chatRoomId).document(message.id).setData(from: message)
    }catch{
        print("error saving",error.localizedDescription)
    }
}

砕いて書くと以下

func addMessage(_ message:LocalMessage,memberId:String){
    do{
        let _ = try Firestore.firestore().collection("FilePath").document(memberId)
   .collection(message.chatRoomId).document(message.id).setData(from: message)
    }catch{
        print("error saving",error.localizedDescription)
    }
}

Collection→documentでディレクトリのフォルダパスのようなイメージ、そこに対してsetDataを行うとドキュメントの追加がなされる。

さらに階層をつけにディレクトリを作る場合は
Collection→document、Collection→document そこに対してsetData(data)を入れるとすれば、
トップフォルダ→フォルダ1→フォルダ2→フォルダ3→フォルダ3にデータを入れる。

こんなイメージをすればよい

Read:現在のFirebaseのデータを読み取る(リスナーは使わない)

//Firebaseにmessageがあるか確認してあったらRealmに保存する
    //保存した後にRealmのデータをチャットに表示する
    func checkForOldChats(_ documentId:String,CollectionId:String){
        FirebaseReference(.Message).document(documentId).collection(CollectionId).getDocuments { snapshot, error in
            
            guard let documets = snapshot?.documents else {
                print("no documents for old chats")
                return
                
            }
            //realmにデータを保存するデータをcompactMapでデコードしてLocalMessageで取得する
            var oldMessages = documets.compactMap {
                //クロージャの引数とクロージャの戻り値
                (QueryDocumentSnapshot) -> LocalMessage? in
                return try? QueryDocumentSnapshot.data(as: LocalMessage.self)
            }
            
            //並び替え
            oldMessages.sort(by: {$0.date < $1.date})
            
            for message in oldMessages{
                RealmManager.shared.saveToRealm(message)
            }
            
        }
    }

砕いて書くと

    //保存した後にRealmのデータをチャットに表示する
    func checkForOldChats(_ documentId:String,CollectionId:String){
        Firestore.firestore().collection("FilePath").document(documentId).collection(CollectionId).getDocuments { snapshot, error in
            //snapshotに取得したデータが保存されている。nilであればreturn
            guard let documets = snapshot?.documents else {
                print("no documents for old chats")
                return
                
            }
            //realmにデータを保存するデータをcompactMapでデコードしてLocalMessageで取得する
            var oldMessages = documets.compactMap {
                //クロージャの引数とクロージャの戻り値
                (QueryDocumentSnapshot) -> LocalMessage? in
                return try? QueryDocumentSnapshot.data(as: LocalMessage.self)
            }
            
            //古いものが上にくるように並び替え
            oldMessages.sort(by: {$0.date < $1.date})
            
            for message in oldMessages{
                RealmManager.shared.saveToRealm(message)
            }
            
        }
    }

結局これはgetDocumentsを使っているにすぎない

Update:これが重要でリスナーを使って行う

①リスナーを使わない場合

//MARK: - UpdateMassageStatus
    func updateMessageInFirebase(_ message:LocalMessage,memberIds:[String]){
        let values = [kSTATUS:kREAD,kREADDATE:Date()] as [String:Any]
        
        for userId in memberIds{
            //FirebaseのchatRoomIdの対象のメッセージのデータをアップデートする。
            FirebaseReference(.Message).document(userId).collection(message.chatRoomId).document(message.id).updateData(values)
        }  
    }

②リスナーを使う場合

チャットアプリでaddSnapshotListerで指定したパスのメッセージの変更を取得する

//update
    //新しいメッセージを受信して取得する。Firebaseのデータを受信してdb(Realm)に反映する。
    //dbに変更があったときにローカルで表示を変更するがそれはloadChatsでRealmのリスナーを作成して反映させている
    func listenForNewChats(_ documentId:String,CollectionId:String,lastMessageDate:Date){
        //ローカルに持っているmessageの最後の日付より新しいデータを取得する
        //addSnapshotListenerで指定したパスのメッセージの変更を取得するListenerを取得出来る
        newChatListener = FirebaseReference(.Message).document(documentId).collection(CollectionId).whereField(kDATE, isGreaterThan: lastMessageDate).addSnapshotListener({ snapshot, error in
            //snapshotを取得して空であればここでreturn
            guard let snapshot = snapshot else {
                return
            }
            //snapshotのdocumentChangesでtypeが追加(.added)の時は新しいデータがFirebaseにあるのでローカルデータベースにも追加
            for change in snapshot.documentChanges{
                if change.type == .added{
                    let result = Result{
                        try? change.document.data(as:LocalMessage.self)
                    }
                    
                    switch result {
                    case .success(let messageObject):
                        if let message = messageObject{
                            
                            //メッセージを送信するときにRealm(ローカルdb)に保存しているので、送信者が自分の時はRealmを更新する必要はない
                            if message.senderId != User.currentId{
                                RealmManager.shared.saveToRealm(message)
                            }
                        }else{
                            print("Document dosent exist")
                        }
                    case .failure(let error):
                        print("Error decoding local message \(error.localizedDescription)")
                    }
                }
            }
            
        })
        
    }

addSnapshotListenerが重要Listenerを設定したオブジェクトに変化があるとクロージャが実行されて、メッセージの変更をアプリが検知することが出来て、メッセージの受信をアプリでリアルタイムで取得できる

Delete:Firebase上のデータ削除

func deleteRecent(_ recent:RecentChat){
        FirebaseReference(.Recent).document(recent.id).delete()
    }

まとめ

ListenerRegistrationを使うとCRUDのUの部分が自動で受信されるのでチャットアプリなどで便利

全体通してちゃんとした説明になってなくてすいません。。これを噛み砕いて説明できるようになりたい。

コメント

タイトルとURLをコピーしました