AppDelegate, singletons et variables globales.

berfisberfis Membre
juin 2013 modifié dans API AppKit #1

Bonjour,


 


Pour commencer, sachez que l'idée même de variables globales me révulse depuis ma première ligne de code, pourtant il vient un moment où... à  force d'empiler les propriétés, de les transmettre en paramètre ou des les récupérer à  coup de mon machin.superview.controller.maVariable, il me vient des envies sournoises de recourir à  cette abomination.


 


Je m'attends à  des commentaires du type: "une application bien conçue ne devrait pas recourir à  de telles extrémités". Je sais et croyez bien que j'ai fait très attention à  ne pas créer de dépendances entre mes classes, à  moins que cela fasse sens. Par exemple, je n'ai pas de cycle #import ce qui est bon signe...


 


Cela dit, il se trouve que mon application stocke une bonne partie de ses paramètres dans une plist. Ce n'est pas que j'aime particulièrement les NSDictionaries, mais je trouve plus pratique de travailler sur ma plist que de modifier des constantes ou des variables dans mon code. Après tout, Cocoa utilise fréquemment des dictionnaires pour passer des options en paramètres, par exemple.


 


L'un deux est assez pratique: celui des user defaults. Voilà  un dictionnaire auquel on peut accéder de partout, je suppose qu'on peut parler d'un "singleton".


 


 


NSUserDefaultsController provides a shared instance of itself via the class methodsharedUserDefaultsController. This shared instance uses the NSUserDefaults instance returned by the methodstandardUserDefaults as its model, has no initial values, and immediately applies changes made through its bindings.


Bon, c'est l'équivalent que j'aimerais reproduire: une instance unique et partagée d'un dictionnaire (ou d'un contrôleur de dictionnaire) mais concrètement... je m'y perds.

 

Avez-vous déjà  implémenté l'une de ces bestioles? Joue-t-elle bien le rôle que j'en attends? Y a-t-il des précautions à  prendre au niveau des propriétés? (je travaille avec ARC et mon dictionnaire est readonly).

 

Et surtout comment fait-on? J'ai lu beaucoup de sujets sur le web mais peu de solutions. Chez vous peut-être?

 

D'avance merci! 

Réponses

  • Pourquoi ne fais-tu pas comme suit :


     


    - tu crées une sous-classe de NSDictionary


    - tu lui donnes une méthode +sharedInstance


     


    ?


     


    Souhaites-tu accéder à  tes clés dans des xib ?


    Dans ce cas, tu peux créer une propriété dans la classe qui t'intéresse qui pointe vers ce BerfisSharedDictionary ;)

  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #3
    Je ne vois pas en quoi ton besoin justifierai une variable globale, donc j'ai du mal à  comprendre le but de ton introduction façon "bon les globales ça me révulse mais je vais devoir y passer qd mm" alors qu'en fait y'a pas besoin ^^ :D

    Tu utilise le pattern SharedInstance (le truc qu'on appelle parfois à  tort singleton par abus de langage) pour faire une classe dédiée à  tes valeurs que tu as dans ton PLIST, tu fais en sorte qu'à  l'initialisation ça aille lire les valeurs dans ton PLIST, tu mets des accesseurs qui vont bien et basta. Pour les accesseurs, tu peux imaginer n'en avoir qu'un seul façon "objectForKey:" et lui passer la clé (une constante, bien sûr), mais c'est quand même plus propre et mieux d'avoir autant d'accesseur que de clés si possible, pour rendre ton code plus lisible, pour typer les valeurs de retour, etc...
    // .h
    @interface AppConstants : NSObject
    +(AppConstants*)sharedInstance;
    @property (nonatomic, readonly) NSString* someString;
    @property (nonatomic, readonly) CGFloat someFloat;
    @property (nonatomic, readonly) BOOL someBool;
    @end
    // .m
    @interface AppConstants() /* Private Interface */
    @property (nonatomic, strong) NSDictionary* plistValues;
    @end

    @implementation AppConstants
    +(AppConstants*)sharedInstance
    {
    static AppConstants* sharedInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    sharedInstance = [AppConstants new];
    NSString* plistFile = [[NSBundle mainBundle] pathForResource:@constants ofType:@plist];
    sharedInstance.plistValues = [NSDictionary dictionaryWithContentsOfFile:plistFile];
    });
    return sharedInstance;
    }

    -(NSString*)someString { return self.plistValues[@KeyForThisStringValue]; }
    -(CGFloat)someFloat { return [self.plistValues[@KeyForThisFloatValue] floatValue]; } // extraire le float du NSNumber retourné par le NSDictionary
    -(BOOL)someBool { return [self.plistValues[@KeyForThisBoolValue] boolValue]; } // extraire le BOOL du NSNumber retourné par le NSDictionary

    @end
    PS : Et encore on pourrait plutôt imaginer des constantes (static NSString* const xxx = yyy) en haut du .m pour lister les clés du PLIST plutôt que les mettre en dur dans le code des getters qu'on surcharge, car les Magic Numbers c'est mal et les constantes c'est mieux, mais bon là  on sait qu'on va les utiliser qu'une seul fois et jamais ailleurs donc bon...
  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #4
    Petit ajout / info au cas où tu ne saurais pas : je suppose que tu connais les fichiers ".strings" qu'on utilise pour faire de la localisation (la traduction de ton appli en plusieurs langues). On écrit en général ces fichiers "strings" dans leur format le plus courant qui est un simple fichier texte avec une ligne par paire clé/valeur, la clé séparé de la valeur par un signe égal :
    "greetings" = "Bonjour !";
    "bye" = "Au Revoir !";
    Eh bien si je te parle de tout ça c'est parce que tu ne le sais peut-être pas, mais il se trouve que ces fichiers .strings sont en fait des fichiers PLIST !

    En effet, un PLIST peut être représenté sur le disque soit par un fichier au format XML, qu'on voit souvent quand on l'édite à  la main, soit par un fichier au format binaire, qui est le format dans lequel Xcode le convertit quand il compile ton application en un .IPA parce que ce format prend moins de place, soit par un fichier au format texte, comme on le fait pour les "Localizable.strings". Si tu demandes à  Xcode d'ouvrir ton Localizable.strings en tant que fichier PLIST, tu verras qu'il le fait sans problème, en t'affichant une ligne par entrée. Car en fait un fichier .strings représente un PLIST qui n'est rien d'autre qu'un NSDictionary de NSStrings

    Donc au final, pour ton fichier de constantes, surtout si ce ne sont que des constantes textuelles, peut tout à  fait être écrit avec ce format texte, et donc facilement éditable avec un éditeur de texte quelconque, tu n'es pas obligé de l'éditer avec l'éditeur de PLIST d'Xcode si son format c'est juste des lignes de texte avec "constante"="valeur" ... je sais pas si ça te servira, mais je trouve ça bon à  savoir en tout cas !
  • berfisberfis Membre
    juin 2013 modifié #5

    @colas: pas question de dériver NSDictionary  :* Tu l'as déjà  fait?



     


     


    Dans ce cas, tu peux créer une propriété dans la classe qui t'intéresse qui pointe vers ce BerfisSharedDictionary

    C'est justement ce que je fais avec mon AppDelegate qui me sert de fourre-tout, mais je n'aime pas ce code à  rallonge (sans compter que ça induit des cycles #include, @class etc.


     


    @AliGator: merci pour l'exemple et les conseils. En fait, pour mes clés j'utilise les #define MA_CLE = @MaCle, ça me donne la sécurité nécessaire contre les fautes de frappes. J'ai un fichier "Commons.h" que j'inclus là  où j'en ai besoin, assorti d'une "raguillée" de commentaires, comme on dit chez nous pour "beaucoup".


     


    Mon problème, c'est que mon plist est assez structuré, une peu comme une "jump table": des clés qui "pointent" vers d'autres clés en un schéma assez satisfaisant en fait, et que je n'aimerais pas perdre. D'où l'idée d'inclure le plist tel quel (donc un dictionnaire au final).


     


    J'aime bien les méthodes de ton "singleton", c'était bien l'idée de reproduire les "integerForKey" etc. du NSUserDefaultsController...


     


    Deux questions:


    1. le bloc dispatch_once garantit que l'initialisation n'aura lieu qu'une fois? Il faudrait l'exécuter dans le applicationWillFinishLaunching?


    2. Comment accéder à  l'instance? Puis-je faire un


    #define AppConstBool [[[AppConstants sharedInstance]objectForKey:x]boolValue] puis des BOOL x = [AppConstBool:MA_CLE]; partout dans mon code (en incluant AppConst.h partout où je veux l'utiliser)?




  • @colas: pas question de dériver NSDictionary  :* Tu l'as déjà  fait?




     


    Y'a-t-il un problème à  faire cela ?


     


     


     




     


    2. Comment accéder à  l'instance? Puis-je faire un


    #define AppConstBool [[[AppConstants sharedInstance]objectForKey:x]boolValue] puis des BOOL x = [AppConstBool:MA_CLE]; partout dans mon code (en incluant AppConst.h partout où je veux l'utiliser)?




     


    Je ne sais pas si ça marche, mais, dans ce cas, il serait plus prudent de créer une @property (strong) ton AppConstBool par exemple dans le AppDelegate. Juste pour que le cycle de vie de AppConstBool soit lié à  celui de l'application.


     


    En effet sinon je pense que à  chaque fois que tu feras appel à  AppConstBool, il recréera l'objet (avec ton initialisation qui va chercher dans un fichier, etc.)


  • En effet sinon je pense que à  chaque fois que tu feras appel à  AppConstBool, il recréera l'objet (avec ton initialisation qui va chercher dans un fichier, etc.)



    static AppConstants* sharedInstance;


    Bon, alors cette variable statique ne l'est pas?
  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #8

    1. le bloc dispatch_once garantit que l'initialisation n'aura lieu qu'une fois? Il faudrait l'exécuter dans le applicationWillFinishLaunching?

    dispatch_once(&onceToken, block) garantit que le code dans le bloc ne sera exécuté que la première fois qu'il est appelé. C'est un peu, pour simplifier, pareil que si tu avais un "static BOOL alreadyInitialized = NO" au lieu du onceToken et que tu faisais un "if(!alreadyInitialized) { block; alreadyInitialized = YES; }". Sauf que ça utilise GCD, il y a les barrières qu'il faut et c'est thread-safe.
    Pas besoin de l'appeler dans "applicationDidFinishLaunching". L'instance sera initialisée la première fois que tu en auras besoin et ira donc lire le PLIST lors de ce premier appel, et restera en vie ensuite, évitant de s'initialiser (et lire le PLIST) à  nouveau les fois d'après.

     

    2. Comment accéder à  l'instance? Puis-je faire un
    #define AppConstBool [[[AppConstants sharedInstance]objectForKey:x]boolValue] puis des BOOL x = [AppConstBool:MA_CLE]; partout dans mon code (en incluant AppConst.h partout où je veux l'utiliser)?

    Alors :
    - non tu ne pourras pas car ce ne serait de toute façon syntaxiquement pas correct
    - quel intérêt et que dommage de continuer à  fonctionner avec des clés lors de tes appels... ça veut dire qu'il faut absolument dans ton fichier où tu as tous tes #define de toutes tes clés (déjà  c'est pas top d'utiliser des #define plutôt que des const pour ça, car du coup tu perds le typechecking et tu peux aussi faire des trucs bizarres comme concaténer tes constantes par erreur...) que tu notes le type de retour associé à  ta constante, et surtout à  l'appel que tu ne te gourres pas à  appeler boolValue avec une clé qui retourne une NSString ou intValue avec une clé qui retourne un float...

    Donc non, si tu suis mon exemple de code je te conseille plutôt fortement de déclarer autant de @property(readonly) (ou de methodes accesseur toutes simples a la limite) que tu as de constantes dans ton PLIST comme ça tu n'as plus à  avoir un fichier plein de #define, plus de risque de faire des mauvaises bidouilles avec tes defines, plus de risque de mélanger une clé associée à  un BOOL avec un appel qui va utiliser floatValue dessus au lieu de boolValue, chaque getter va intégrer directement l'étape de unboxing (retourner un BOOL et pas un NSNumber, etc)

    Pour l'utiliser c'est simple : CGFloat f = [AppConstants sharedInstance].someFloat;
  • tabliertablier Membre
    juin 2013 modifié #9

    Si je me souviens bien, les fichiers .strings peuvent être structurés avec des dictionnaires et des Array. C'est expliqué dans la doc.


    Petits problèmes quand même, le Property List Editor les lit parfaitement mais refuse désormais de sauver sous forme ".strings file" et pour ne rien perdre on a intérêt à  utiliser l'option "Show strings as Non-lossy ASCII"


  • AliGatorAliGator Membre, Modérateur
    Oui je trouve ça dommage mais j'avais remarqué aussi, Apple a abandonné la possibilité de générer des fichiers au format ASCII. Tant dans l'IDE que dans les API. Le module de lecture de ce format ASCII existe toujours, lui (dans l'IDE et l'API), et continuera d'exister (ne serait-ce que pour les fichiers .strings justement) mais pas l'écriture, c'est dommage...
  • laudemalaudema Membre
    juin 2013 modifié #11

    Confronté à  une problématique sans doute peu semblable à  la tienne j'utilise les NSUserDefaults pour certaines choses et un NSDictionary / plist pour d'autres.


    Dans le UserDefaults je mets par exemple les URL de dossiers ou fichiers sur le disque dont j'ai besoin un peu partout dans mon application. En plus dans les UserDefaults tu as la méthode URLKForKey: les keys étant déclarées dans un fichier à  part en extern. que j'importe donc dans les implémentation des classes qui en ont besoin. J'y met aussi des éléments d'interface qui peuvent se retrouver dans plusieurs xib mais dans ces cas là  je crée la clef directement dans IB puis la recopie dans mon fichier d'externes. Par contre il me faut après avoir un IBOutlet sur l'élément d'interface quand je dois en récupérer la valeur dans le code du contrôleur. ça marche très bien pour des strings ou des cases à  cocher.


    Enfin, pour l'instant les tableaux d'objets de mes principales classes (5 au total) sont encore dans AppDelegate et j'ai aussi un dictionnaire pour certaines propriétés qui changent rarement mais doivent être connues au lancement de l'application et dans les contrôleurs et lui est sur le disque et je le charge/décharge directement depuis mes contrôleurs quand j'en ai besoin, les valeurs étant uniques par contrôleur et AppDelegate ne s'en sert qu'au lancement. L'adresse du fichier étant dans les UserDefaults. La dernière partie (tableaux + dictionnaire) est appelée à  changer pour un SharedCollections qui récupérera tout ça, disons que ça remonte à  des expérimentations à  mes et débuts et comme ça marche comme ça depuis un bon moment je n'éprouve pas d'urgence à  m'y coller ;)




  • - quel intérêt et que dommage de continuer à  fonctionner avec des clés lors de tes appels




    Eh bien, je fabrique certaines clés a l'exécution. J'ai une variable (int) level qui devient une clé (je la fabrique avec une NSString stringWithFormat).


     


    Comme le dictionnaire du niveau 1 a la clé "1", j'accède aux options du level 1, par exemple.


     


    Il me faudrait un truc du style [JumpTable sharedInstance].optionsForLevel: (int)level...



  •  ça marche comme ça depuis un bon moment




    Ah oui, j'ai oublié de dire que chez moi aussi ça marche parfaitement. Simplement je suis sûr qu'il existe une façon plus élégante de faire -- et ici, élégance veut dire "si dans deux mois je veux reprendre tout ça et ajouter des niveaux, j'ai intérêt à  comprendre ce-que-j'ai-fabriqué-dans-un-éclair-de-génie-qui-a-disparu-depuis"...

  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #14
    Je trouve cela fou que tant de gens utilisent l'AppDelegate comme lieu fourre-tout.

    L'AppDelegate, c'est comme son nom l'indique, le delegate de l'application. A la base il ne sert qu'à  ça. Vous avez une UIApplication, et il se trouve que cet objet UIApplication a un delegate, tout comme une UITableView a un delegate et un datasource.

    Alors certes, UIApplication est un singleton (vous avez une sharedInstance qui représente votre application courante), donc il n'y a qu'une seule UIApplication par appli, et forcément donc l'objet qui est associé en tant que delegate de cette unique UIApplication est lui aussi unique (à  proprement parler, l'AppDelegate n'est pas un singleton, c'est un objet qu'on a attribué à  une propriété (delegate) d'un singleton, déjà ). Et c'est (malheureusement) pour cela (= le fait que l'AppDelegate soit unique) qu'il est utilisé à  mauvais escient pour y foutre un peu tout et n'importe quoi.

    Mais son but à  cet objet AppDelegate, c'est d'être le délégué de votre UIApplication, donc d'implémenter les méthodes de UIApplicationDelegate, pour dire à  votre application comment répondre à  des évènements comme "voilà  ce que tu dois faire une fois que tu as fini de te lancer" ou "exécute ce code quand tu reçois une notification Push" etc. C'est pas vraiment de gérer des constantes ou des trucs globaux...

    C'est comme si vous implémentiez du code dans une classe qui n'a rien à  voir, tout ça parce que cette classe vous l'avez sous la main donc ça vous arrange. Non, si vous avez besoin d'une classe qui fournit des constantes, autant créer un singleton dédié à  cet usage, plutôt que de mettre ces constantes dans le delegate de l'application tout ça parce que l'application est un singleton qui existe déjà  et son delegate est donc forcément unique... un peu tordu non ?

    D'ailleurs, moi je ne suis pas pour une classe dédiée aux constantes. Je suis plutôt d'avis de faire un découpage fonctionnel. Donc :
    • si vous avez besoin de constantes pour tout ce qui est UI, genre les couleurs de votre charte graphique, les polices utilisées... Bah vous faites une classe UITheme ou GraphicalTheme ou je ne sais quoi. Vous en faites une sharedInstance (pas de raison d'en faire un singleton et d'interdire la création d'autres instances, on peut vouloir manipuler plusieurs thèmes graphiques pendant la vie de l'application, même si on a une sharedInstance qui pointe sur le thème en cours d'affichage), et vous mettez dedans tout ce qui a attrait aux outils ou constantes graphiques. Ca va des constantes de couleur d'éléments de votre UI, les polices, etc... mais pourquoi pas aussi en plus de ces constantes, des méthodes utilitaires qui sont liées à  cet aspect charte graphique (je sais pas moi, une méthode qui calcule automatiquement la variante "sélectionnée" d'une couleur de votre thème, en rajoutant un peu de luminosité dessus ou je ne sais quoi...)
    • A côté de ça, si vous avez besoin de constantes pour tout ce qui est réseau, genre une liste d'URLs de vos WebServices par exemple, ça n'a pas de sens de les mettre avec les constantes de vos couleurs et thèmes graphiques. Ca a plus de sens de créer une classe NetworkConfiguration, là  encore vous en faites une sharedInstance, et dedans vous mettez les accesseurs vers toutes les constantes d'URLs. Et pourquoi pas des méthodes pour retourner la reachability du réseau (réseau down, ou cellulaire, ou Wifi...) ou autres méthodes utilitaires, en plus de ces constantes.
    Bref, organiser ses blocs par fonctionnalités, et faire des sharedInstance par découpage fonctionnel plutôt que de tout mettre en vrac dans un gros fichier de constantes fourre-tout, ou encore pire de les mettre dans le AppDelegate alors qu'il n'a rien à  voir avec tout ça et que ce n'est pas son rôle...


    PS : Pour plus de facilité à  l'usage, moi il m'arrive de déclarer dans l'interface public les méthodes comme des méthodes de classe, comme ça je fais "NetworkConfiguration.webserviceURL" plutôt que "[NetworkConfiguration sharedInstance].webServiceURL". Même si à  l'implémentation ça demande la valeur de ma constante à  la sharedInstance par exemple !
    // .h
    @interface NetworkConfiguration : NSObject
    +(NSURL*)myWebServiceURL;
    +(NSURL*)authenticationServiceURL;
    ...
    @end
    // .m
    @interface NetworkConfiguration ()
    +(NSDictionary*)plistValues;
    @end

    @implementation NetworkConfiguration
    +(NSDictionary*)plistValues {
    static NSDictionary* plistValues;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    plistValues = [NSDictionary dictionaryWithContentsOfFile:... /* fichier PLIST de constantes */];
    });
    return plistValues;
    }
    +(NSURL*)myWebServiceURL { return [NSURL URLWithString: self.plistValues[@ServiceURL] ]; }
    +(NSURL*)authenticationServiceURL { return [NSURL URLWithString: self.plistValues[@AuthServiceURL] ]; }
    @end
    Utilisation :
    NSURL* url = NetworkConfiguration.myWebServiceURL;
    Tout simplement.
  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #15

    Eh bien, je fabrique certaines clés a l'exécution. J'ai une variable (int) level qui devient une clé (je la fabrique avec une NSString stringWithFormat).
     
    Comme le dictionnaire du niveau 1 a la clé "1", j'accède aux options du level 1, par exemple.
     
    Il me faudrait un truc du style [JumpTable sharedInstance].optionsForLevel: (int)level...

    Rien ne t'empêche de faire ça, c'est pas dur à  coder. Tu lis toujours ton PLIST en entier au premier usage de sharedInstance comme je fais, mais tu adaptes tes accesseurs pour prendre un level en paramètre.
    Comme ça si tu as une URL qui doit avoir une valeur différente pour le level 1 ou le level 2, bah ton accesseur va retourner plistValues[@1][@monURL] ou plistValues[@2][@monURL] selon le level passé en paramètre de l'accesseur et basta.

    Ou alors si chaque "level" a des clés différentes, que tes "levels" n'ont pas tous les mêmes clés mais avec des valeurs différentes, mais sont plutôt utilisés pour hiérarchiser tes constantes :
    - Si tu veux vraiment garder ce genre de structure tu reproduis dans des objets modèle la structure de ton PLIST. Une classe qui va déclarer les @property correspondant aux constantes que tu mets dans level1, une autre qui va déclarer les property pour les constantes que tu mets dans level2... Et après ton sharedInstance "AppConfig" ne va plus exposer des accesseurs vers les constantes directement mais vers ces objets. Comme ça tu pourras faire AppConfig.level1.constante1 ou AppConfig.level2.constante42 et retrouver ta structuration comme dans ton PLIST


  •  


    berfis, on 08 Jun 2013 - 7:04 PM, said:snapback.png




    @colas: pas question de dériver NSDictionary   :*  Tu l'as déjà  fait?




     


    Y'a-t-il un problème à  faire cela ?




     


    Oui, y a un problème car NSDictionary est une "Class Cluster" !


     


    http://stackoverflow.com/questions/4789852/subclassing-nsmutabledictionary

  • @Aligator : on multiplie les classes? C'est peut-être pour ça qu'on bourre tout dans le AppDelegate. Note que Apple ne nous en dissuade pas vraiment en fourrant son code par défaut... dans l'AppDelegate.


     


    C'est vrai aussi que j'ai pris de mauvaises habitudes avec l'AppDelegate en codant une année entière avec ApplescriptObjC, qui lui aussi flanque tout dans le délégué. On s'y accroche (je suis conscient que c'est un tort) mais là  j'avoue que je suis un peu de retour à  la case départ avec ma sharedInstance. J'obtiens juste erreur sur erreur à  la compilation, ce n'est pas juste un transfert de compétence de l'AppDelegate à  un autre objet, c'est une tout autre approche. Je ne saisis pas encore très bien (ce qui ne veut pas dire que j'ai envie de faire machine arrière...)




  • Oui, y a un problème car NSDictionary est une "Class Cluster" !


     


    http://stackoverflow.com/questions/4789852/subclassing-nsmutabledictionary




    Et on ne peut pas vraiment dire que la doc nous dise "Allez-y les gars, NSDictionary ne demande qu'à  être dérivé". Un class cluster est opaque, et je crois qu'il y a au moins 6 méthodes à  implémenter s'il nous prenait la mauvaise idée de le faire...

  • AliGatorAliGator Membre, Modérateur

    @Aligator : on multiplie les classes? C'est peut-être pour ça qu'on bourre tout dans le AppDelegate. Note que Apple ne nous en dissuade pas vraiment en fourrant son code par défaut... dans l'AppDelegate.

     

    heu dans les templates de projet Xcode je n'ai jamais vu autre chose que les méthodes de UIApplicationDelegate (comme applicationDidFnishLaunchingWithOptions: & co) dans l'AppDelegate. Tu parles peut-être des Sample Codes d'Apple, et là  par contre oui je te crois car ces Sample Code n'ont jamais été reconnus comme étant des bonnes pratiques et souvent critiqués pour parfois montrer des trucs pas très cleans.
    Pour leur défense si je devais me faire l'avocat du diable, ces Sample Codes sont pour montrer en général quelles lignes de code écrire/enchaà®ner pour faire fonctionner un truc (dans quel ordre appeler les méthodes, a quel moment, les liens entre un truc à  passer en paramètre à  une méthode qu'on peut récupérer avec telle autre méthode...) et pas pour t'enseigner les bonnes pratiques d'architecture (bien organiser tes classes, bien découper ton code...) c'est sans doute pour cela qu'ils foutent ça "en vrac dans l'AppDelegate" parce que le but n'est pas de faire une appli propre mais de montrer vite fait des bouts de code. Mais bon quand même ils pourraient le faire proprement, je suis d'accord.


    Par contre en terme d'architecture de projet, tant dans les bonnes pratiques de la POO que dans ce qu'on voit dans l'ensemble dans la doc et les Programming Guides et ce qui est conseillé, et tant aussi qu'au niveau des bonnes pratiques en usage dans ce domaine, tout le monde va dans le sens d'inciter à  faire plus de classes. Enfin peut-être pas a ce point là , mais je veux dire que tout le monde est d'accord pour dire qu'il faut mieux découper proprement ton code et avoir une archi claire et des classes dédiées, quitte a avoir beaucoup de fichiers .h/.m, plutôt que de tout mettre en vrac dans un gros fichiers de plusieurs milliers de ligne tout ça pour "éviter d'avoir à  créer une classe" ou pour "éviter d'avoir trop de fichiers source"
    Je ne pense pas que mettre toutes tes affaires en vrac dans un carton géant soit une bonne idée plutôt que de ranger tes affaires dans des cartons séparés par thème pour mieux t'y retrouver quand tu chercheras quelquechose, même si ça fait + de cartons... ;)
  • berfisberfis Membre
    juin 2013 modifié #20

    Mais là , avec ma plist, tu es en train de parler d'une boà®te pour les crayons rouges, une boà®te pour les crayons rouges avec une mine, et une boà®te pour les crayons rouges avec une mine bien taillée... non? Imaginons que j'aie cette structure


    coordonnéesType1 (array)


      topLeft 20, 100


      bottomLeft 20, 20


      topRight 100, 20


      bottomRight 100, 100


     


    coordonnéesType2 (array)


      topLeft 20, 80


      bottomLeft 20, 20


      topRight 80, 20


      bottomRight 80, 80


    ...


    image1 (dictionary)


      coordonnées coordonnéesType1


      couleurDeFond couleurNeutre


    ...


    collection1 (array)


      image1


      image2


     


    Je devrais pouvoir accéder à  collection1.image1.coordonnees.topRight.x idéalement de n'importe où. Actuellement j'y arrive via le dictionnaire (flanqué dans l'AppDelegate, évidemment) et un valueForKeyPath. C'est pratique et c'est moche, je suis d'accord que ça revient à  fouiller dans un grand carton avec un chalumeau pour m'éclairer. Je ne répugne pas à  créer des classes (j'en ai déjà  une douzaine dans l'application, dont certaines héritent d'autres) mais je ne vois toujours pas, malgré tes exemples et explications, comment faire pratiquement. Je dois être complètement bouché...


     


    Bon, une heure après, je commence à  me déboucher... C'est vrai que c'est une façon de faire qui est plus propre. Comme ma classe JumpTable ne dépend de rien, je suis en train de liquider un nombre conséquent de propriétés passées comme paramètres (une horreur, de toute façon)... <_<


  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #21
    En fait ton souci est sans doute de ne pas réussir à  dissocier dans ton esprit l'organisation de ton PLIST (sa représentation pour stockage sur disque, sa serialisation) et l'organisation de tes données elles mêmes.


    Là  si je reprend ton exemple, pourquoi tu n'as pas une classe ImageInfo avec une propriété de type CGRect pour les coordonnées, et une propriété UIColor, et une classe ImageCollection qui contient un NSArray pour stocker ta collection d'objets ImageInfo et qui saurait s'initialiser à  partir de ton PLIST ?


    Après ta classe ImageCollection serait une sharedInstance, ici va lire le PLIST, créer les objets ImageInfo qui vont bien avec leurs sous-objets ImageCoordinates tout ça a partir du PLIST lu. Et a l'usage tu pourras faire un macollection.images[0].coordinates pour avoir le CGRect représentant les coordonnées de la première image de ta collection (et avec des fonctions comme CGRectGetMinX, CGRectGetMaxY, CGRectGetWidth etc tu as tout ce qu'il faut déjà  pour manipuler tout ça en plus)


    C'est pas parce que ton PLIST est organisé en clés et structuré d'une certaine façon que les classes et méthodes pour récupérer ces données doivent absolument suivre la même organisation. C'est pas parce que tu as un PLIST d'un côté qu'il faut absolument passer par des noms de clés et des valueForKeyPath pour accéder à  la constante que tu veux. Le PLIST n'est qu'un moyen comme un autre de stocker des données.

    Si un jour pour une raison X ou Y tu veux ou dois changer le format de ce fichier pour passer d'un PLIST à  un XML ou à  un format binaire ou à  un fichier texte ou même des fichiers séparés, il faut pas que ça te fasse tout changer. Si tu as bien pensé les choses ton code est indépendant du format de fichier que tu utilises derrière pour stocker tes constantes et tu n'auras pas à  changer le code qui appelle et utilise ces constantes. Tu auras juste à  adapter le code qui lit le fichier et construit les objets ImageInfo a partir de ça, mais tu utiliseras dans le reste du code les objets ImageInfo qu'ils aient été alimentés par un PLIST un XMl ou autre.
  • Le sharedInstance qui lit le fichier de données peut-il lui même instancier d'autres sharedInstances?


  • AliGatorAliGator Membre, Modérateur
    Pourquoi il instancierait absolument des sharedInstances ? Pour quoi faire ? De simples objets suffisent.
  • berfisberfis Membre
    juin 2013 modifié #24

    Donc l'idée c'est que chaque sharedInstance relit tout le plist pour en extraire les renseignements qui la concernent? En fait, puisque je ne peux pas me servir d'un modèle Core Data (voir http://forum.cocoacafe.fr/topic/11075-rajouter-un-modèle-core-data-après-coup/), c'est à  moi de faire son boulot, recréer des objets à  partir d'un modèle statique? Intéressant...


     


    J'ai un autre petit souci. Je déclare une deuxième classe sur le modèle que tu m'as donné, et je l'appelle Player (oui c'est un jeu). Mon idée c'est de regrouper dans cette classe toutes les propriétés du joueur (score, durée de vie etc.) qui se trouvaient précédemment dans l'AppDelegate...


     


    Maintenant j'aimerais lier ces propriétés à  certains éléments d'interface. Je décide donc, comme pour le Shared User Defaults, de créer un cube bleu "Player" dans le nib, et de lier ces propriétés. So far, so good, IB reconnait ma classe, liste les propriétés disponibles, et donc je lie.


     


    Et là , bizarre, pas de mise à  jour des propriétés (un NSLog me donne pourtant des valeurs correctes). Pourtant, si j'ai bien compris tes explications, au réveil du nib, mon cube va être instancié une unique fois, avant même que je tripote ses propriétés. Que j'y fasse référence ou que mon nib y fasse référence, cocoa va créer ma sharedinstance unique, même si je colle un cube bleu dans chaque nib, non?


     


    Notes: les propriétés dans Player sont identiques à  celles définies dans l'AppDelegate (et qui fonctionnaient). Elles ont les attributs par défaut donnés par ARC (strong, etc). Le binding a les attributs "Continuously Update Value" et "Validates Immediately"...


  • AliGatorAliGator Membre, Modérateur

    Donc l'idée c'est que chaque sharedInstance relit tout le plist pour en extraire les renseignements qui la concernent?

    ah non ! Il va y avoir une seule sharedInstance qui va lire le PLIST et créer ensuite les sous-objets et tu vas accéder a ces sous-objets via la sharedInstance. C'est un peu comme la UIApplication qui est unique et qui a un delegate, ta classe AppDelegate n'est pas un singleton ou une sharedInstance, mais tu n'as qu'un seul AppDelegate par application car UIApplication est un singleton. Si ça t'amusais rie ne t'empêcherait dans ton code d'écrire quelque part [[AppDelegate alloc] init] pour en créer une autre instance, l'AppDelegate n'est pas un singleton en soi, il n'a pas non plus de méthode sharedInstance... c'est seulement qu'en pratique il y en a un qui est associé à  une propriété (delegate) du si flétan UIApplication donc ce AppDelegate là  est facilement accessible de partout.


    Bah pour ton cas c'est pareil, tu peux avoir une classe AppConstants qui a lire le PLIST, créer des objets en conséquence représentant tes infos dans le PLIST (ImageInfo, etc) et tout mas ces dernières ne seront pas une sharedInstance.

    En fait dans l'absolu il te faut même oublier ce concept de sharedInstance un premier temps. Tu t'en fiches de tout ça pour l'instant, il faut déjà  que tes classes marchent bien ensemble. SharedInstance ou pas c'est pas la question, déjà  il te faut une classe à  qui tu fournis un chemin vers un fichier PLIST et qui sache le lire et créer des objets ImageInfo a partir des données qu'il lit etc. Autrement dit, désérialiser les données du PLIST pour les transformer en une représentation dans ton modèle objet (des objets ImageInfo, UIColor, des CGRect...)

    Limite cette classe que tu fais qui va lire le PLIST et créer la structure correspondante qui va bien en objets, il ne faut pas forcément pour l'instant que tu l'imagines en en sharedInstance, non, rien ne t'empêche d'imaginer que tu vas pouvoir en créer plusieurs, en leur passant chacun un PLIST différent. Pour l'instant tu fais une classe generique, qui crée tes objets quand tu lui files un PLIST quelconque, et tu pourrais imaginer vouloir en instancier 2, avec AppConfig* config1 = [AppConfig configurationWithPLIST:cheminVersPLIST1] et AppConfig* config2 = [AppConfig configurationWithPLIST:cheminVersPLIST2] pourquoi pas, faut pas que ton code soit restrictif.

    C'est seulement après que tu vas dire "Bon j'ai une classe qui fat le boulot de me représenter une configuration et qui sait la créer en lisant un PLIST, c'est bien beau mas il se trouve que j'ai quad même u objet AppConfig un le particulier qui m'intéresse dans tout ça, c'est celui créé à  partir du PLIST un peu spécial "myconfig.plist" que j'ai dans mon mainBundle" et là  du coup tu vas ajouter une méthode "sharedInstance" a ta classe AppConfig pour pouvoir accéder à  cet objet AppConfig depuis n'importe où et ne le créer qu'une fois lors du premier accès pour le garder de côté pour les suivantes. Mais faire en sorte que ta classe AppConfig fournisse une sharedInstance, ala limite c'est accessoire. Il faut que ta classe fonctionne toute seule d'abord, sans avoir besoin de cette méthode sharedInstance ;)



     

    DoncEt là , bizarre, pas de mise à  jour des propriétés (un NSLog me donne pourtant des valeurs correctes). Pourtant, si j'ai bien compris tes explications, au réveil du nib, mon cube va être instancié une unique fois, avant même que je tripote ses propriétés. Que j'y fasse référence ou que mon nib y fasse référence, cocoa va créer ma sharedinstance unique, même si je colle un cube bleu dans chaque nib, non?

    Alors pas tout a faitnon. C'est la différence entre une sharedInstance et un singleton.


    - Un singleton c'est une classe qui fait en sorte que même si on essaye d'allouer une nouvelle instance (avec alloc/init etc) ça retourne a chaque fois immanquablement la même unique instance. Il est impossible d'en créer une autre.

    - Une sharedInstance est une classe comme une autre. Tu peux créer autant d'instance que tu veux. C'est juste que dans le lot, tu fournis une méthode "sharedInstance" qui va te retourner une instance particulière (qui va créer cette instance la première fois et te la retourner ensuite à  chaque appel suivant à  sharedInstance plutôt que de t'en fournir une différente). Mais ça ne t'empêche pas d'appeler alloc/init pour créer une instance completement à  part. (Un exemple dans Cocoa, la classe NSFileManager, où tu as une sharedInstance mais peut aussi créer d'autres instances avec alloc/init)


    En général, il n'y a aucune justification à  limiter les possibilités de ta classe et à  stricto sensu interdire la création d'instances différentes. Le but en général c'est juste d'avoir une instance un peu particulière que tu veux pouvoir accéder depuis n'importe où. Mais ça ne justifie pas d'interdire d'en créer d'autres. Si un jour, en plus de ton AppConfig principal, représentant les données que tu as dans config.plist dans ton Bundle, tu voulais instancier un autre objet AppConfig, se basant sr un autre PLIST (par exemple pour gérer d'autres configurations et permettre à  l'utilisateur de choisir entre plusieurs configs, etc) y'a aucune raison de l'interdire formellement.


    Pour en revenir à  XIB, quand tu mets un objet dans le XIB (une UIView ou un petit cube bleu, c'est pareil dans les 2 cas) le XIB va instancier ces objets quand il va être desarchivé... c'est à  dire qu'il va faire un alloc/init sur ces objets. Tu auras donc compris que ton objet Player va être crée avec un [[Player alloc] init] donc une nouvelle instance à  chaque fois, plutôt que ton cube bleu représente ta sharedInstance. Si tu voulais que le XIB, en faisant alloc/init sur la classe Player, te retourne toujours le même, il te faudrait un singleton (mais est-ce justifié, en général pas vraiment). Si t voulais passer la sharedInstance au XIB plutôt qu'il crée une nouvelle instance indépendante à  chaque fois, tu as les Proxy Objects pour ça dans les XIB. Ce sont les petits cubes transparents représentant des objets non pas que le XIB va instancier, mais des objets déjà  crées que tu vas passer au XIB lors de son desarchivage. "File's Owner" en étant un d'ailleurs, mais tu peux en placer d'autres dans ton XIB, faudra par contre les passer dans les options dans la méthode qui te sert à  desarchiver ton XIB, genre "loadNibNamed:owner:options:"

    (Je sens la question venir du "Oui mais si mon XIB c'est pas moi qui le desarchive explicitement, mais que je passe par un UIViewController et "initWithNibName:bundle:" où est-ce que je passe on ProxyObject [Player sharedInstance] au XIB ?" Et hé j'ai pas encore réfléchi à  la solution ^^)
  • berfisberfis Membre
    juin 2013 modifié #26

    En attendant je vais essayer le binding par code... on va bien voir.


     


    Ouais ça marche... bof c'est laid le binding par code. On se croirait sous iOS.


     


    Hé, j'ai fait quelque chose d'encore plus tordu. J'ai déclaré une propriété thePlayer dans mon AppDelegate, je la set à  [Player sharedInstance] dans le awakeFromNib, et je binde sur AppDelegate.thePlayer...


    ça marche... mais est-ce bien raisonnable?


  • AliGatorAliGator Membre, Modérateur
    juin 2013 modifié #27
    Bah franchement, c'est pas si crade. C'est peut-être pas idéal mais c'est le moins bordélique par rapport à  tout mettre dans l'AppDelegate en boxon comme avant. C'est donc déjà  un bon pas en avant.


    Je préfère largement que tu me dises "J'ai fait une classe bien propre nickel qui organise proprement mes données et j'ai découpé ça proprement avec des classes dédiées aux propriétés dédiées plutôt qu'avec des accès via des clés" et "J'ai rajouté une sharedInstance a cette classe car j'ai une instance de ça dont j'ai besoin partout... mais pour pouvoir accéder a cette sharedInstance via mes XIB j'ai dû l'affecter aussi à  une propriété de mon AppDelegate", plutot que tu dises "Bon j'ai tout mis en vrac dans l'AppDelegate" et "J'accède a mes données par des clés partout plutôt qu'avec des @property"


    Car au moins ta structuration de données est propre, tu as organisé ça en classes bien découpées et pas tout en vrac... et certes après tu as mis ta sharedInstance accessible via une propriété de ton AppDelegate pour y accéder par ton XIB mais au moins c'est moins le bordel en vrac.
  • Merci Ali pour ta patience et ta compétence-- je vais la mettre à  l'épreuve dans un autre sujet (sur les notifications) parce que là  je considère que le sujet est bien exploré!


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