Home > Enterprise >  Issue trying to return a Codable Type.self through a method of a Swift enum
Issue trying to return a Codable Type.self through a method of a Swift enum

Time:08-11

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.

  • Related