Bindings & NSUserDefaults (2)
muqaddar
Administrateur
Tutorial réalisé par ClicCool
Complétons la fenêtre des préférences
Nous allons maintenant compléter la fenêtre des préférences et poursuivre notre découverte de l'accès aux préférences bindées, gérer le binding sur les polices, faire connaissance avec les NSTransformer et utiliser le MultipleValuesPatern Binding.
Revenons sous IB avec notre MainMenu.nib et faisons de la place dans notre fenêtre de préférences en augmentant sa hauteur, puis descendonx en bas le bloc NSColorWell et NSTextView ainsi que leurs étiquettes. (voir illustration)
Nous pouvons aussi définir la taille de la fenêtre comme taille minimale. Pour cela, cliquons sur le bouton "Current" du Paneau "Size" de la palette "Infos", en face de la ligne "Min". Notez également que si nous voulons interdire l'élargissement de la fenêtre tout en autorisant son re-dimensionnement vertical, il suffit de cliquer aussi sur "Current" de la ligne des maximales puis de fixer la Hauteur à 0.
Nous avons maintenant de la place dans notre fenêtre. Nous ajoutons deux lignes (en laissant un gran espace en dessous) avec :
- une étiquette "Police" de type "NSTextField"
- un bouton "Changer" de type "NSButton"
- un champ blanc NSTextField sur toute la longueur qui servira à afficher la police en cours, mais pas à la modifier. Nous décochons donc les boà®tes "Editable" et "Selectable".
Vous devriez avoir ce résultat :
[Fichier joint supprimé par l'administrateur]
Complétons la fenêtre des préférences
Nous allons maintenant compléter la fenêtre des préférences et poursuivre notre découverte de l'accès aux préférences bindées, gérer le binding sur les polices, faire connaissance avec les NSTransformer et utiliser le MultipleValuesPatern Binding.
Revenons sous IB avec notre MainMenu.nib et faisons de la place dans notre fenêtre de préférences en augmentant sa hauteur, puis descendonx en bas le bloc NSColorWell et NSTextView ainsi que leurs étiquettes. (voir illustration)
Nous pouvons aussi définir la taille de la fenêtre comme taille minimale. Pour cela, cliquons sur le bouton "Current" du Paneau "Size" de la palette "Infos", en face de la ligne "Min". Notez également que si nous voulons interdire l'élargissement de la fenêtre tout en autorisant son re-dimensionnement vertical, il suffit de cliquer aussi sur "Current" de la ligne des maximales puis de fixer la Hauteur à 0.
Nous avons maintenant de la place dans notre fenêtre. Nous ajoutons deux lignes (en laissant un gran espace en dessous) avec :
- une étiquette "Police" de type "NSTextField"
- un bouton "Changer" de type "NSButton"
- un champ blanc NSTextField sur toute la longueur qui servira à afficher la police en cours, mais pas à la modifier. Nous décochons donc les boà®tes "Editable" et "Selectable".
Vous devriez avoir ce résultat :
[Fichier joint supprimé par l'administrateur]
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Quand la valeur stockée liée diffère de la valeur à afficher :
Nous voulons que le champ affiche le nom de la police (en bon Français), c'est-à -dire le résultat de [font displayName]. Mais nous ne voulons pas stocker dans les préférences un nom de police localisé mais plutôt un nom directement utilisable par le FontManager et conforme au code de langage PostScript, celui obtenu par [font fontName]. Pas question de "stocker" les 2 noms et ce parce que :
1- La règle, en programmation, est de ne jamais avoir à stocker 2 données dont l'une peut-être totalement déduite de l'autre, sinon gare aux conflits de mise à jour (et perte de mémoire accessoirement)...
2- La police choisie (et les valeurs des préférences) ne doit pas dépendre de la localisation.
3- Et surtout j'ai pas envie... ;-)
Il va nous falloir créer un "NSTransformer" qui permet de renvoyer un "displayName" localisé quand on lui passe un "fontName".
Quand le Binding dépend de deux valeurs (ou plus) :
Nous voulons que le champ affiche le nom localisé de la police mais aussi sa taille, obtenue par [font pointSize] qui du reste retourne un float et non un NSNumber (plus utile au NSSharedUserDefaults).
Nous allons donc créer 2 nouvelles clefs de Préférences: fontName et fontSize (désolé mais, en anglais, c'est plus concis que nomDeLaFonte et tailleDeLaFonte, mais faites comme bon vous semble)... Il va aussi falloir utiliser le "MultiplevaluesPattern" des bindings pour associer le nom et la taille dans le même champ.
Le binding sera donc :
- "Value with Pattern" -> "displayPatternValue1"
- "Bind To" : "Shared User Default"
- "Controller Key" : "values"
- "Model Key Path" : "fontName"
- "NSTransformer" : là , nous tapons le nom du transformer que nous coderons plus tard : "FontNameToDisplayTransformer"
Il reste le champ de formatage (qui ne reprend pas les standards du C). Les valeurs à afficher sont représentées par des "%{valueN}@" où N est l'indice de la valeur dans l'ordre des bindings. Ecrivons donc :
- "Display Pattern" : "%{value1}@ en %{value2}@ points" (sans les guillemets)
Vous l'avez compris, il reste à binder la "value2" qui est apparue dès que nous avons bindé la "value1". Une "value3" serait disponible après que la "value2" soit bindée, et ainsi de suite...
- "Value with Pattern" -> "displayPatternValue2"
- "Bind To" : "Shared User Default"
- "Controller Key" : "values"
- "Model Key Path" : "fontSize"
- "Display Pattern" : est déjà écrit : "%{value1}@ en %{value2}@ points"
Il manquerait plus que ça que ce soit différent !
Ce champ saisissable "Display Pattern" disponible à chaque valeur bindé est, à mon sens, assez "limite" sur le plan élégance surtout de la part d'Apple qui pourtant ne se prive pas de nous tirer les oreilles chaque fois qu'on fait mine de s'écarter un peu du sacro-saint (mais utile) "Apple Human Interface Guidelines" !
Nous allons maintenant avoir besoin d'un contrôleur pour gérer le bouton "changer" et le paneau des polices. En théorie, la fenêtre des préférences aurait mérité un contrôleur dérivé de "NSWindowController", mais pour ce que nous allons en faire, notre "AppDelegate" fera l'affaire.
Enregistrons notre .nib et retournons dans Xcode, dans le fichier header AppDelegate.h. Si tout va bien, il ne s'est pas envolé.
Nous allons ajouter :
- un outlet "window", nous en aurons besoin pour signaler que la fenêtre est prête à recevoir un message du FontManager
- un IBAction "choisirPolice", qui ouvrira le FontManager
- et un accesseur pour l'outlet window, soyons KVC compliant !
Ce qui nous donne :
Sauvegardons le fichier AppDelegate.h et revenons à IB pour incorporer notre outlet et notre action. Double-cliquons sur notre cube bleu "AppDelegate", cela nous bascule dans le panneau classes avec la classe AppDelegate déjà sélectionnée. Choisissons alors le menu "Classes -> Read AppDelegate.h". C'est Magique, IB a pris en compte nos nouveaux éléments !
Repassons au panneau "Instances". Nous allons connecter l'Outlet "window":
Ctrl-Cliquez sur AppDelegate, puis glissez jusqu'à la barre de titre de la fenêtre préférences. Enfin choisissez connecter l'outlet "window". Pour connecter l'action "choisirPolice", il suffit d'un Ctrl-Clic sur le bouton, puis d'un glissé vers le cube "AppDelegate", enfin connectez l'action "choisirPolice" dans les infos. Enregistrons et retournons à Xcode.
[Fichier joint supprimé par l'administrateur]
Puis implémentons l'action "choisirPolice", elle doit afficher le Font Manager en le renseignant sur la sélection actuelle.
Récupérons les préférences de police auprès de ce cher "sharedUserDefaultsController" :
Créons la police conformément aux préférences, en n'oubliant pas que les préférences peuvent "pointer" sur une Font qui n'est plus disponible, il faut donc gérer "nil" comme valeur de retour possible.
Initialisons le FontPanel :
Et n'oublions pas, préparons-nous à recevoir les messages du FontPanel :
Il nous faut maintenant implémenter "changeFont", qui sera envoyé par le "Font Panel" à chaque modification de la police sélectionnée.
Le Font Manager utilise exclusivement des messages de conversion et non pas des changements explicites de traits ou de polices (cela lui permet "d'interpréter" à sa façon le résultat en cas d'indisponibilité de la fonte souhaitée par exemple). Nous ferons de même, c'est une habitude à prendre.
Récupérons la police sélectionnée en replaçant une éventuelle absence (nil) par FontSystem :
Obtention de la Fonte validée par le biais de "convertFont" :
Création du NSNumber fontSize pour les préférences :
Et enfin, accès en écriture aux préférences par notre copain le sharedUserDefaultsController et par notre accesseur "setValue: forKey:" :
Il nous reste bien sûr encore à coder le transformeur. Commençons par un peu de théorie.
Aller-simple ou aller-retour ?
Un Transformer dérive d'une classe abstraite : "NSValueTransformer". Tout ce qu'on lui demande c'est de renvoyer un objet quand on lui en passe un. La tambouille qu'il fait importe peu.
- Il peut être "Aller-Retour", c'est-à -dire capable d'inverser la transformation faite : comme le "NSUnarchiveFromData".
Par exemple un Transformer qui renvoie un code unique quand il reçoit une identification d'utilisateur (par sa méthode: -(id)transformedValue:(id)value) doit être capable à l'inverse de renvoyer l'identification si on lui passe le code (par sa méthode -(id)reverseTransformedValue:(id)value).
- Il peut aussi n'être qu'"Aller-simple" et ne pas implémenter "reverseTransformedValue". C'est ce que nous allons devoir faire (essayez donc de retrouver le nom d'une police à partir de n'importe quel nom localisé !) et c'est pour cela que le NSTextField n'est pas saisissable directement. La Classe d'un transformer doit donc répondre à +(BOOL)allowsReverseTransformation en fonction de ses possibilités.
Quel type d'objet renvoie le transformeur ?
La classe d'un transformeur doit également pouvoir déclarer le type d'objet qu'elle renvoie en répondant à la requête : + (Class)transformedValueClass.
Déclaration du transformeur pour que le lien avec le paramétrage IB se fasse.
En fait, quand nous codons une sous-classe de NSTransformer, c'est pour n'en faire qu'une seule instance que nous déclarons alors immédiatement sous le nom EXACT attendu par IB. Cette déclaration doit avoir lieu une seule fois et avant le chargement du fichier .nib, idéalement dans le "initialize" de la classe créant le transformer.
Fort de nos connaissances, créons donc le transformer attendu par notre binding.
Dans Xcode, cliquons sur le groupe des classes en maintenant ctrl enfoncée et choisissons "add -> new file", choisissons Objective-C Class, validons (Next), nommons la "FontNameToDisplayTransformer" et validons (Finish).
FontNameToDisplayTransformer est une sous-classe de NSValueTransformer, et non de NSObject, corrigeons donc le header :
Implémentons maintenant FontNameToDisplayTransformer.m.
C'ets là que se fait tout le travail. Notre transformeur attend un nom de fonte, il crée une fonte (de n'importe quelle taille, on ne récupère après que le nom localisé de la fonte), puis il appelle la fameuse méthode "displayName" et en renvoie directement le résultat :
En théorie, nous aurions dû ici aussi gérer le cas Nil, mais une valeur à nil est acceptée par le binding, et nous éliminerons la prochaine fois, la seule possibilité restante d'une valeur nulle.
Notre transformeur étant écrit, il nous reste à l'instancier et le déclarer. Nous le faisons au moment où la classe AppDelegate est initialisée. Pour que AppDelegate sache ce qu'elle doit manipuler, ajoutons lui ce bout de code :
Et tapons la méthode "initialize" :
Enfin, pour terminer, nous allons réaliser un "truc" inutilement rigolo. Retournons sous IB, et utilison provisoirement l'espace laissé sous le champ d'affichage de la police. Ajoutons un slider vertical, un slider horizontal, un stepper, et un NSTextField sur lequel on fait glisser un NSNumberFormatter.
Paramétrons le tout avec "Valeur Min à 9", "Valeur Max. à 96" (panneau Attributes pour les 4 premiers, panneau Formatter pour le NSTextField). Et pusi bindons les avec la clé fontSize. Nous voilà avec 5 façons différentes d'afficher et de modifier la valeur fontSize. C'est bien sûr inutile et pas trsè beau, mais ça se fait très vite et ça marche sans une seule ligne de code, sans Outlet, ni IBAction.
Sauvegardons, compilons, et exécutons ! C'est fonctionnel, mais il reste des détails à peaufiner. Par exemple, pourquoi ne pas binder le "Font -> fontName" du NSTexteField avec la fonte choisie ? Tant qu'on y est, pourquoi ne pas binder la fontSize du deuxième NSTextField, pour que l'affichage corresponde en tout point ? ;-)
Il va y avoir un os. En effet, comment faire pour adapter la taille du champ blanc à la taille de la fonte ? Nous allons voir cela dans le prochain volet. Tournez la page. ;-)
Vous devriez avoir quelque chose comme ceci :
[Fichier joint supprimé par l'administrateur]
Nous en étions restés à notre champ d'affichage de la police dont nous voudrions synchroniser la taille avec la fonte elle-même. En effet, la taille (du cadre blanc) n'est pas "bindable".
Il nous faut ici nous souvenir de ce que sont les bindings, et surtout de l'architecture qui les soutend. "Le Cocoa Binding se repose lourdement sur le KVC et le KVO" nous dit la documentation officielle.
KVC : Key Value Coding
C'est en quelque sorte une syntaxe dans la dénomination des accesseurs aux propriétés. Pour l'accès à une clé simple, par exemple une NSString dénommée laChaine, l'accesseur en lecture sera dénommé : -(NSString*) laChaine; et l'accesseur en écriture : (void) setLaChaine: (NSString*) inChaine; .
Pour un BOOL nommé "valide", on acceptera en lecture :
-(BOOL) isValide; mais également -(BOOL)valide;
KVO : Key Value Observing
Le KVO permet, en s'enregistrant auprès d'un objet, de recevoir immédiatement une notification en cas de changement de la propriété observée. Le KVO est à la base un protocole informel auquel se conforme NSObject et ses descendants, ce qui permet à une sous-classe de NSObject qui est "KVC compliant" d'assurer un KVO automatique.
Voilà qui est bon à savoir. C'est donc sur le couple KVC/KVO que repose les bindings. Nous ne pouvons binder directement la frame du champ, mais nous pouvons peut-être nous inscrire comme "observateur" KVO comme l'aurait fait un binding puis modifier "à la main" la frame de ce fameux champ.
Et là s'offre à nous deux solutions, la première et la deuxième. Aucune d'entre-elles n'est tout à fait satisfaisante à mes yeux, mais toutes deux sont une super occasion pour s'essayer au KVO. Avant tout, faisons de la place effaçons les sliders, stepper et autres fariboles mises en place tout à l'heure.
1) 1ère possibilité - Observer les UserDefaults
Là nous allons coller au plus près du fonctionnement du NSUserDefaultsController. Nous allons comme lui "observer" les UserDefaults.
Dans Xcode, ajoutons un Outlet à notre header "AppDelegate.h"
Nous en aurons besoin par la suite pour accéder à son frame. Notons que nous ne sommes pas obligés de typer l'Outlet, on aurait pu écrire "IBOutlet id fontField". Mais alors nous perdons, au débuguage, le contrôle à la compilation des méthodes envoyées à l'outlet. Nous aurions pu aussi le typer comme NSControll (NSTextField en est un descendant) puisque nous utiliserons sur lui des méthodes propres aux NSControll, mais là , le contrôle risque d'être excessif si nous décidons à l'avenir d'utiliser une méthode propre aux NSTextField.
Dans IB, connectons notre Outlet.
Ouvrons le mainMenu.nib, double-cliquons sur notre instance AppDelegate, choisissons "Read AppDelegate.h", revenons au panneau instances, "Ctrl-Drag" de AppDelegate au champ et connectons l'Outlet.
Maintenant, l'installation du KVO. Nous allons utiliser cette méthode :
- Envoyée à un objet supportant le KVO, et, en l'occurrence, le même que celui auquel est "abonné" notre NSUserDefaultsController et que l'on obtient, vous l'avez deviné, par le bon vieux [NSUserDefaults standardUserDefaults].
- addObserver: l'objet Observateur: ici self (notre instance de AppDelegate).
- Sur la clef d'entrée "fontSize" (dans notre code elle est modifiée à chaque changement de fonte).
- Avec pour option possible:"NSKeyValueObservingOptionNew" qui signifie que nous voulons que nous soit transmise, avec la notification, la nouvelle valeur. L'autre Option NSKeyValueObservingOptionOld signifie que nous voulons l'ancienne valeur. Si nous voulons les 2 valeurs, l'association des deux options se fait en écrivant: options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld). (usage du OR)
Nous pourrions ici transmettre nil comme option car nous n'utiliserons pas les valeurs transmises, nous demanderons néanmoins les 2 options histoire d'en vérifier la syntaxe et de jeter un oeil sur le NSDictionary.
- Avec un contexte à nil car ici nous n'en avaons pas besoin. Mais on peut passer ici un pointeur (pointeur C ou sur un Objet d'O.C.) qui sera transmis à l'observateur. Attention car le pointeur sera transmis, mais l'éventuel objet associé ne sera pas "retenu" il ne faut pas que l'objet pointé soit "releasé" tant que le lien KVO est actif.
Donc dans Xcode, dans la méthode awakeFromNib ajoutons le code suivant :
Et voilà , dorénavant toute modification de fontSize sera signalée à notre instance de AppDelegate qui recevra le message :
C'est la méthode qu'il faut maintenant implémenter dans notre observateur AppDelegate et qui reçoit:
- dans keyPath : la clé d'entrée qui a été modifiée
- dans object : l'objet observé dont la clef a été modifiée
- dans change : un NSDictionary avec comme clés d'entrée possibles les constantes suivantes (prenez tout votre temps pour lire) :
* NSKeyValueChangeKindKey précisant le type de modification effectué (NSKeyValueChangeSetting, NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement)
* NSKeyValueChangeNewKey qui nous renvoie (si nous avons précisé NSKeyValueObservingOptionNew), si NSKeyValueChangeSetting la nouvelle valeur ou si NSKeyValueChangeInsertion un Array des valeurs insérées ou si NSKeyValueChangeReplacement un array des valeurs en ayant remplacé d'autres
* NSKeyValueChangeOldKey qui nous renvoie (si nous avons précisé NSKeyValueObservingOptionOld ) si NSKeyValueChangeSetting l'ancienne valeur ou si NSKeyValueChangeRemoval un Array des valeurs retirées ou si NSKeyValueChangeReplacement un array des valeurs en ayant été remplacées d'autres
* NSKeyValueChangeIndexesKey qui nous renvoie, si une des 3 dernières valeurs citées comme NSKeyValueChangeKindKey est retournée, un NSIndexSet des objets concernée
- dans context: le pointeur fourni lors de l'activation de l'observation.
Ne stressons pas, nous n'utiliserons pour l'heure que le keyPath. Implémentons donc ette méthode dans AppDelegate.m. Nous commençons, pour la forme, par tester la clé ayant provoqué la notification, puis nous allons provoquer le redimensionnement du champ grâce à la méthode "SizeToFit" de NSControl.
Bien, sauvegardons, compilons et exécutons tout ça.
Le résultat est très imparfait. Pour commencer, le champ n'est pas du tout adapté au lancement del'application, ajoutons donc dans awakeFromNib :
Ensuite, le champ se balade un peu trop dans els redimensionnements. Nous pouvons donc le redescendre, puisque nous avons enlevé les sliders.
Enfin et surtout le redimensionnement n'est pas adapté lorsqu'on se contente de modifier la taille de la police par le panneau des fontes. Il semble en effet que l'on soit informé du changement de "fontSize" AVANT que le champ n'ait lui-même été changé de fonte. Si on clique chaque fois deux fois sur la taille voulue, à la deuxième fois, c'est bon. A ma connaissance, y'a pas moyen d'avoir un contrôle sur l'ordre de distribution des notifications de changement.
On peut ici corriger le comportement en se passant de "l'automatisme" de sizeToFit. Ce qui suit est une proposition et ne fait pas à proprement partie du tutorial, vous pouvez sauter à la deuxième possibilité ("Observer le champ lui-même" si vous voulez).
Nous remplaçons dans awakeFronNib et dans observeValueForKeyPath: ofObject: change: context: les lignes suivantes (commentons les avec des // ou des /* ... */ car nous les réutiliserons après).
Par une méthode personnelle du type:
-(void)adaptFieldToFont
Par un appel à une méthode que nous nommerons: -(void)adaptFieldToFont
Que nous déclarons dans le header et que nous implémenterons par exemple comme ceci :
Cette fonction a l'avantage de maintenir la partie haute du champ en place et de maintenanir la largeur du champ par rapport à la fenêtre. Le code n'est pas élégant...mais le résultat est plus esthétique à mes yeux.
2) 2ème possibilité - Observer le champ lui-même
Les NSTextField héritent bien de NSObject, de plus on y trouve pas mal d'accesseurs KVC compliants et en particulier: "font" et "setFont" (hérités de NSControll). Il ne nous en faut pas plus, nous avons là une clef "observable"
Commentons (ou effaçons) dans awakeFromNib la ligne :
Nous la remplaçons par :
Dans la foulée, rétablissons à la place de l'appel à notre méthode maison précédente dans awakeFromNib et observeValueForKeyPath: ofObject: change: context:
N'oublions pas de modifier le test sur la clef modifiée dans: observeValueForKeyPath: ofObject: change: context: pour y mettre la clef désormais observée: "font" (on peut aussi ajouter le test sur cette clé au test sur "fontSize" pour pouvoir plus facilement basculer d'une version à l'autre en vue de choisir l'option finale) :
Sauvegardons, compilons et exécutons: ça marche ! (Sauf que je trouve le résultat fort peu esthétique, mais là n'était pas le but de ce tutorial).
Voilà nous savons maintenant identifier les objets "observables" et mettre en place un KVO. Les bindings nous sont déjà bien moins mystérieux ! Pour l'amateur que je suis, c'est déjà très satisfaisant.
Si vous le voulez bien, nous allons continuer avec un dernier (?) mot très court sur le fonctionnement du NSUserDefaultsController qui, sous IB, met à notre disposition des actions utiles: revert:, save: et revertToInitialValues: que nous allons utiliser ici.
Toute bonne application ayant une gestion des préférences utilisateur, devrait fournir à celui-ci la possibilité de restaurer les préférences par défaut. Il nous reste donc en effet à prévoir des valeurs par défaut à nos userDefaults et à les rendre accessibles.
Dans IB, ajoutons en bas de notre fenêtre un bouton "Restaurer". Connectons le à l'action adéquate : "Ctrl-drag" du bouton vers notre cher contrôleur de préférences (instance verte "Shared Defaults"). Dans le panneau connexions de la palette info nous choisissons et connectons "revertToInitialValues:".
Dans Xcode: créons le dictionnaire des préférences par défaut
Celles-ci peuvent être en dur dans le code d'initialisation (ce que nous allons faire ici), ou dans un fichier plist dans le bundle de notre application. (Ce qui est de loin préférable à mon sens).
Dans notre méthode + initialize de AppDelegate, créons un dictionnaire dans lequel nous mettons toutes les valeurs par défaut de nos clés de préférences:
Nous avons pris soin d'écrire ces valeurs de la même façon dont le contrôleur l'aurait fait. En Particulier :
- pour la couleur nous avons stocké un NSData en utilisant un NSArchiver
- pour le texte avec attribut nous avons stocké un NSData avec RTFDFromRange qu'utilise aussi le contrôleur.
- pour la fonte on se base sur la fonte system par defaut.
Déclarons ces valeurs par défauts, simplement par la méthode "setInitialValues" à la suite du code précédent :
Et voilà , c'est très rapide, exécutons tout ça, et cliquons sur notre nouveau bouton. Vos préférences sont réinitialisées ! A la prochaine fois, avec les tableaux et NSArrayController cette fois !