Bindings & NSUserDefaults (1)

muqaddarmuqaddar Administrateur
février 2007 modifié dans API AppKit #1
Tutorial réalisé par ClicCool

Création de la fenêtre des préférences

Commençons par créer un nouveau projet sous Xcode de type Cocoa Document-Based Application, que l'on peut appeler "LaboBindings" par exemple. Double-cliquons sur la ressource NIB:MainMenu.nib. Nous voilà  dans Interface Builder.
Ajoutons une fenêtre NSWindow en faisant glisser celle-ci à  partir de la palette Cocoa-Windows sur le bureau. Nous pouvons renommer l'instance "PrefsWindow" dans la fenêtre MainMenu.nib pour éviter toute confusion ultérieure entre les noms d'instances, et nommer la fenêtre "Préférences" dans la palette des informations pour continuer à  être original...

Dans cette fenêtre, nous allons ajouter les éléments suivants, toujours à  partir de la palette informations :
- une case à  cocher "switch" de type NSButton que nous nommons "Ouvrir une fenêtre de document à  l'ouverture de l'application"
- au dessous, une étiquette "System Font Text" de type "NSTextField", nous la nommerons "Nom par défaut"
- à  sa droite un champ "NSTextField" (rectangle blanc simple) que nous allongeons un peu
- une autre étiquette en dessous qui a pour titre "Couleur"
- à  sa droite une champ "NSColorWell" (le rectangle bleu avec une bordure)
- en dessous un bouton "switch" dont nous fixons le titre à  "Texte d'accueil"
- et pour terminer, ajoutons un "NSTextView" qui se rapportera au bouton précédent.

Nous pouvons maintenant connecter le menu "Préférences" (dans le menu "New Application") à  la barre de titre de la fenêtre "Préférences" (par Ctrl-Clic déplacé) en choisissant l'action "makeKeyAndOrderFront" de la palette des informations de façon à  ce qu'elle apparaisse lorsque nous choisirons le menu Préférences.

[Fichier joint supprimé par l'administrateur]

Réponses

  • muqaddarmuqaddar Administrateur
    février 2007 modifié #2
    Plongeons dans les Bindings

    Maintenant que notre fenêtre est créée et que nos objets sont posés, nous pouvons aborder notre sujet : les bindings. Attachez-votre ceinture, c'est parti.

    Sélectionnons le premier switch (en haut) et affichons le panneau "Bindings" dans la fenêtre des informations. Vous remarquez que ce panneau contient des titres blancs sur fond bleu ("Value", "Availabaility"...etc), ainsi que des sous-menus précédés d'un petite triangle noir, qui sont la propriété précise de l'objet d'interface "bindé". Cliquons sur le petit triangle noir justement pour découvrir toutes les propriétés possible de cet objet ("switch").
    - "Bind To" est déjà  positionné sur "Shared User Defaults", ce qui est parfait.
    - "Controller Key" : avec "Shared User Defaults", la seule clé de contrôleur possible est "values".
    - "Model Key Path" : nous pouvons ici donner un nom de clé, par exemple "ouverture", qui est en rapport avec le but de notre "switch"
    - enfin, assurons-nous que la boà®te à  cocher "Bind" en haut à  droite soit bien... cochée. ;-) Je ne le répèterai plus dorénavant.

    Pour le "NStextField" au dessous, nous paramétrons les même propriétés de Bindings, en changeant juste le "Model Key Path" à  "nom".

    Le "NSColorWell" a aussi les mêmes attributs, son "Model Key Path" est "couleur". Mais là , il y a une précaution particulière à  prendre : les "NSColor" ne sont pas des "property list objects" et ne peuvent être sauvegardés sur le disque tel quel. Cocoa Bindings nous permet alors d'utiliser les "NSTransformer", et Interface Builder nous en propose un : le "NSUnarchiveFromData". Il nous suffit alors dans notre cas de sélectionner "NSUnarchiveFromData" dans le menu déroulant "Value Transformer", et le tour est joué.

    Au passage, sélectionnons l'étiquette "Couleur" et bindons là  aussi. Nous ne binderons pas "value" mais la couleur de son texte. Allons dans "Text Color" -> "text color" en bas du panneau des Bindings :
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key Path" : "couleur"
    - "NSTransformer" : "NSUnarchiveFromData" (n'oublions pas sinon gare aux messages d'erreurs)

    Pour la CheckBox du dessous ("Texte d'accueil"), nous bindons sa "value" et tapons "accueillir" comme clé. Vous remarquez que j'ai inventé un nouveau verbe "binder" depuis quelques paragraphes, j'espère que vous ne m'en voulez pas, c'est pour aller plus vite et c'est plus compréhensible. :-)

    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    01:44 modifié #3
    La "NSScrollView" (de "NSTextView) se sélectionne par un simple clic. Son binding "Availability" -> "hidden" est :
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key Path" : "accueillir"
    Quand accueillir est vrai, nous voulons que le texte ne soit pas caché et vice-versa. Pour l'instant, ce binding tel que nous l'avons défini fait tout son contraire. Qu'à  cela ne tienne, nous aurons encore recours aux NSTransformer dont 3 viennent d'apparaà®tre ici, pour ce Binding qui gère un BOOL (booléen, true ou false). Il ne nous reste qu'à  sélectionner "NSNegateBoolean" qui fait exactement ce qu'on attend !

    Enfin, la "NSTextView" (sélection par double-clic cette fois) aura les caractéristiques suivantes, menu "Value" puis "data" :
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key Path" : "texteAccueil"

    Voilà  pour les délcarations de bindings. Vous pouvez maintenant enregistrer votre NIB (si ce n'est déjà  fait). Puis compiler et éxécuter le projet. Un fichier .plist sera d'ores et déjà  créé par défaut (~/Bibliothèque/Preferences/com.apple.myApp.plist) et mis à  jour avec les clés que nous avaons définies. Vous pouvez l'examiner si vous le voulez.

    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    01:44 modifié #4
    Continuons notre exploration

    Voilà , vous avez maintenant bien jouer avec ces balbutiements ? Améliorons maintenant le fonctionnement de notre fenêtre préférences en fignolant. Tout n'est pas parfait peut-être l'avez-vous remarqué. Ne vous inquiétez pas !

    1-Mise à  jour et validation des saisies

    Si vous avez redémarré votre application, vous avez dû vous apercevoir que "NSUserDefaults" n'est pas toujours correctement mis à  jour pour le dernier champ de type texte modifié. En effet, la prise en compte des modifications ne se fait que lorsque l'objet perd le focus, "Did End Editing" est alors envoyé.
    Sélectionnons notre "NSTextField" (le champ blanc) et intéressons nous aux options de bindings dans "Value".

    "Continuously Updates Value" : permet de répercuter les modifications sur tous les objets d'interface au fur et à  mesure de la frappe (je trouve cela décoiffant). Cochons cette option pour nos deux champs texte (View et Field).
    Nous allons maintenant binder la fenêtre pour nous amuser un peu :
    - "Parameters" -> "title"
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key path" : "nom"
    Vous pouvez compiler et tester tout de suite ! ;-) C'est complétement inutile pour une fenêtre de préférences mais très instructif !

    Les autres options à  cocher (pour un NSTextField) ne concernent guère que les ArrayController et leur sous-classes (mais rien n'empêche d'avoir un Array dans les NSUserDefaults) :
    - "Allows Editing Multiple Values Selection" : cochée par défaut, elle permet de répercuter une saisie sur tous les objets sélectionnés (et non plus sur l'objet sélectionné) dans le cas d'une relation 1 pour n avec un binding sur selectedObjects (non disponible pour un NSUserDefaults mais très utile pour un NSArrayController).
    - "Conditionally Sets Editable" : cochée par défaut, elle permet une prise en charge automatique de la propriété "Editable" de l'objet d'interface bindé et de laisser l'ArrayControler décider si l'objet est éditable. En gros si la sélection concerne plusieurs objets, ils ne sont éditables par ce champ que s'ils sont considérés comme tous égaux.
    - "Conditinnally Sets Enable" : idem pour la propritété "Enable"
    - "Conditionally Sets Hidden" : idem pour la propritété "Hidden"
    - "Raise For Not Applicable Key" : cochée par défaut, elle est utile (pour tous type de NSController) dans la phase de débuguage. Cela permet de lever une exception si la clé aboutit à  un résultat inattendu (la clé renvoie sur un BOOL au lieu d'un Array par exemple)
    - "Validate Immediatly" : les répercussions au modèle objet (nos préférences) passent par une méthode de validation (à  coder) conforme au "Key Value Validation" permettant alors soit d'accepter la valeur saisie, soit de la substituer par une autre, soit de la refuser en émettant une erreur descriptive

    2-MyDocument.nib

    Nous avons opté pour un projet "Cocoa Document based Application", et bien, nous devons aller plus loin et passer à  la fenêtre du document. Ouvrons "MyDocument.nib" sans en changer le contenu : une simple étiquette au centre "Your document contents here".

    Nous allons simplement binder l'étiquette par défaut :
    - "Value" -> "value"
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key path" : "nom"
    puis :
    - "text Color" -> "textColor"
    - "Bind To" : "Shared User Defaults"
    - "Controller Key" : "values"
    - "Model Key path" : "couleur"
    - "Value Transformer" : "NSUnarchiveFromData"

    C'est magique, le binding se répercute d'un nib à  l'autre ! Il reconnaà®t les noms des "Model Key Path". Bon voilà  pour un bon début sur les bindings, maintenant, codons un peu tout de même !

    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    01:44 modifié #5
    Codons un peu maintenant

    Vous avez dû encore remarqué que tout ne marchait pas, par exemple, la fenêtre de document s'affiche tout le temps, même quand la checkbox correspondante est sur "false". Il va falloir coder un peu pour approndir nos connaissances.

    1-Commençons par un peu de théorie

    Les bindings nous permettent de gérer les préférences de façon "quasi-automatique", mais il est parfois nécessaire de lire ou écrire certaines valeurs de façon "manuelle". Nous avons jsuque-là  beaucoup bindé sur le "shared User Defaults Controller" à  qui nous avons confié la gestion de nos préférences. Ainsi, c'est par ce dernier que passent tous les accès à  nos préférences dès lors qu'il est utilisé dans notre interface, y compris dans le code.

    Si nous utilisons, comme nous en avons l'habitude, un appel du type :

    MesPrefs = [NSUserDefaults standardUserDefaults];  // PAS GLOP :(
    


    Nous accédons directement aux préférences enregsitrés en ignorant le "shared User Defaults Controller" et en perdant la garantie de synchronisation apportée par les bindings. Les valeurs lues ne sont pas tout à  fait garanties mais surtout les éventuelles modifications faites par les "standardUserDefaults" vont se télescoper avec celles du "Shared User Defaults Controller". Inutile de s'affoler, il suffit de modifier légèrement nos habitudes et d'adopter la méthode d'accès suivante :

    id mesPrefs = [[NSUsersDefaultsController sharedUserDefaultsController] values];  // GLOP GLOP :)
    


    Elle nous permet de trouver l'unique instance partagée de sharedUserDefaultsController (instance qui soit dit en passant invoque [NSUserDefaults standardUserDefaults] pour créer les valeurs par défaut), et de lui demander toutes les valeurs. L'objet renvoyé "mesPrefs" est "Key Value Compliant", c'est à  dire qu'il obéit à  la syntaxe des accesseurs "Key Value Coding". C'est tout ce que nous avons à  savoir sur lui (ne le répétez pas, mais en l'occurrence le bruit court que c'est un NSDictionary...). Pour dialoguer avec lui, nous utiliserons la syntaxe satndard KVC : "valueForKey:" ou "setValue: forKey:". Attention à  la casse à  respecter absolument !

    2-Mise en pratique (ouf)

    Les avis sont partagés et l'utilisateur final est peut-être le seul à  pouvoir dire quel comportement il attend de son application à  son lancement :
    - ouverture de l'application seule
    - ouverture de l'application et d'une fenêtre de création d'un nouveau document
    Nous allons donc laisser le choix à  l'utilisateur et utiliser la clé "ouverture" que nous avons définie dans notre fenêtre préférences.

    Commençons par créer un "Delegate" à  notre application. Dans Xcode, cliquons sur le groupe de classes en maintenant la touche "ctrl" enfoncée et choisissons "Add -> New File", "Objective-C Class", "Next", nommons-là  "AppDelegate" et validons pour terminer. Nos deux fichiers AppDelegate.h et AppDelegate.m sont créés.

    Dans IB, revenons à  notre MainMenu.nib. Cliquons sur l'onglet classe de la fenêtre du MainMenu.nib, puis choisissons le menu "Classes -> Read files", localisons sur le disque dur et sélectionnons notre nouveau fichier header "AppDelegate.h" et validons (Parse). Choisissons alors dans ce même menu Classes "Instantiate AppDelegate". Une instance du delegate est alors créée et la fenêtre bascule sur l'onglet "Instances", un joli cube bleu "AppDelegate" vient d'être ajouté.

    Il ne reste plus qu'à  indiquer que c'est le délégate de notre application.
    Avec la touche "ctrl" enfoncée, cliquons sur l'icône du File's Owner (l'application est propriétaire du mainMenu.nib) et glissons (ctrl enfoncée) au dessus de notre cube bleu. Dès que nous lachons le clic, la fenêtre des informations nous affiche les infos de connexions possibles (par Outlet). Nous choisissons donc l'outlet "delegate" et cliquons sur le boutons "Connect".
    Sauvegardons mainMenu.nib.

    [Fichier joint supprimé par l'administrateur]
  • muqaddarmuqaddar Administrateur
    01:44 modifié #6
    Retournons à  Xcode pour coder notre AppDelegate. Accrochons-nous, il y a au moins deux lignes à  écrire ! ;-)

    Un délégate d'application reçoit régulièrement de l'application des messages l'informant de tel ou tel évènement en lui demandant si'il les accepte. L'utilisation d'un délégate permet souvent de personnaliser le comportement d'une application sans avoir à  sous-classer NSApplication. Le délégate n'a du reste pas l'obligation d'implémenter toutes les méthodes possibles d'un délégué d'application. Simplement, les méthodes non implémentées aboutiront à  un comprtement standard de l'application.

    Nous allons ici nous contenter d'écrire la méthode suivante :
    - (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
    qui est invoquée immédiatement avant l'ouverture d'un document sans titre (Attention : cela fonctionne lors du lancement de l'application et de son rappel au premier plan si aucun document n'est ouvert, mais PAS si on sélectionne le menu "File->New").

    Implémentons maintenant AppDelegate :

    @implementation AppDelegate<br /><br />- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender<br />{<br />&nbsp; &nbsp; //récupérons les valeurs des préférences auprès du contrôleur<br />&nbsp; &nbsp; id mesPrefs = [[NSUserDefaultsController sharedUserDefaultsController] values];<br />&nbsp; &nbsp; //renvoyons la valeur de la boà®te à  cocher sous forme d&#39;un simple booléen<br />&nbsp; &nbsp; return [[mesPrefs valueForKey: @&quot;ouverture&quot;] boolValue];<br />}<br /><br />@end
    


    Il ne reste plus qu'à  compiler et éxécuter l'application. Selon la boite à  cocher des préférences, un nouveau document est ouvert ou pas:
    - au Lancement de l'application
    - et, s'il n'y a aucune fenêtre ouverte, lorsque l'on ramène toute l'application au premier plan (en cliquant sur son icône dans le dock).
    Vous pouvez vérifier qu'en sélectionnant le menu "File ->New" un nouveau document et quand même toujours ouvert. :-)
    Cette fonction est très utile, et apporte une réponse complète à  une "Frequently Asqued Question". Elle a aussi le mérite de nous mettre gentiement un pied dans l'aspect code des Bindings. La prochaine fois, promis, on fera quelque chose d'inutilement rigolo.
Connectez-vous ou Inscrivez-vous pour répondre.