✴️

Cannot pass function of type '() async -> Void' to parameter expecting

2024/02/11に公開

💡Tips

SwiftUIのButtonの中には、非同期処理をそのまま書けないらしい?

Firestoreにデータをaddする処理をそのまま書くとエラーが出る???
他の言語だったらasync/awaitを書くけどSwiftUIはどうすれば良いのか?

Task.iniというクロージャーを追加すればいいらしい?

// これで囲む
Task.ini {
// 処理を書く
}

https://developer.apple.com/documentation/swift/task/init(priority:operation:)-5k89c

init(priority:operation:)
Runs the given throwing operation asynchronously as part of a new top-level task on behalf of the current actor.

init(優先度:操作:) 現在のアクターに代わって、新しいトップレベル タスクの一部として、指定されたスロー操作を非同期で実行します。

デモアプリを作る

デモアプリの作り方は、FirebaseのiOS SDKを追加して、addするメソッドを追加するだけ。Storyboardよりは簡単ですね。

アプリのエントリーポイントのファイルに、FirebaseCoreの設定をする。

import SwiftUI
import FirebaseCore


class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    FirebaseApp.configure()

    return true
  }
}


@main
struct FirestoreCookBookAppApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

デフォルのアプリのUIを作るコードに、Firestoreにaddする処理を追加する。

import SwiftUI
import FirebaseFirestore

struct ContentView: View {
    @State var textValue: String = ""
    let db = Firestore.firestore()
    
    var body: some View {
        VStack {
            TextField("本を追加", text: $textValue)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button(action: {
                Task.init {
                    // Add a new document with a generated ID
                    do {
                      let ref = try await db.collection("books").addDocument(data: [
                        "title": textValue,
                        "createdAt": Timestamp(date: Date())
                      ])
                      print("Document added with ID: \(ref.documentID)")
                    } catch {
                      print("Error adding document: \(error)")
                    }
                }
            }) {
                Text("本を追加")
                    .bold()
                    .padding()
                    .frame(width: 100, height: 50)
                    .foregroundColor(Color.white)
                    .background(Color.orange)
                    .cornerRadius(25)
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Viewとロジックが一緒だとなんだかかっこよくない💦
ロジックをメソッドにまとめて、クラスとして別の場所に配置して、使うときはインスタンス化して呼び出すようにしてみた。

import SwiftUI
import FirebaseFirestore

// ロジックをViewから切り分けてクラスの中にメソッドとして定義
class BookRepository {
    let db = Firestore.firestore()
    
    // Firestoreにデータを追加するメソッド
    func addBook(textValue: String) async {
        // Add a new document with a generated ID
        do {
            let ref = try await db.collection("books").addDocument(data: [
                "title": textValue,
                "createdAt": Timestamp(date: Date())
            ])
            print("Document added with ID: \(ref.documentID)")
        } catch {
            print("Error adding document: \(error)")
        }
    }
}

struct ContentView: View {
    @State var textValue: String = ""
    // リポジトリクラスをインスタンス化する
    let bookRepository = BookRepository()
    
    var body: some View {
        VStack {
            TextField("本を追加", text: $textValue)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            Button(action: {
                Task.init {
                    // リポジトリクラスのメソッドを呼び出す
                    await bookRepository.addBook(textValue: textValue)
                }
            }) {
                Text("本を追加")
                    .bold()
                    .padding()
                    .frame(width: 100, height: 50)
                    .foregroundColor(Color.white)
                    .background(Color.orange)
                    .cornerRadius(25)
            }
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

データも保存できているようなので問題なし。

まとめ

今回は、FirestoreをSwiftUIで使う練習をしたかったので、ドキュメントのコードをそのまま使うのをやってみようとしましたが、なかなかうまくいきませんでした💦
非同期処理の書き方のパターンをわかっていないと、使えないな〜悩まされながらコードを書いて試してみました。

参考になった記事:
https://blog.code-candy.com/swift_closure_basic/

Discussion