nickjf89
nickjf89

Reputation: 468

Decode array of collections from Firebase Cloud Firestore with codable

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

Answers (2)

James Wolfe
James Wolfe

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

Robert TuanVu
Robert TuanVu

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

Related Questions