KVO et CoreData : cas où le setter d'un attribut CoreData est implémenté
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...
Réponses
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 ;-)
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
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
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
Je suis sans doute à côté de la plaque :
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 à
?
On peut faire les deux.
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.
Réponse:
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 !
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
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...
Petit essai pour en être sûr:
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)
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 ?
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 :
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.
@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 !
- 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...)
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à .
La réponse à presque tout sur SO :
http://stackoverflow.com/questions/3728247/nsmanagedobject-and-kvo-vs-documentation
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) :
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.
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 ?
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 :
Imagine que mes attributs sont :
isRed
isBlue
isOpaque
isBW
et tu ne peux pas être simultanément isRed, isBlue et isBW
Des options mutuellement exclusives, selon les HIG, cela s'appelle des boutons-radio.