implementing a getter for Firebase Firestore in an iOS app, trying to make the getter as generic and re-usable as possible. It wants a model to use for mapping data, I created an enum through which I can get the raw value of directory and the type through a method but I get multiple errors.
1- for the method, the types show "Type cannot conform to Decodable/Encodable"
2- where I call the method (type.model() close to the bottom of the getter) "Type Any cannot conform to Decodable"
Type example:
import FirebaseFirestoreSwift
import Foundation
struct Type1:Identifiable, Codable
{
@DocumentID var id:String?
var name:String?
}
Enum:
enum RetrievalType:String, Codable
{
case Directory1 = "Collection1"
case Directory2 = "/Collection2"
case Directory3 = "/Collection3"
case Directory4 = "/Collection4"
case Directory5 = "Collection5"
func model() -> some Codable
{
switch self
{
case .Directory1:
return Type1.self
case .Directory2:
return Type2.self
case .Directory2:
return Type3.self
case .Directory4:
return Type4.self
case .Directory5:
return Type5.self
}
}
}
Getter:
func get(type:RetrievalType){
var qSnapshot:QuerySnapshot?
if type == RetrievalType. Directory5
{
store.collection(RetrievalType.Directory1.rawValue).whereField(RetrievalType. Directory5.rawValue, isEqualTo: userId).addSnapshotListener
{
qSnapshot, error in
if let error = error
{
print("Error - \(error.localizedDescription).")
return
}
}
}else
{
store.collection(type.rawValue).addSnapshotListener
{
querySnapshot, error in
if let error = error
{
print("Error - \(error.localizedDescription).")
return
}
}
}
self.documentsForDirectory = qSnapshot?.documents.compactMap{
document in
try? document.data(as: type.model())
} ?? []
}
CodePudding user response:
Using an enum
is not a great solution for this kind of problem, because you have a tight coupling between your "getter" and the potential types. When you find yourself passing a type as a parameter to a function, that is a good indicator that the solution may be to use generics.
If you define your getter as a generic, then you don't need an enum and there is no coupling between the getter and the types it can get.
A minimal example of a generic Firestore "manager"
class FirestoreManager<T:Codable> {
private let database: Firestore
private let collection: CollectionReference
init(database: Firestore, collectionPath: String) {
self.database = database
self.collection = self.database.collection(collectionPath)
}
func query(queryField: String, queryValue: String) async throws ->[T] where T: Identifiable {
let querySnapshot = try await self.collection.whereField(queryField, isEqualTo: queryValue).getDocuments()
let documents:[T] = querySnapshot.documents.compactMap { document in
do {
let document = try document.data(as: T.self)
return document
} catch {
print("Error decoding document \(error)")
return nil
}
}
return documents
}
}
Note that your original code didn't handle the asynchronous nature of Firebase fetches. I have used async/await in my code to handle this.
You could call this function using something like:
let manager: FirestoreManager<Type1> = FirestoreManager(database: Firestore.firestore(app: app), collectionPath:"collection/document/collection")
let docs = try await manager.query(queryField: "userName", queryValue: userName)
print(docs)
If you wanted to encapsulate the configuration to get a little closer to your original design, you could use a protocol with an associated type:
protocol FirestoreDataType {
associatedtype ObjectType: Codable
var collectionPath: String { get }
var queryField: String { get }
}
struct User: Codable, Identifiable {
@DocumentID var docId: String?
var id: String?
let firstName: String
let secondName: String
let userName: String
}
struct UserType: FirestoreDataType {
typealias ObjectType = User
let collectionPath: String
let queryField: String
init(collectionPath: String) {
self.collectionPath = collectionPath
self.queryField = "userName"
}
}
class FirestoreManager<T:FirestoreDataType> {
private let database: Firestore
private let collection: CollectionReference
private let firestoreDataType: T
init(database: Firestore, firestoreDataType: T) {
self.database = database
self.firestoreDataType = firestoreDataType
self.collection = self.database.collection(self.firestoreDataType.collectionPath)
}
func query(queryValue: String) async throws ->[T.ObjectType] where T.ObjectType: Identifiable {
let querySnapshot = try await self.collection.whereField(self.firestoreDataType.queryField, isEqualTo: queryValue).getDocuments()
let documents:[T.ObjectType] = querySnapshot.documents.compactMap { document in
do {
let document = try document.data(as: T.ObjectType.self)
return document
} catch {
print("Error decoding document \(error)")
return nil
}
}
return documents
}
}
And you would use it like this:
let userType = UserType(collectionPath: "collection/document/collection")
let manager = FirestoreManager(database: Firestore.firestore(app: app), firestoreDataType: userType)
let docs = try await manager.query(queryValue: userName)
print(docs)
You would define additional structs, similar to UserType
for your other object types.