[Swift] NSUndoManager modifie les Int
Je me trouve face à un problème de taille. Pour gérer le undoManager avec Swift j'ai codé une petite extension à NSObject :
func registerValueToUndoManager(value: AnyObject?, forKey key: String) {
if let undoManager = NSApplication.sharedApplication().keyWindow?.undoManager {
undoManager.prepareWithInvocationTarget(self).setValue(value, forKey: key)
}
}
func registerUndoForSelfWithSelector(sel: Selector, object: AnyObject?) {
if let undoManager = NSApplication.sharedApplication().keyWindow?.undoManager, let object: AnyObject = object {
undoManager.registerUndoWithTarget(self, selector: sel, object: object)
}
}
C'est très pratique et ça fonctionne (presque) correctement. Explications :
Si registerValueToUndoManager fonctionne du tonnerre il n'est utile que pour les propriétés et utilisé dans un didSet.
C'est pourquoi j'ai aussi registerUndoForSelfWithSelector pour utiliser une méthode n'ayant qu'un paramètre. Si ça fonctionne dans la plupart des cas quand on utilise un Int c'est autre chose. Ma méthode est tout le temps appelée avec une valeur fantasque. Enfin pas tant que ça il s'avère que ça fait un truc dans ce genre :
val << 8 || b00110111
Les grandes valeurs sont tronquées et les petites sont augmentées. Et c'est tout le temps le même "algo" qui est utilisé. 0 devient 55, 1 devient 311, etc...
Quelqu'un a déjà vu une telle chose ?
Je précise que je suis en Swift 1.2 Xcode 6.3b1
Réponses
Int n'est pas un AnyObject (juste un Any) ; c'est sans doute là qu'est le problème. (d'autant que je me demande si la valeur que tu obtiens je correspond pas à un Tagged Pointer, ce qui aurait du sens dans ce contexte surtout s'il box automatiquement ton Int dans un NSNumber et que quand ta valeur est petite du coup il utilise un Tagged Pointer pour l'optimisation)
Si tu interprète ta valeur comme étant un NSNumber (après tout le NSUndoManager ne manipule que des objets ou des valeurs boxed dans un NSNumber ou une NSValue) comme d'ailleurs tu le ferais en ObjC, ça donne pas des choses plus cohérentes ?
Par exemple :
1) si tu tapes ceci dans un Playground vide, sans importer Foundation :
Alors cela ne va pas compiler, car tu fais alors du pur Swift (tu n'as pas importé le module Foundation ni fait de Bridging Objective-C), et donc swiftArray est sensé être un tableau de AnyObject (autrement dit de reference types, d'instances de classes si tu préfères).
Or en Swift pur, les types String et Int ("Hello" et 5) sont des value type (des struct, en fait), donc ce n'est pas un AnyObject et le compilateur va te sortir une erreur.
2) Si tu remplaces dans l'exemple précédent AnyObject par Any, là le code Swift va compiler, car les types String et Int de Swift sont des struct qui se conforment au type générique Any (mais pas AnyObject).
3) Par contre si tu importes Foundation avant, alors tu as une implémentation Objective-C, derrière, autrement dit "AnyObject" correspond alors un peu à "id", et surtout "Hello" sera casté en type NSString, et ton 5 sera automatiquement boxed/encapsulé dans un NSNumber (comme si tu avais écrit @5 en ObjC) :
---
Comme tu le vois, si tu n'importes pas Foundation et que tu fais du Swift pur, AnyObject ne peut représenter que des classes, et une String ou un Int swift n'en font pas partie...
MAIS dès que tu importes Foundation, alors les types NSString et NSNumber, qui eux sont conformes à AnyObject, permettent tout à coup un bridging Objective-C implicite et changent la donne. Et après tout si tu utilises un NSUndoManager c'est que tu bridges ton code Swift avec de l'Objective-C... et en Objective-C, NSUndoManager box déjà lui-aussi les valeurs dans des NSNumber ou NSValue, donc pas étonnant qu'en Swift la même chose se passe aussi !
---
Tout ça pour dire qu'à mon avis, si tu interprètes ton Int dont tu parles plus haut dans ton code comme un NSNumber (car c'en est sûrement un, ton Int ayant été automatiquement boxé en NSNumber), et demande d'en extraire sa intValue, tu devrais retomber sur tes pas...
Merci pour toutes ces explications ! J'avais mal compris le bridging visiblement.
Comme j'ai modifié le code qu'il fonctionne et qu'il est commit je vais éviter de tout casser et je vais tester sur un autre truc que j'ai à implémenter.
Maintenant y'a quand même une subtilité qui me turlupine un peu: tes explications montrent pourquoi le code suivant ne fonctionne pas correctement :
Mais cela n'explique pas pourquoi ce code fonctionne :
Le setValue:forKey: doit certainement y être pour quelque chose j'imagine...