環境
言語:Swift 5.4
開発環境;xcode 12.5
クラウドデータベース:Cloud Firestore
やりたいこと
Swift×FirestoreでToDoリストアプリを作っています。
最終的には、マルチデバイスでどのデバイスとも同期できるようにしたいと思い、
Firestoreと連携するTableViewのアプリを作成しています。
困っていること
以前下記の質問をした際に、
Firestoreと連携するクラスをインスタンス化しようとすると「 Missing argument for parameter 'document' in call」のエラーが出る
回答を頂いた方からのコメントに、「テーブルに行を追加するのは、Firestore の処理が成功してからの方が良い」と頂き、確かにコンソールのログを見ると、非同期のfunc tableView(_:numberOfRowsInSection:)がテーブル行問合せを何度もしているので、下記の順番で処理をさせたいと考えています。
■初回起動時
firebaseのデータ取得処理
↓
func tableView(:numberOfRowsInSection:)
↓
func tableView(:cellForRowAt:)
■初回起動後
func tableView(:numberOfRowsInSection:)と
func tableView(:cellForRowAt:)は一般的なtableViewでの使われ方
ネット等で調べて、順番に処理をさせるには、クロージャの@escapingの書き方が使えるとわかりましたが、下記の点で行き詰まっていますので対処法を教えてもらえないでしょうか。
①func tableView(:numberOfRowsInSection:)とfunc tableView(:cellForRowAt:)でクロージャの@escapingの書き方がわからない。tableViewの引数がいろいろつきすぎてて書き方が不明。(また、そもそもfunc tableView(:numberOfRowsInSection:)とfunc tableView(:cellForRowAt:)でクロージャの@escapingをする方法が適切なのか)
②初回起動時にクロージャの@escapingで順番に処理することができても、初回起動後にfunc tableView(:numberOfRowsInSection:)とfunc tableView(:cellForRowAt:)はこれまで通りの処理になるようにしたい場合どうしたら良いのか。
import UIKit import Firebase //Firebase仕様 import FirebaseFirestoreSwift //Firebase仕様 var todoList = [MyTodo]() //MyTodo配列クラスをインスタンス化してtodoListプロパティ配列生成 //************************************************* // ViewControllerクラス //************************************************* class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let db = Firestore.firestore() //************************************************* //UITableViewのoutlet定義(行を追加した時の通知等で、UITableViewに対して操作を行うために必要となる) //************************************************* @IBOutlet weak var tableView: UITableView! //------------------------------------------------- //初回ロード処理 //------------------------------------------------- override func viewDidLoad() { super.viewDidLoad() print("ログ_viewdidload:start、時間:(logTime.formatSimple())") //データを取得 readFirestoreDocuments(completion: { // rd.readFirestoreDocuments(MyTodo().todoId, completion: { //tableViewを更新 self.tableView.reloadData() }) } //------------------------------------------------- //Firestoreからのデータ受信処理 //------------------------------------------------- func readFirestoreDocuments(completion: @escaping ()->()) { print("ログ_readFirestoreDocuments:プロシージャ@escaping開始、時間:(logTime.formatSimple())") //++++++++++++++++++++++++++++++++++++++++++++ //Firestoreからドキュメントのデータを取得して、todoListのデータ型に変換し配列の各要素に入れる //++++++++++++++++++++++++++++++++++++++++++++ //Firestoreからドキュメントのデータ取得。querySnapshotにドキュメントデータが配列になって入っている print("ログ_readFirestoreDocuments:Getting documents is start、時間:(logTime.formatSimple())") db.collection("todo").getDocuments() { (querySnapshot, err) in //Firestoreからドキュメントが取得できているか、取得したデータの内容の確認ログ if let err = err { print("ログ_readFirestoreDocuments:Error getting documents(err)、時間:(logTime.formatSimple())") } else { //データ取得の成功、データの中身のログ出力 print("ログ_readFirestoreDocuments:Getting documents is success!、時間:(logTime.formatSimple())") for document in querySnapshot!.documents{ print("ログ_readFirestoreDocuments:documentの中身を確認(document.data())、時間:(logTime.formatSimple())") } } print("ログ_readFirestoreDocuments:todoListの中身を確認(MyTodoクラスの型への変換前):(todoList)、時間:(logTime.formatSimple())") todoList = querySnapshot!.documents.map { //querySnapshotの各ドキュメントデータから要素を一つ一つ取り出して、MyTodoのデータ型に変換してtodoList配列の全ての要素に入れる(ドキュメントのデータをMyTodoのデータ型に変換したデータはdataとしてreturnで配列に戻し、各ドキュメントで繰り返す) // document in let data = MyTodo(document: document) document in let data = MyTodo(document: document) return data } print("ログ_readFirestoreDocuments:todoListの行数確認(MyTodoクラスの型への変換後):(todoList.count)、時間:(logTime.formatSimple())") //読み出し完了時のクロージャを呼び出す completion() } } //************************************************* // +ボタンをタップした時の処理(タスク入力オブジェクトの作成・処理) //************************************************* @IBAction func tapAddButton(_ sender: Any) { // タスク入力オブジェクトとなるアラートダイアログを生成 let alertController = UIAlertController( title: "ToDo追加", message: "ToDoを入力してください", preferredStyle: UIAlertController.Style.alert ) // テキストエリアを追加 alertController.addTextField(configurationHandler: nil) //************************************************* //OKボタンの配置、ボタン押下時の処理 //************************************************* // OKボタンを追加 let okAction = UIAlertAction( title: "OK", style: UIAlertAction.Style.default ){ (action: UIAlertAction) in // OKボタンがタップされたときの処理(ToDoの配列に入力値を挿入。テーブルの先頭行に挿入する) if let textField = alertController.textFields?.first { let myTodo = MyTodo()//MyTodoクラスをインスタンス化してmyTodo定数を生成 myTodo.todoTitle = textField.text! todoList.insert(myTodo, at: 0) print("ログ_func tapAddButton:新たなToDoとして入力された言葉:(myTodo.todoTitle!)") // テーブルに行を追加 self.tableView.insertRows( at: [IndexPath(row: 0, section: 0)], with: UITableView.RowAnimation.right ) let ref = self.db.collection("todo").document() print("ログ_func tapAddButton:作られたドキュメントのID:(ref.documentID)") myTodo.todoId = ref.documentID ref.setData([ "todoId": ref.documentID, "todoTitle": myTodo.todoTitle!, "todoDone": false ]) { err in if let err = err { print("ログ_func tapAddButton:Error adding document: (err)") } else { print("ログ_func tapAddButton:Document added with ID:(ref.documentID)") } } } } // OKボタンがタップされたときの処理 alertController.addAction(okAction) //************************************************* //キャンセルボタンの配置、ボタン押下時の処理 //************************************************* // CANCELボタンがタップされたときの処理 let cancelButton = UIAlertAction( title: "CANCEL", style: UIAlertAction.Style.cancel, handler: nil ) // CANCELボタンを追加 alertController.addAction(cancelButton) // アラートダイアログを表示 present(alertController, animated: true, completion: nil) } //************************************************* // テーブルの行数返却 //************************************************* // テーブルの行数を返却する func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int ) -> Int { // Todoの配列の長さを返却する print("ログ_tableView_numberOfRowsInSection:配列の長さは(todoList.count)、時間:(logTime.formatSimple())") return todoList.count } //************************************************* // テーブルのセルの表示(テーブルの行ごとにセルを返却) //************************************************* // テーブルの行ごとのセルを返却する func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { // Storyboardで指定したtodoCell識別子を利用して再利用可能なセルを取得する let cell = tableView.dequeueReusableCell( withIdentifier: "todoCell", for: indexPath ) // 行番号に合ったToDoの情報を取得 let myTodo = todoList[indexPath.row] // print("ログ_tableView_cellForRowAt:myTodoの中身は(todoList[indexPath.row])") print("ログ_tableView_cellForRowAt:行番号に合ったToDoは(String(describing: myTodo.todoTitle))、時間:(logTime.formatSimple())") // セルのラベルにToDoのタイトルをセット cell.textLabel?.text = myTodo.todoTitle // セルのチェックマーク状態をセット if myTodo.todoDone { // チェックあり cell.accessoryType = UITableViewCell.AccessoryType.checkmark } else { // チェックなし cell.accessoryType = UITableViewCell.AccessoryType.none } return cell } } //************************************************* // MyTodoクラス //************************************************* //class MyTodo: NSObject, NSSecureCoding { class MyTodo: NSObject{ //Firebase仕様(NSCodingプロトコルを削除) //ドキュメントID var todoId: String? // ToDoのタイトル var todoTitle: String? // ToDoを完了したかどうかを表すフラグ var todoDone: Bool = false //取得したデータを当てはめていくための型(MyTodoのデータ型) init(document: QueryDocumentSnapshot) { self.todoId = document.documentID self.todoTitle = document.data()["todoTitle"] as? String self.todoDone = document.data()["todoDone"] as! Bool } override init() { } }
あなたの回答
tips
プレビュー