I have the following reproduced example in Swift/SwiftUI. I'm simply trying to pull 2 "ratings" from firebase, and then display them once they are loaded. I have 2 possible methods of loading the ratings below.
Method 1: Wait for the data to load, and display it once it's loaded. However, my issue is that this code does not load the data, and my list of ratings remains empty. The database throws no errors, though.
Method 2: Instead of waiting for the data to load, a timer is used to simply wait 2 seconds and hope that the data is loaded by then before displaying it.
Method 1 is what I'm trying to make work. Method 2 works, however, so I know that it isn't an issue with the database or my data, but most likely is an issue with my async/await usage.
Below is my reproduced code. It looks like a lot but it's nothing crazy, sorry!
struct test: View {
@State var ratings: [Double] = []
@State var ratingsAreLoading = true
func getRatings() async {
let db = Firestore.firestore() //database object
await db.collection("users").document("user 1).getDocument { (doc, error) in
if let doc = doc { //just gets some data
ratings.append(doc["rating1"] as? Double ?? 1.0)
ratings.append(doc["rating2"] as? Double ?? 1.0)
}
}
}
var body: some View {
VStack {
if ratingsAreLoading {
Text("LOADING...")
} else {
Text(String(ratings[0])) //displays a rating
}
}
//IMPLEMENTATION HERE
}
}
Method 1: Doesn't seem to populate "ratings" list
.task {
await getRatings() //gets ratings
await MainActor.run {
ratingsAreLoading = false //displays the ratings
}
}
Method 2: Works
.onAppear {
getRatings() //gets ratings
DispatchQueue.main.asyncAfter(deadline: .now() 2) {
ratingsAreLoading = false //displays ratings after 2 seconds
}
}
What am I doing wrong with the async/await method? How can I get it to load my data in time or at all?
CodePudding user response:
You have to convert the completion handler to a Continuation that is compatible with async await
It would look something like this
///Retrieves a single document with the provided id at the collection's path
public func retrieve<FC : FirestoreCodable>(path: String, id: String) async throws -> FC{
typealias MyContinuation = CheckedContinuation<FC, Error>
return try await withCheckedThrowingContinuation { (continuation: MyContinuation) in
let docRef = store.collection(path).document(id)
docRef.getDocument(as: FC.self){
result in
switch result{
case .success(let object):
continuation.resume(returning: object)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
To complement the code above I put a property at class
/struct
level
private let store : Firestore
That way you can add any setup code in the init
such as isPersistenceEnabled
FirestoreCodable
is just a protocol
to help with generics
public protocol FirestoreCodable: Decodable, Encodable{
/// Wrap with `@DocumentID` from `import FirebaseFirestoreSwift`
var id: String? { get set }
}
Your struct
would look something like
struct User: FirestoreCodable{
@DocumentID var id: String?
var rating1: Double
var rating2: Double
}
You would use it something like
let user: User = try await retrieve(path: "users", id: "user 1")
You can find more about async
/await
I the WWDC21 video
https://developer.apple.com/wwdc21/10132