Reputation: 468
I've looked everywhere, and I can't find anything that works for the problem I am having.
With Cloud Firestore, I want to get all documents in a collection, which is easy enough like so:
let ref = db.collection("users")
ref.getDocuments { (collection, error) in
guard let documents = collection?.documents else {
return
}
// I have an array of documents here
}
What I want to do is decode this array of documents using Codable Firebase, which works great when getting a single document:
let user = try? FirestoreDecoder().decode(User.self, from: document.data())
But, I cannot do:
let users = try? FirestoreDecoder().decode([User].self, from: documents.map { $0.data() })
for example from my array of documents from the collection as the data
arguments is [String: Any]
I could decode each document one by one in a for loop, but I am using generics, so if I pass in [User]
as my generic expected response, I cannot then in my for loop see what the generic element is and convert to User.self
rather than [User].self
, as far as I know.
Any tips would be greatly appreciated.
Upvotes: 1
Views: 1937
Reputation: 350
So I have a custom class that implements the decodable protocol, let's call it job
.
I also have a class that inherits from JSONDecoder, if you are using dates in your object then this part is very important as the decoder needs a strategy for decoding to Date
as the server will just return them as strings.
class MyDecoder: JSONDecoder {
// MARK: - Initializers
override init() {
super.init()
self.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
return self.stringToDate(string: string)
})
}
// MARK: - Utilities
private func stringToDate(string: String) -> Date {
let formatter = DateFormatter()
formatter.calendar = Calendar.autoupdatingCurrent
formatter.locale = Locale.autoupdatingCurrent
formatter.timeZone = TimeZone.autoupdatingCurrent
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.date(from: string) ?? Date()
}
}
Then I just simply serialize the documents returned into a data object and decode using our custom decoder.
do {
let documents = snapshot?.documents.map({ $0.data() }) ?? [[:]]
let data = try JSONSerialization.data(withJSONObject: documents, options: .fragmentsAllowed)
let jobs = try MyDecoder().decode(Array<Job>.self, from: data)
return jobs
} catch let error {
throw error
}
Upvotes: 0
Reputation: 843
You can try this solution, it works for me. Let's create an extension like snapshotExtensions.swift with the content as below:
import Foundation
import FirebaseFirestore
extension QueryDocumentSnapshot {
func decoded<Type: Decodable>() throws -> Type {
let jsonData = try JSONSerialization.data(withJSONObject: data(), options: [])
let object = try JSONDecoder().decode(Type.self, from: jsonData)
return object
}
}
extension QuerySnapshot {
func decoded<Type: Decodable>() throws -> [Type] {
let objects: [Type] = try documents.map({try $0.decoded() })
return objects
}
}
in your ViewController, you can use:
Firestore.firestore().collection("your_collection").getDocuments { (snapshot, error) in
self.myModelArray = try! snapshot!.decoded()
}
it is a late reply but I hope it helps others :)
Upvotes: 1