KVO et CoreData : cas où le setter d'un attribut CoreData est implémenté

colas_colas_ Membre
janvier 2014 modifié dans Objective-C, Swift, C, C++ #1

Bonjour à  tous !


 


Il me semble que KVO et CoreData ne fonctionnent pas ensemble !!!! ce qui me semblerait hallucinant !!! Je voulais avoir votre avis là -dessus.


 


Je suis tombé sur ce problème comme ça :


- j'ai un entité qui a deux attributs booléens : isA et isB


- j'ai réécrit les setters de isA et de isB comme suit :



setIsA:(NSNumber *)number
{
if ([number boolValue])
{
[self setIsB:[NSNumber numberWithBool:NO] ;
}

[self setPrimitiveIsA:number] ;
}


setIsB:(NSNumber *)number
{
if ([number boolValue])
{
[self setIsA:[NSNumber numberWithBool:NO] ;
}

[self setPrimitiveIsB:number] ;
}

Comme ça, isA et isB ne peuvent pas être à  YES simultanément.


 


- ensuite, dans mes propriétés sont bindés sur des checks box


- quand je coche les check-box, les méthodes setIsA et setIsB sont bien appelées, mais je n'ai pas l'effet attendu, à  savoir que cocher A décoche B automatiquement.


- en revanche, si j'ajoute dans mon code des willChangeValue et des didChangeValue, ça marche nickel.


 


 


Je précise que j'utilise mogenerator (j'espère que ce n'est pas lui qui fait merder le KVO, car mogenerator est super)


 


 


Avez-vous déjà  été confronté à  cela ?


Confirmez-vous ?


 


J'ai trouvé sur le site d'Apple quelque chose qui laisserait entendre cela...


https://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdAccessorMethods.html


«1

Réponses

  • CéroceCéroce Membre, Modérateur

    Il faut appeler willChangeValueForKey: et didChangeValueForKey: pour que le KVO sache qu'il s'est passé quelque chose et puisse déclencher la notification.


    Il se trouve que les accesseurs @synthetisés le font.


     


    Ce que tu constates est que les méthodes setPrimitive... ne le font pas, mais ce n'est guère étonnant, il me semble même que c'est à  ça qu'elles servent ;-)


  • colas_colas_ Membre
    juin 2013 modifié #3

    Oui !


    Comme je réécris les méthodes setIsA et setIsB, elles ne sont plus auto-synthétisées et donc elles n'appellent plus willChange, didChange.


     


    It makes sense !


     


    Je pensais que lorsque si toto était une property, alors quand on appelait la méthode setToto, le KVO fonctionnait (quelque soit la méthode setToto).


     


    Merci pour ta réponse Céroce ;)


  • AliGatorAliGator Membre, Modérateur

    C'est bizarre il me semblait que normalement il n'y avait plus à  appeler manuellement/explicitement les "willChangeValueForKey:" / "didChangeValueForKey:" et que c'était fait tout seul ? Et que si on veut repasser à  de la notification manuelle des changements et appeler nous-même willChange/didChange, il fallait surcharger "+automaticallyNotifiesObserversForKey:" ?


     


    https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE


  • CéroceCéroce Membre, Modérateur

    Effectivement, j'en étais resté aux vieilles façon de faire. La doc d'Apple donne peu de détails, il faudrait tester pour être sûrs.


  • Bonsoir,


     


    Y a-t-il moyen de "lier" les deux propriétés à  l'aide de


     


     


    + (NSSet *)keyPathsForValuesAffectingIsA {return [NSSet setWithObjects:@isB, nil];}

    + (NSSet *)keyPathsForValuesAffectingIsB {return [NSSet setWithObjects:@isA, nil];}

     

    ?

  • Je suis sans doute à  côté de la plaque :



    NSNumber * isB () {
    return ([self.isA boolValue] ? [NSNumber numberWithBool:FALSE] : [NSNumber numberWithBool:TRUE]);
    }

    Et définir isB comme transiant.


  • @jpimbert : ta solution ne convient pas car on peut avoir isB=isA=false, ce que ta solution empêche.


     


    @berfis : ça a l'air super intéressant ! Je vais essayer (pour un autre problème en fait). Es-tu sûr que tu voulais pas faire référence à  



    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    // ajouter les attributs voulus ici dans keypaths

    return keyPaths;
    }

    ?


  • AliGatorAliGator Membre, Modérateur

    On peut faire les deux.


  • KaroxysKaroxys Membre
    juin 2013 modifié #10

    Bonjour,


     


    J'avais comme information que le willChangeValueForKey et didChanfeValueForKey n'étaient pas obligatoire si on effectuait un appel avec "self.maVariable" dans le cas contraire il devait être obligatoire par exemple si on utilise "_maVariable".


     


    Es-ce complètement faux ?


     


    Merci pour votre retour.


     


    K.


  • CéroceCéroce Membre, Modérateur


    J'avais comme information que le willChangeValueForKey et didChanfeValueForKey n'étaient pas obligatoire si on effectuait un appel avec "self.maVariable" dans le cas contraire il devait être obligatoire par exemple si on utilise "_maVariable".




     


     


    Réponse:


     




    Il faut appeler willChangeValueForKey: et didChangeValueForKey: pour que le KVO sache qu'il s'est passé quelque chose et puisse déclencher la notification.


    Il se trouve que les accesseurs @synthetisés le font.



  • Est-ce que vous avez une solution du même type que keyPathsForValuesAffectingValueForKey: pour la situation suivante : 


     


    Je souhaite que si la propriété toto d'un objet A change alors la propriété tata d'un objet B soit mise au courant ?


     


    Merci !


  • berfisberfis Membre
    juin 2013 modifié #13


     


    @berfis : ça a l'air super intéressant ! Je vais essayer (pour un autre problème en fait). Es-tu sûr que tu voulais pas faire référence à  




     


    J'ai suivi la convention pour nommer les méthodes (c'est de la même veine que "setParam" pour la propriété param).


    Voir listing 2 de ce lien:


    https://developer.apple.com/library/mac/#documentation/cocoa/conceptual/KeyValueObserving/Articles/KVODependentKeys.html


  • AliGatorAliGator Membre, Modérateur


    Réponse:




    Ce n'est pas en accord avec ce que dit la doc Apple que j'ai cité, si ?


     


    Moi j'ai vu énormément de code de personnes qui écrivaient leurs propres setters mais ne mettaient plus le willChange/didChange, y compris du code de certaines pointures en Cocoa ou auteurs de frameworks célèbres... et il me semble qu'il m'avait été répondu à  l'époque qd j'avais demandé que ce n'est plus nécessaire...


    D'après la doc du moment qu'on respecte les conventions de nommage, ça le fait tout seul. Ce que je comprend à  lire la doc c'est que ça le fait tout seul qu'on implémente nous-même la méthode "setName:" ou qu'on la @synthesize, mais en effet c'est quand même à  confirmer par tests pour être sûr...

  • CéroceCéroce Membre, Modérateur
    juin 2013 modifié #15

    Petit essai pour en être sûr:


     


  • colas_colas_ Membre
    juin 2013 modifié #16

    Pour être sûr aussi, j'ai créé une petite appli qui reproduit le comportement non-voulu.


     


    Dans cet exemple (avec CoreData et mogenerator), le KVO ne marche pas.


    Si vous avez des idées, je suis preneur !


     


     


    PS : J'ai vérifié, la même appli sans CoraDate+mogenerator marche bien.


    (cf Test2)


  • AliGatorAliGator Membre, Modérateur

    J'ai la flemme de tester, que retourne [NSManagedObject automaticallyNotifiesObserversForKey:] ?


    Peut-être qu'Apple l'a surchargé pour les NSManagedObject pour limiter les notifications KVO et éviter qu'il y en ait trop de partout (car CoreData doit faire un usage assez conséquent au appels des setters de plein de propriétés dans tous les sens, c'était p'tet un peu violent ?) et a préféré le gérer lui-même pour les cas qui l'intéressent ?


  • jpimbertjpimbert Membre
    juin 2013 modifié #18


    @jpimbert : ta solution ne convient pas car on peut avoir isB=isA=false, ce que ta solution empêche.


     




     


    Bon ! Vous allez me trouver rabat-joie mais y'a pas besoin de KVO dans ce cas.


     


    Exemple.


    J'ai un truc à  me dire là  de suite.


    J'ai deux solutions :


    1/ Je m'écris ce que j'ai à  me dire dans un courriel. Je m'envois le courriel. Je relève mes messages et tiens ! J'en ai reçu un, il est de moi-même. Je le lis. Du coup je suis au courant ; j'ai eu l'info que j'avais à  me dire (utilisation du KVO)


    2/ Je me dis directement ce que j'ai à  me dire (pas de KVO)


     


    Je vais le dire en plus technique : le pattern Observateur est fait pour améliorer le découplage entre classes. L'observateur doit connaà®tre la classe observée mais cette dernière ignore totalement la classe observatrice. Dans le cas présent l'observateur et l'observée sont la même classe ; cela n'a pas de sens d'essayer de découpler une classe d'elle-même. Cela complique inutilement le code.


     


    Dans le cas présent les couples de valeurs autorisées pour ( A, B ) sont (Faux, Faux), (Vrai, Faux) et (Faux, Vrai). Le couple ( A, B ) a trois états possibles. Donc j'utiliserais un attribut state dont la valeur peut être 0, 1 ou 2 pour coder ces trois états (on peut utiliser un enum c'est plus propre).


     


    isA et isB peuvent alors être des attributs transient avec les méthodes suivantes :




    - (void) setIsA: (BOOL) newVal {
    self.state = (newVal ? 1 : 0);
    }

    - (BOOL) isA {
    return (self.state == 1);
    }

    - (void) setIsB: (BOOL) newVal {
    self.state = (newVal ? 2 : 0);
    }

    - (BOOL) isA {
    return (self.state == 2);
    }


    PS : j'espère que je ne vais convaincre personne car les discussions sur ce sujet sont très intéressantes. Ce serait dommage qu'elles s'arrêtent.


  • colas_colas_ Membre
    juin 2013 modifié #19

    @jpimbert


    En fait, j'ai besoin du KVO car mes propriétés isA et isB ont un impact sur l'affichae (des check boxs) et donc je veux que les check boxs changent automatiquement quand les propriétés changent.


     


    @Ali


    Je regarderai ça demain, trop crevé là  :(


    ça m'étonnerait que CoreData désactive le KVO.


    Peut-être à  cause de mogenerator...


    À tester !


  • AliGatorAliGator Membre, Modérateur
    Autre test a faire (éventuellement dans un projet test à  part) :

    - Créer un XCDataModel CoreData

    - Générer les classes avec Xcode, ajoute ton code pour setIsA: et setIsB: et tester le KVO (si ça marche pas c'est peut être parce que c'est du CoreData et pas des NSObject ?!)

    - Supprimer les classes générées avec Xcode, les régénérer avec mogenerator, refaire le même test (si ça marchait en 1 mais pas là  c'est que c'est mogenerator. Mais j'y crois pas trop)

    - Changer, dans les classes générées, la classe parente dans le .h de NSManagedObject en NSObject et du coup utiliser ces classes modèle comme des objets modèle standards et pas des entités CoreData, refaire le test (si ça se met à  marcher c'est que ça marche pour NSObject mais pas NSManagedObject...)


    Aussi, est-ce que tu as fais un save sur ton MOC lors de tes tests ? Peut-être que CoreData met de côté les notifications KVO et ne les émet que quand le MOC est sauvé, ce qui pourrait avoir du sens...


    (P.S. : attention aux dépendances cycliques, si changer A va emettre une notif qui va demander dechanger B, ce qui va émettre une notif qui va changer A...)
  • colas_colas_ Membre
    juin 2013 modifié #21

    Salut Ali,


     


    j'ai fait les tests et il semble que les NSManagedObject ne gèrent pas le KVO ! ce qui me semble hallucinant.


    Mais, oui c'est vrai !


     


    Si je save mon doc, ça ne change rien.


     


    Le code est là .


  • colas_colas_ Membre
    juin 2013 modifié #22
  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #23
    Ok donc tout s'explique !

    Le point important que j'avais subodoré et qu'on avait manqué dans la doc, c'est que pour un NSManagedObject, "automaticallyNotifiesObserversForKey:" retourne :
    • NO pour les propriétés du modèle, parce que à  priori les accesseurs synthétisés par iOS (@dynamic) se chargent eux-mêmes d'appeler willChange/didChange pour optimiser les notifications KVO générées par CoreData.
    • YES pour les propriétés non modelisées que tu aurait rajoutées en dehors de CoreData, ça c'est comme pour n'importe quel autre NSObject
    Ca explique donc pourquoi si tu réimplémentes toi-même les setters de propriétés présentes dans ton xcdatamodel (comme "setIsA:" / "setIsB:" dans ton exemple), il faut que tu appelles explicitement willChange/didChange (ou alors, si tu veux éviter, il faut que tu surcharges "automaticallyNotifiesObserversForKey:" pour retourner YES pour ces propriétés)


  • Le point important que j'avais subodoré et qu'on avait manqué dans la doc, c'est que pour un NSManagedObject, "automaticallyNotifiesObserversForKey:" retourne :


    • NO pour les propriétés du modèle, parce que à  priori les accesseurs synthétisés par iOS (@dynamic) se chargent eux-mêmes d'appeler willChange/didChange pour optimiser les notifications KVO générées par CoreData.
    • YES pour les propriétés non modelisées que tu aurait rajoutées en dehors de CoreData, ça c'est comme pour n'importe quel autre NSObject

    Ca explique donc pourquoi si tu réimplémentes toi-même les setters de propriétés présentes dans ton xcdatamodel (comme "setIsA:" / "setIsB:" dans ton exemple), il faut que tu appelles explicitement willChange/didChange (ou alors, si tu veux éviter, il faut que tu surcharges "automaticallyNotifiesObserversForKey:" pour retourner YES pour ces propriétés)

     




     


    Donc ma solution du post #18 n'est pas si conne que ça. Il faut simplement ne pas définir les attributs isA et isB dans le modèle.

  • Une petite précision :


     


    Si on veut écrire soi-même les setters d'un attribut myAttribute Core Data et qu'on veut des notifications KVO, alors comme dit ci-dessus,  il faut surcharger "automaticallyNotifiesObserversForKey:" pour retourner YES pour cet attribut.


     


    Mais, il semble alors que la notification n'est pas donnée si l'on appelle setPrimitiveMyAttribute !


     


    Je voulais signaler cette constatation qui pourra servir à  d'autres !


     


    Solution que j'ai choisie (assez moche je trouve) :



    - (void)_setPrimitiveMyAttribute:(NSNumber *)value
    {
        [self willChangeValueForKey:@myAttribute] ;
        [self setPrimitiveMyAttribute:value] ;
        [self didChangeValueForKey:@myAttribute] ;
    }

    Si vous avez d'autres idées !


     


    PS : je suis obligé d'écrire moi-même le setter car en changeant attr1, je change aussi attr2, attr3, etc. Pour éviter les boucles infinies, je passe par un "primitiveSetter" et en l'occurrence, j'utilise celui fourni par CoreData. Mais, le problème est qu'il ne gère pas les KVO.


  • AliGatorAliGator Membre, Modérateur
    Oulà , c'est une mauvaise solution à  mon avis, car justement c'est précisément le but exact de "setPrivitiveXXX:" / "setPrimitiveValue:forKey:" que d'affecter une valeur à  un attribut sans déclencher le KVO.

    Du coup si tu surcharges "setPrimitiveXXX:" pour déclencher le KVO avec willChange:/didChange: cela enlève tout l'intérêt de cette méthode. Alors que tu as "setXXX:" qui fait la même chose et déclenche déjà  le KVO.

    Si tu as des attributs avec des dépendances, il y a tout ce qu'il faut pour gérer ce genre de cas tout en évitant les boucles infinies et tout, c'est déjà  prévu : KeyValueObserviing : Registering Dependent Keys.

    Donc je te déconseille fortement de surcharger "setPrimitiveXXX:" encore moins pour y rajouter du KVO, à  la place utilise "keyPathsForValuesAffectingValueForKey:" / "keyPathsForValuesAffectingXXX" pour cela, d'ailleurs normalement du coup si tu le fais comme ça, tu n'as même plus à  surcharger tes setters du tout puisque les dépendances de clés sont gérées par ce mécanisme et plus par tes setters custom.
  • Message #6.


  • Je ne suis pas sûr de comprendre la logique de ta réponse @Ali et si c'est ce que je pense, je ne trouve pas ça hyper mieux. Est-ce que tu penses à  ca ?



    - (void)setAttrA:(NSNumber *)value
    {
        if ([self.attrB isEqual:@1])
        {
            [self.setPrimitiveAttrA:@2] ;
        }
        else
        {
            [self.setPrimitiveAttrA:value] ;
        }
    }


    + (NSSet *)keyPathAffectingAttrA
    {
        return [NSSet setWithObject:@AttrB] ;
    }
  • AliGatorAliGator Membre, Modérateur
    janvier 2014 modifié #29
    Ta logique me parait très bizarre (ça veut dire que si tu affectes B à  1 puis ensuite que tu affectes A à  n'importe quoi, ça va forcer à  affecter A à  2... mais si tu affectes A d'abord et affecte ensuite B à  1, ça ne fera pas cela...) et je ne comprend pas trop l'intérêt de la demande ici.

    Le cas d'usage c'est plutôt un truc du genre si tu as une méthode qui retourne le nom complet d'une personne, tu indiques que cet attribut dépend du nom de famille et du prénom, et comme ça quand tu changes le prénom tu auras un KVO à  la fois pour la clé "firstname" et pour la clé "fullname" qui dépend de "firstname", permettant à  ton interface de se mettre à  jour que tu aies bindé sur l'un ou sur l'autre ou sur les 2.

    Après pour reprendre ton cas bizarre, si tu veux vraiment faire un A qui retourne 2 si B vaut 1, je verrai plutôt l'approche suivante :
    - (NSNumber *)attrA
    {
    return [_attrB isEqual:@1] ? @2 : _attrA;
    }


    + (NSSet *)keyPathAffectingAttrA
    {
    return [NSSet setWithObject:@AttrB] ;
    }
  • Imagine que mes attributs sont : 


     


    isRed


    isBlue


    isOpaque


    isBW


     


    et tu ne peux pas être simultanément isRed, isBlue et isBW


  • berfisberfis Membre
    janvier 2014 modifié #31

    Des options mutuellement exclusives, selon les HIG, cela s'appelle des boutons-radio.


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