Des pistes pour contourner le fameux: Protocol can only be used as a generic constraint...

Bonjour à vous  !

Ça fait maintenant 2 mois que je m'arrache un peu la tête sans parvenir à trouver une solution à mon problème. Le voici succinctement présenté:
Je dispose d'une base de données de plantes gérée via Core Data. Les plantes ont une cinquantaine de caractéristiques et je souhaite mettre en place un système de filtres dynamiques selon ces attributs. Du genre: "filtrer les plantes caduques, aux fleurs jaunes, et qui poussent en terre acide."
Les attributs sont donc de différents types, par ex:
nom: String = "Lys"
hauteur: Double = 2.5
densiteParM2: Int = 4
toxique: Bool = false
arrosage: Arrosage = .faible
fleurs: [Fleur] = [.rouge, .rose, .blanc]

La majorité des attributs sont des enum (comme Arrosage et Fleur), stockés sous forme de texte dans Core Data et récupérés comme enum via un accesseur. Ils se conforment à un protocol Optionable.
Un critère de filtre ne correspond pas toujours exactement à un attribut (par exemple à l'attribut hauteur correspond un critère avec 2 valeurs: un minimum et un maximum: cela n'a pas de sens de filtrer des hauteurs précises). J'ai donc créé des structs pour chaque type de critère: critère de texte, de bool, d'Optionable... se conformant à un protocol Filterable

protocol Filterable { 
    associatedtype: T
    var value: T { get set }
    var label: String { get }
    var predicate: NSPredicate { get }
}
    struct BoolFiltre: Filterable { … }
    struct StringFiltre: Filterable { … }
    struct OptionableFiltre<Optionable>: Filterable { … }

Mon problème vient du fait que
-d'une part, je dois gérer des critères de filtre ensemble (dans un array ?)
-d'autre part, je suis obliger d'avoir un associatedtype au protocol, puisque les types de valeurs change.

Une configuration comme ci-dessous me donne donc une erreur:

let toxique = BoolFiltre(value: true, …)
let name = StringFiltre(value: "lys", …)
let arrosage = OptionableFiltre<Arrosage>(value: .faible)
let filtres = [toxique, name, arrosage] //<— Error: Protocol 'Filterable' can only be used as a generic constraint because it has Self or associated type requirements

Comment puis-je gérer dynamiquement ces filtres (les afficher dans une liste via un ForEach, les modifier, et les utiliser pour faire mes requêtes) ?
Je suspecte que mon approche du protocole n'est pas la bonne mais je n'ai pas d'expérience en POP, et franchement, je tatonne...
Tous vos conseils et éclairages sont les bienvenus, parce que je sèche complètement !

PS: J'ai essayé de faire succinct dans la description mais j'ai exploré beaucoup de pistes: si vous voulez que j'ajoute du code, dites-le moi !

Mots clés:

Réponses

  • PyrohPyroh Membre
    janvier 2022 modifié #2

    Je vais partir du principe que tu utilise la propriété predicate de Filterable pour la filer à CoreData.

    NSPredicate n'ayant pas de valeur associée on va se baser la dessus pour effacer le vrai type de filtre on appelle ça le type erasure (je te laisse chercher des infos complémentaires).
    Voici une technique que j'utilise, elle consiste à utiliser une closure pour te débarrasser du vrai type et n'en sortir que le predicate:

    protocol Filterable {
        associatedtype T
        var value: T { get set }
        var label: String { get }
        var predicate: NSPredicate { get }
    }
    
    extension Filterable {
        func eraseToAnyFilterable() -> AnyFilterable {
            .init(erasing: self)
        }
    }
    
    struct AnyFilterable {
        private let predicateProvider: () -> NSPredicate
    
        var predicate: NSPredicate { predicateProvider() }
    
        init<F: Filterable>(erasing filter: F) {
            predicateProvider = { filter.predicate }
        }
    }
    

    Ensuite tu faire un [AnyFilterable] avec:

    let toxique = BoolFiltre(value: true, …)
    let name = StringFiltre(value: "lys", …)
    let arrosage = OptionableFiltre<Arrosage>(value: .faible)
    let filtres = [toxique.eraseToAnyFilterable(), name.eraseToAnyFilterable(), arrosage.eraseToAnyFilterable()] 
    

    Et voilà.

  • JoshuaTreeJoshuaTree Membre
    janvier 2022 modifié #3

    Bon, je sais qu'il faut rester poli sur les forums, tout ça tout ça... mais PUT***  ! Pyroh, je te remercie ! ! !
    Franchement, c'est exactement la solution à mon problème, je ne savais juste pas comment le caractériser: en effet, il s'agit de pouvoir effacer le type...
    Du coup, j'ai commencé à regarder le concept de type erasure. J'ai notamment trouvé une vidéo sur youTube qui explique exactement ce que tu décris. Pour ceux que ça intéresse:
    https://youtube.com/watch?v=yWLRz1SVIUg

    Encore un grand merci pour ce coup de pouce, Pyroh !
    Bon week-end à tous !

    Jo

  • Mais de rien 😉
    Bonne chance pour le reste.

Connectez-vous ou Inscrivez-vous pour répondre.