teratail header banner
teratail header banner
質問するログイン新規登録

回答編集履歴

1

追記

2021/04/11 12:14

投稿

hoshi-takanori
hoshi-takanori

スコア7903

answer CHANGED
@@ -21,7 +21,7 @@
21
21
  ```
22
22
 
23
23
  ```swift
24
- func checkExistUserInfo (_ uid: String, callback: @escaping (Bool) -> Void) {
24
+ func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) {
25
25
  db.collection("Users").whereField("user_id", isEqualTo: uid).getDocuments { (querySnapshot, err) in
26
26
  var exist: Bool = false
27
27
  if let err = err {
@@ -38,4 +38,82 @@
38
38
 
39
39
  ---
40
40
 
41
- ところで、ユーザーの情報を持つドキュメントにアクセスするために user_id フィールドに uid を持たせてますが、ドキュメントの id を uid にすれば検索しなくても直接そのドキュメントにアクセスできるため、insertUserInfo と updateUserInfo を区別する必要がなくなります。(存在しないドキュメントに直接 setData できますし、merge: true を指定すれば既存のフィールドはそのままで新しい値だけ設定できます。)
41
+ ところで、ユーザーの情報を持つドキュメントにアクセスするために user_id フィールドに uid を持たせてますが、ドキュメントの id を uid にすれば検索しなくても直接そのドキュメントにアクセスできるため、insertUserInfo と updateUserInfo を区別する必要がなくなります。(存在しないドキュメントに直接 setData できますし、merge: true を指定すれば既存のフィールドはそのままで新しい値だけ設定できます。)
42
+
43
+ ---
44
+
45
+ > しかしcheckExistUserInfoメソッドの動きについてかなり調べたのですが、
46
+ > 自分の理解力が乏しくどういう動きをしているのか理解ができませんでした・・・
47
+
48
+ まず、クロージャがどういうものか理解が曖昧なのでは。クロージャは次のように独立したメソッドとして書くこともきます。(クロージャをそれぞれ registerCallback, checkExistUserInfoCallback として切り出してみましたが、コンパイルを通してないので、文法的には曖昧です。また、メソッドに分けると checkExistUserInfo に渡す callback の受け渡し方法が問題になります…。)
49
+
50
+ ```swift
51
+ class ViewController: UIViewController {
52
+
53
+ // 呼び出し順: A1
54
+ @IBAction func registerUser() {
55
+ //ユーザー情報の登録を行う
56
+ if baseDto.uid != nil {
57
+ dbAccessModel.checkExistUserInfo(baseDto.uid, callback: registerCallback)
58
+ }
59
+ }
60
+
61
+ // 呼び出し順: B4
62
+ func registerCallback(exists: Bool) {
63
+ //ユーザー情報が既に存在する場合は更新、それ以外の場合は登録
64
+ if exists {
65
+ dbAccessModel.updateUserInfo(baseDto)
66
+ } else {
67
+ dbAccessModel.insertUserInfo(baseDto)
68
+ }
69
+ }
70
+ }
71
+
72
+ class DBAccessModel {
73
+ // 仮に callback をプロパティとする。
74
+ private var callback: ((Bool) -> Void)?
75
+
76
+ // 呼び出し順: A2
77
+ func checkExistUserInfo(_ uid: String, callback: @escaping (Bool) -> Void) {
78
+ self.callback = callback
79
+ db.collection("Users")
80
+ .whereField("user_id", isEqualTo: uid)
81
+ .getDocuments(checkExistUserInfoCallback)
82
+ }
83
+
84
+ // 呼び出し順: B2
85
+ func checkExistUserInfoCallback(querySnapshot: QuerySnapshot?, err: Error?) {
86
+ var exist: Bool = false
87
+ if let err = err {
88
+ print("Error getting documents: (err)")
89
+ } else {
90
+ if querySnapshot!.documents.count > 0 {
91
+ exist = true
92
+ }
93
+ }
94
+ callback(exist)
95
+ }
96
+ }
97
+ ```
98
+
99
+ この処理の実行は、次の A と B の 2 つのフェーズで実行されます。
100
+
101
+ A1 登録ボタンを押すと registerUser が呼ばれる。
102
+ A2 registerUser から dbAccessModel.checkExistUserInfo を呼び出す。
103
+ A3 checkExistUserInfo は Firebase の db.collection 〜 .getDocuments を呼び出す。
104
+ A4 getDocuments は Firebase サーバーに問い合わせを開始し、すぐに return する。
105
+
106
+ ここでいったんボタンを押したイベントに対する処理は終了して、メインスレッドは別のイベント (画面の更新や、他のボタンを押したイベントなど) を処理できるようになります。そして、Firebase サーバーからお返事が返ってきたら、次の処理が行われます。
107
+
108
+ B1 Firebase サーバーからお返事が返る。
109
+ B2 getDocuments のコールバック checkExistUserInfoCallback が呼ばれる。
110
+ B3 checkExistUserInfoCallback では結果をチェックして exist の値をセットする。
111
+ B4 その exist を引数にして checkExistUserInfo のコールバック registerCallback を呼ぶ。
112
+
113
+ このように複数のフェーズに分かれて処理が行われるのが非同期処理の特徴です。そして、クロージャの部分は独立した関数として考える必要があり、かつ呼ばれるタイミングが違うことを理解する必要があります。
114
+
115
+ ---
116
+
117
+ > あ、mergeを指定すれば、なければInsert、あればupdateに自動的になるということでしょうか。
118
+
119
+ ドキュメントが存在しなければ merge の指定に関係なく insert になります。問題はドキュメントが存在する場合ですが、元のドキュメントが { id: 123, name: "abc", address: "def" } で、baseDto が { id: 123, name: "xyz" } の場合、merge を指定しない (または false を指定する) とドキュメントは上書きされるので address: "def" が失われてしまいますが、merge: true とした場合は baseDto に含まれないものはそのまま残ります。