結論から言うと、非同期処理を同期処理に変換することはできませんので、ちゃんと非同期として処理しましょう。
DispatchSemaphore を使ったコードが動かない理由は、Firebase のコールバックはメインスレッドで処理されますが、semaphore.wait() が先に実行されるので、これが終わらないとコールバックは実行されません。ところが、semaphore.wait() はコールバックで semaphore.signal() が実行されるのを待ってますので、デッドロックになり、永遠に終了しません。
また、(Firebase では不可能ですが) たとえコールバックをバックグラウンドで呼び出すことが可能だったとしても、処理を待つ間はメインスレッドを停止することになり、これは非常に良くないことです。
とりあえず今回のコードを非同期処理として書き直すと次のようになるでしょう。(なお、非同期処理の終了を待つ間に他の操作をされないような対策が必要になると思います。)
swift
1 //ユーザー情報の登録を行う
2 if baseDto.uid != nil {
3 //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録
4 dbAccessModel.checkExistUserInfo(baseDto.uid) { exists in
5 if exists {
6 dbAccessModel.updateUserInfo(baseDto)
7 } else {
8 dbAccessModel.insertUserInfo(baseDto)
9 }
10 }
11 }
swift
1 func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) {
2 db.collection("Users").whereField("user_id", isEqualTo: uid).getDocuments { (querySnapshot, err) in
3 var exist: Bool = false
4 if let err = err {
5 print("Error getting documents: (err)")
6 } else {
7 if querySnapshot!.documents.count > 0 {
8 exist = true
9 }
10 }
11 callback(exist)
12 }
13 }
ところで、ユーザーの情報を持つドキュメントにアクセスするために user_id フィールドに uid を持たせてますが、ドキュメントの id を uid にすれば検索しなくても直接そのドキュメントにアクセスできるため、insertUserInfo と updateUserInfo を区別する必要がなくなります。(存在しないドキュメントに直接 setData できますし、merge: true を指定すれば既存のフィールドはそのままで新しい値だけ設定できます。)
しかしcheckExistUserInfoメソッドの動きについてかなり調べたのですが、
自分の理解力が乏しくどういう動きをしているのか理解ができませんでした・・・
まず、クロージャがどういうものか理解が曖昧なのでは。クロージャは次のように独立したメソッドとして書くこともきます。(クロージャをそれぞれ registerCallback, checkExistUserInfoCallback として切り出してみましたが、コンパイルを通してないので、文法的には曖昧です。また、メソッドに分けると checkExistUserInfo に渡す callback の受け渡し方法が問題になります…。)
swift
1class ViewController: UIViewController {
2
3 // 呼び出し順: A1
4 @IBAction func registerUser() {
5 //ユーザー情報の登録を行う
6 if baseDto.uid != nil {
7 dbAccessModel.checkExistUserInfo(baseDto.uid, callback: registerCallback)
8 }
9 }
10
11 // 呼び出し順: B4
12 func registerCallback(exists: Bool) {
13 //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録
14 if exists {
15 dbAccessModel.updateUserInfo(baseDto)
16 } else {
17 dbAccessModel.insertUserInfo(baseDto)
18 }
19 }
20}
21
22class DBAccessModel {
23 // 仮に callback をプロパティとする。
24 private var callback: ((Bool) -> Void)?
25
26 // 呼び出し順: A2
27 func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) {
28 self.callback = callback
29 db.collection("Users")
30 .whereField("user_id", isEqualTo: uid)
31 .getDocuments(checkExistUserInfoCallback)
32 }
33
34 // 呼び出し順: B2
35 func checkExistUserInfoCallback(querySnapshot: QuerySnapshot?, err: Error?) {
36 var exist: Bool = false
37 if let err = err {
38 print("Error getting documents: (err)")
39 } else {
40 if querySnapshot!.documents.count > 0 {
41 exist = true
42 }
43 }
44 callback(exist)
45 }
46}
この処理の実行は、次の A と B の 2 つのフェーズで実行されます。
A1 登録ボタンを押すと registerUser が呼ばれる。
A2 registerUser から dbAccessModel.checkExistUserInfo を呼び出す。
A3 checkExistUserInfo は Firebase の db.collection 〜 .getDocuments を呼び出す。
A4 getDocuments は Firebase サーバーに問い合わせを開始し、すぐに return する。
ここでいったんボタンを押したイベントに対する処理は終了して、メインスレッドは別のイベント (画面の更新や、他のボタンを押したイベントなど) を処理できるようになります。そして、Firebase サーバーからお返事が返ってきたら、次の処理が行われます。
B1 Firebase サーバーからお返事が返る。
B2 getDocuments のコールバック checkExistUserInfoCallback が呼ばれる。
B3 checkExistUserInfoCallback では結果をチェックして exist の値をセットする。
B4 その exist を引数にして checkExistUserInfo のコールバック registerCallback を呼ぶ。
このように複数のフェーズに分かれて処理が行われるのが非同期処理の特徴です。そして、クロージャの部分は独立した関数として考える必要があり、かつ呼ばれるタイミングが違うことを理解する必要があります。
あ、mergeを指定すれば、なければInsert、あればupdateに自動的になるということでしょうか。
ドキュメントが存在しなければ merge の指定に関係なく insert になります。問題はドキュメントが存在する場合ですが、元のドキュメントが { id: 123, name: "abc", address: "def" } で、baseDto が { id: 123, name: "xyz" } の場合、merge を指定しない (または false を指定する) とドキュメントは上書きされるので address: "def" が失われてしまいますが、merge: true とした場合は baseDto に含まれないものはそのまま残ります。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/04/11 10:55
2021/04/11 10:57 編集
2021/04/11 12:21
2021/07/14 08:38
2021/07/14 09:10
2021/07/14 09:38