Best Practice pour l'init d'un widget (classe dérivée de UIView)

AliGatorAliGator Membre, Modérateur
Hello,

Je me pose souvent la question de savoir quel est le moyen le plus propre d'initialiser mes variables.
D'habitude bien sûr on fait ça dans le "init".

Mais quand je crée des contrôles personnalisés (que j'appelle des "widgets"), c'est à  dire en général des classes dérivant de UIView, ou de UIControl, ou d'un UIControl particulier genre UISlider, etc... ce n'est pas "init" qui est utilisé, mais tantôt "initWithFrame:" et tantôt "initWithCoder:" selon que le widget est initialisé par le code ou par désarchivage (d'un XIB typiquement). Du coup, où mettre mes initialisations, quand dans la plupart des cas se sont exactement les mêmes que soit soit initialisé par l'une ou l'autre des façons ?

Jusqu'à  présent, j'ai pris l'habitude d'avoir une méthode [tt]-(void)initVariables;[/tt], que je n'expose pas dans le .h (déclarée dans le .m), et que j'appelle à  la fois dans initWithFrame et initWithCoder, que je suis donc obligé de surcharger tout de même pour ça. Mais je me demandais si vous n'utilisiez pas d'autres pratiques ou façons de faire qui pourraient être plus propres, ou vos suggestions et pratiques de manière générale ?

@interface MonWidget() // Private methods as anonymous category<br />-(void)initVariables;<br />@end<br />@implementation MonWidget<br />-(id)initWithFrame:(CGRect)frame<br />{<br />&nbsp; self = [super initWithFrame:frame];<br />&nbsp; if (self != nil) {<br />&nbsp; &nbsp; [self initVariables];<br />&nbsp; }<br />&nbsp; return self;<br />}<br />-(id)initWithCoder:(NSCoder*)aDecoder<br />{<br />&nbsp; self = [super initWithCoder:aDecoder];<br />&nbsp; if (self != nil) {<br />&nbsp; &nbsp; [self initVariables];<br />&nbsp; }<br />&nbsp; return self;<br />}<br /><br />-(void)initVariables<br />{<br />&nbsp; // ... mes inits à  moi (valeurs par défaut des variables d&#39;instances, etc.) ici<br />}<br />@end
«1

Réponses

  • CéroceCéroce Membre, Modérateur
    20:16 modifié #2
    Personnellement, je crée quand même une méthode -init, que j'appelle dans l'autre méthode d'initialisation.
  • muqaddarmuqaddar Administrateur
    20:16 modifié #3
    dans 1262616868:

    Personnellement, je crée quand même une méthode -init, que j'appelle dans l'autre méthode d'initialisation.


    Oui, ça me paraà®t assez propre, peut-être le plus simple ? (pas besoin de la déclarer dans le .h du coup non ?)
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #4
    Il me semblais justement dangereux d'appeler cette méthode "init" justement, étant donné que c'est un nom réservé, non ?
    Genre peut-être que le "init" des UIViews n'est jamais appelé par le framework Apple (car c'est initWithFrame ou initWithCoder qui est appelé à  la place), mais si c'est une sous-classe de UIAutreChose, genre UISlider ou UIProgressView, ... est-ce toujours valable...?

    Bref justement j'hésite à  nommer ma méthode init plutôt que initVariables... car en effet ça m'éviterai de devoir la déclarer, mais j'ai peur des effets de bords... non ?
  • muqaddarmuqaddar Administrateur
    20:16 modifié #5
    dans 1262622431:

    Il me semblais justement dangereux d'appeler cette méthode "init" justement, étant donné que c'est un nom réservé, non ?


    Je suis aussi d'accord, il faut se méfier de la mécanique qui est derrière.

    Le but final de ton sujet c'est d'éviter une déclaration dans tes classes, tu pousses pas un peu loin la fénéantise là  ? ;)
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #6
    Peut-être, oui...
    Mais en fait non ce qui me dérange c'est pas tant de créer une méthode privée dédiée pour ça, c'est que contrairement aux autres langages, les objets en Cocoa n'ont pas de constructeur unique.
    Normalement, le constructeur, là  où l'on met toutes les initialisations, c'est dans le "init", point barre.

    Ce qui me gène c'est le fait que pour le cas des UIViews, il y ait 2 constructeurs différents, et que ça appelle l'un ou l'autre selon les cas... mais que ça n'appelle jamais un commun. A la limite, que le cas soit différentié pour l'initialisation par NSCoder (désarchivage XIB) ou par code (initWithFrame) ça me dérange pas... mais ça aurait été bien que ces méthodes appellent de toute façon au final un "init" commun toutes seules.

    Comme ça si on a du code à  mettre que pour dans un cas, genre du code qui utilise le NSCoder dans le cas du désarchivage du XIB, on le met dans le initWithCoder, etc... mais tout le code commun, on le met dans le init... comme on le fait pour toutes les autres classes habituellement, quoi !

    J'aurais donc juste espéré pouvoir implémenter une méthode "init" comme je ferais pour des classes autrs que UIView... et c'est tout. Et ne pas avoir à  surcharger initWithCoder et initWithFrame à  chaque fois pour leur demander de faire... bah rien d'autre que d'appeler "init", comme le fait Céroce.

    Ca éviterait toutes les lignes :
    - de déclaration de la méthode privée (bon ça c'est pas méchant)
    - mais aussi de surcharge de initWithCoder pour ne faire qu'appeler la méthode de super + mon init
    - et aussi de surcharge de initWithFrame pour ne faire qu'appeler la méthode de super + mon init
  • muqaddarmuqaddar Administrateur
    20:16 modifié #7
    Mais initWithFrame et initWithCoder n'appellent eux non plus aucun super constructeur commun dans leur définition ?
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #8
    Ben c'est tout l'objet de ma question, au final :P
  • CéroceCéroce Membre, Modérateur
    20:16 modifié #9
    À vrai dire, de par mon expérience personnelle, il est rare de ne pas initialiser toutes les variables d'instance dans une méthode -initWithCoder: parce que déjà , le codeur contient une bonne partie des valeurs des variables d'instance, et celles qui restent sont déduites. Certes, il y a le risque d'oubli d'une des variables, mais c'est comme d'habitude en programmation: il faut faire gaffe !

    La règle pour les initialisateurs est la suivante: il n'existe qu'un initialisateur désigné (à  bien documenter), les autres méthodes d'init y font appel. Pour une vue, on peut admettre que déclarer une méthode -init n'a pas de sens (une vue sans rectangle?), et c'est donc la méthode initWithFrame:qui est l'initialisateur désigné. De fait -initWithCoder: appellerait plutôt initWithFrame: si besoin.
  • AliGatorAliGator Membre, Modérateur
    janvier 2010 modifié #10
    Ben c'est justement ce que je trouvais manquant de ne pas avoir d'initialiseur désigné pour les UIViews.

    Je veux bien qu'on considère que initWithFrame soit l'initialiseur désigné pour les UIViews, ça semblerait en effet logique / cohérent. Bien que certains UIControls ne demandent pas de frame dans leur constructeur et qu'il faut l'affecter à  part après la création de l'objet (par exemple UIButton et son constructeur buttonWithType qui nécessite un "monBouton.frame = xx" ensuite pour affecter la frame), donc déjà  ça fait louche dans l'API, mais bon.

    Du coup c'est quand même un peu bizarre que par défaut ça ne soit pas le cas que initWithFrame soit le constructeur désigné : je veux dire par là  que si on n'implémente que du code dans initWithFrame et qu'on surcharge pas initWithCoder, le initWithFrame ne sera jamais appelé pour les UIViews créées par désarchivage d'un XIB (le initWithCoder par défaut de UIView n'appelle pas initWithFrame).

    Donc au final pour être sûr que notre objet est initialisé, on est obligé d'implémenter à  la fois initWithFrame et initWithCoder, dans tous les cas. (Et c'est vraiment ça qui me gène). Que ce soit pour y mettre un code quasi identique dans les deux, ou que ce soit pour que les deux appellent une méthode privée commune comme initVariables, ou que ce soit pour que initWithCoder appelle initWithFrame... dans tous les cas faut surcharger les deux méthodes puisque l'implémentation de 'super' (donc de ces méthodes dans la classe UIView) ne le fait pas.




    D'ailleurs comment tu ferais pour appeler initWithFrame depuis initWithCoder ?
    -(id)initWithCoder:(NSCoder*)aDecoder<br />{<br />&nbsp; self = [super initWithCoder:aDecoder]; // appel de super obligatoire ici, on va pas appeler [super initWithFrame:self.frame] ici bien sûr<br />&nbsp; if (self != nil)<br />&nbsp; {<br />&nbsp; &nbsp; self = [self initWithFrame:self.frame]; // tu ferais comme ça ??<br />&nbsp; }<br />&nbsp; return self;<br />}
    
    Si tu fais un truc comme ça, ça m'embête un peu aussi car du coup comme initWithFrame va appeler [tt][super initWithFrame:...][/tt], la logique d'initialisation ne sera pas super non plus : tu auras appelé deux initialiseurs de "super" dans le processus, initWithCoder suivi de initWithFrame, avant d'en venir à  ton propre code d'initWithFrame de ta classe. Et appeler deux fois un init, c'est pas super, comme pratique, vu qu'en général c'est prévu pour être appelé une seule fois...
  • muqaddarmuqaddar Administrateur
    20:16 modifié #11
    Dans ton exemple Ali, self.frame est déjà  connu à  ce moment-là  ? (ici c'est self.frame de la vue du xib ?)
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #12
    Oui dans mon exemple self.frame est déjà  initialisé puisqu'il a été initialisé par [tt][super initWithCoder:aDecoder][/tt] qui a désarchivé la UIView depuis le XIB (donc self.frame a déjà  comme valeur la frame de cette UIView dans le XIB à  ce moment).

    De toute façon, tu voudrais passer quoi à  la place ? CGRectZero ? Si tu fais ça tu risques de remplacer justement la frame initialisée par le initWithCoder, la frame de ta UIView dans ton XIB quoi, par une frame nulle...
  • CéroceCéroce Membre, Modérateur
    20:16 modifié #13
    dans 1262683107:

    Donc au final pour être sûr que notre objet est initialisé, on est obligé d'implémenter à  la fois initWithFrame et initWithCoder, dans tous les cas. (Et c'est vraiment ça qui me gène).

    :o J'ai enfin compris quel était vraiment le problème !
    Et... je n'ai pas de solution.

  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #14
    C'est pas possible, car comme le dit la devise Shadok de ma signature, "s'il n'y a pas de solution, c'est qu'il n'y a pas de problème".
    Donc il y a forcément une solution. CQFD :D
  • zoczoc Membre
    20:16 modifié #15
    C'est clair qu'à  partir du moment ou initWithCoder n'appelle pas l'initialiseur désigné (ce qui est le cas pour UIView en tout cas, il y a une note à  ce sujet dans la documentation de initWithFrame pour UIView), il n'y a pas vraiment de solution plus propre qu'une autre pour initialiser les membres qui ne sont pas initialisés lors du désarchivage...
  • ClicCoolClicCool Membre
    janvier 2010 modifié #16
    C'est pas plus propre de mettre toutes tes petites initialisations par défaut dans awakeFronNib et d'appeler ce awakeFronNib à  la fin du initWithFrame pour les widgets créés par code ?
  • muqaddarmuqaddar Administrateur
    20:16 modifié #17
    Pourquoi appeler cela un widget au fait ? Ce mot réprésente tellement de choses en informatique... ;)
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #18
    awakeFromNib ne fonctionne pas tout à  fait pareil sur iPhone SDK que sous le SDK OSX si j'ai bonne mémoire.
    Du coup en pratique on n'utilise jamais le awakeFromNib avec le iPhone SDK, on préfère faire ce qu'il fait dans des méthodes comme le viewDidLoad du UIViewController associé à  la UIView, par exemple.


    [EDIT] J'ai retrouvé, la différence c'est que sous iPhone OS le awakeFromNib n'est envoyé qu'aux objets instanciés par le processus de chargement du NIB, autrement dit il n'est pas envoyé au File's Owner, ni aux éventuels autres Object Proxies comme le First Responder etc.
    4. It sends an awakeFromNib message to the appropriate objects in the nib file that define the matching selector:
    • In Mac OS X, this message is sent to any interface objects that define the method. It is also sent to the File's Owner and any proxy objects that define it as well.
    • In iPhone OS, this message is sent only to the interface objects that were instantiated by the nib-loading code. It is not sent to File's Owner, First Responder, or any other proxy objects.



    En même temps le awakeFromNib, vu son nom, est tout de même fait pour l'initialisation des objets... créés par Nib et pas par le code, donc à  prendre avec des pincettes que de l'appeler nous-même par code, non ?
  • ClicCoolClicCool Membre
    20:16 modifié #19
    dans 1262702512:
    .../...
    En même temps le awakeFromNib, vu son nom, est tout de même fait pour l'initialisation des objets... créés par Nib et pas par le code, donc à  prendre avec des pincettes que de l'appeler nous-même par code, non ?

    Certes, mais c'est "moins pire" que d'appeler un autre initialiseur.
    En particuliers le awakeFromNib est à  l'appréciation seule et entière du développeur et n'est pas sensé transmettre l'appel à  la super-Classe... donc tu y fais ce que tu veux sans conflit possible d'initialisation ;)

    Bien sûr, si tu développes une classe amenée à  être distribuée et peut-être sous-classée, ça marche plus dans la mesure où justement la sous classe transmettra pas le awakeFromNib à  super.
  • Eddy58Eddy58 Membre
    20:16 modifié #20
    dans 1262702512:

    En même temps le awakeFromNib, vu son nom, est tout de même fait pour l'initialisation des objets... créés par Nib et pas par le code, donc à  prendre avec des pincettes que de l'appeler nous-même par code, non ?


    La méthode awakeFromNib est appelée dès que les objets du Nib ont tous été initialisés, donc ensuite rien n'empêche de faire ce que l'on veut en matière d'initialisation au sein de cette méthode.
  • yoannyoann Membre
    20:16 modifié #21
    Une "bonne" méthode que je vois serait de faire une sous classe abstraite de [NS|UI]View pour palier à  ce manque avec 3 méthode :

    initWithFrame:, initWithCoder: et _commonInit, l'implémentation des deux premiers ne fait rien d'autre que la procédure standard et appeler _commonInit.

    _commonInit quand à  lui ne fait rien, c'est une méthode abstraite qui sera implémenté par par les fils

    ça nécessite d'avoir une classe à  rajouter à  chaque fois mais je pense que tout le monde ici a son propre set de classe qui fait tout donc bon

    Qu'en pensez vous ?
  • zoczoc Membre
    janvier 2010 modifié #22
    dans 1262808236:

    _commonInit quand à  lui ne fait rien, c'est une méthode abstraite qui sera implémenté par par les fils

    Tss Tsss Tsss, les noms de message/variables commençant par un underscore sont réservés par Apple et ne doivent pas être utilisés par les développeurs tiers.  :D
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #23
    J'y avais bien sûr pensé à  cette solution...
    Mais elle ne marche pas vraiment quand on veut utiliser une sous-classe d'un UIControl existant.
    Car UIControl dérive de UIView, pas de YoannView. Et tous les UIControls sous-jacents (UISegmentedControl, UISlider, ...) aussi du coup bien sûr.

    Une catégorie sur UIView ce n'est pas possible, puisqu'il faut appeler les méthodes standard (celles de "super" / du vrai UIView)

    Du "method swizzeling" à  la limite pourrait faire l'affaire, en remplaçant à  la volée l'implémentation de initWithCoder et initWithFrame par nos propres méthodes qui appellent les anciennes puis appellent commonInit... Mais là  c'est un peu sortir le bazooka par contre je trouve ;)
  • yoannyoann Membre
    janvier 2010 modifié #24
    Hop je viens de faire 2/3 test et avec de l'échange de méthode ça marche, juste un fichier à  inclure (pas besoin du .h), voilà  le code :

    <br />//<br />//&nbsp; NSView-CommonInit.m<br />//&nbsp; SPView<br />//<br />//&nbsp; Created by Yoann GINI on 06/01/10.<br />//&nbsp; Copyright 2010 iNig-Services. All rights reserved.<br />//<br /><br />#import &lt;objc/runtime.h&gt;<br />#import &lt;Cocoa/Cocoa.h&gt;<br /><br />@interface NSView (CommonInit)<br /><br />@end<br /><br />@implementation NSView (CommonInit)<br /><br />-(void)_commonInit {<br />}<br /><br />-(id)ci_initWithFrame:(NSRect)frameRect {<br />	self = [self ci_initWithFrame:frameRect];<br />	if (self) {<br />		[self _commonInit];<br />	}<br />	return self;<br />}<br /><br />-(id)ci_initWithCoder:(NSCoder*)aDecoder {<br />	self = [self ci_initWithCoder:aDecoder];<br />	if (self) {<br />		[self _commonInit];<br />	}<br />	return self;<br />}<br /><br />static BOOL ci_nsview_initialized = NO;<br />+(void)initialize {<br />	[super initialize];<br />	if (ci_nsview_initialized) return;<br />	<br />	method_exchangeImplementations(class_getInstanceMethod([self class], @selector(ci_initWithFrame:)),<br />				&nbsp; &nbsp; &nbsp;  class_getInstanceMethod([self class], @selector(initWithFrame:)));<br />	<br />	method_exchangeImplementations(class_getInstanceMethod([self class], @selector(ci_initWithCoder:)),<br />				&nbsp; &nbsp; &nbsp;  class_getInstanceMethod([self class], @selector(initWithCoder:)));<br />	<br />	ci_nsview_initialized = YES;<br />}<br /><br />@end<br /><br />
    


    Rajoutez des NSLog un peut partout pour regarder, le code sera bien appeler pour tous les descendant de NSView


    Quand à  la remarque sur les méthode commençant par le underscore, pour ma part je trouve ça vraiment très pratique pour repérer les méthodes "privé" et autre truc spéciaux. C'est en effet un ancien dev d'Apple qui me l'a montré mais avec le conseil d'en abusé et non de ne surtout pas s'en servir.

    EDIT: Je viens de voir qu'il y avait un lien dans le texte de Zoc (pas super visible d'ailleurs la couleur). Ok pour la doc, pour ma part je continue à  utiliser cette méthode qui est plus que pratique pour accéder rapidement aux méthode privé.
  • zoczoc Membre
    20:16 modifié #25
    dans 1262810524:

    C'est en effet un ancien dev d'Apple qui me l'a montré mais avec le conseil d'en abusé et non de ne surtout pas s'en servir.

    Et c'est un document officiel d'Apple qui dit tout le contraire (voir lien dans mon intervention précédente)... Apple se réserve les noms commençant par underscore pour éviter les "collisions" de noms de méthodes privées entre son code et le code des autres développeurs.

  • yoannyoann Membre
    20:16 modifié #26
    dans 1262810792:

    dans 1262810524:

    C'est en effet un ancien dev d'Apple qui me l'a montré mais avec le conseil d'en abusé et non de ne surtout pas s'en servir.

    Et c'est un document officiel d'Apple qui dit tout le contraire (voir lien dans mon intervention précédente)... Apple se réserve les noms commençant par underscore pour éviter les "collisions" de noms de méthodes privées entre son code et le code des autres développeurs.


    Voir mon édit, je n'avais pas vu le lien. Soit mais vu le confort que ça apporte, tampi pour la guideline dans mes code ;-)
  • yoannyoann Membre
    20:16 modifié #27
    Bon, l'histoire des conventions de nomage étant close, vous pensez quoi du code, des critiques hormis sur le underscore ?
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #28
    Ca me parait ok comme code... A part la protection du initialize qui n'est pas complète : il faut toujours pour +initialize n'exécuter son code que si self est de la classe attendue, car si le +initialize d'une sous-classe n'est pas implémenté, et c'est le +initialize de sa superclass qui est appelé... et du coup au final ça appelle deux fois la méthode (et du coup dans ton cas ça fera un double-échange d'implémentations donc ça annulera le tout)
    Explications dans la doc de +initialize.

    Sinon, j'ai proposé le swizzling à  titre d'idée comme ça en passant... Mais est-ce une bonne idée? Car il me semblait que le Method Swizzling était deprecated (à  moins que ce soit juste la méthode poseAsClass ? Je sais plus) depuis OSX.5 ? Enfin à  valider, je sais plus.
    Doc intéressante à  lire aussi sur les principes à  respecter pour les init, constructeurs désignés, etc
  • ClicCoolClicCool Membre
    20:16 modifié #29
    Yoann avait mis un flag [tt]ci_nsview_initialized[/tt] pour éviter des multiples exécutions ;)

    dans 1262810524:
    +(void)initialize {<br />	[super initialize];<br />	if (ci_nsview_initialized) return;.../...
    



    Par contre il me semble pas qu'une méthode [tt]initialze[/tt] doive appeler [tt]super[/tt] !  :P

    Et c'est vrai que c'est plus propre de tester la classe plutôt que d'utiliser un flag.
  • yoannyoann Membre
    20:16 modifié #30
    C'est vrai que j'aurais pu tester à  la classe aussi, peut être a combiner avec le BOOL avant qui est plus rapide comme test que la comparaison de classe (voir si le changement n'a pas déjà  été fait et le faire que si on est sur la bonne classe)

    Pour l'appel du super dans initialize, ça me parait logique de le mettre dans le cas où il y ai quelque chose dans les classes parente.
  • AliGatorAliGator Membre, Modérateur
    20:16 modifié #31
    Non, il ne faut surtout pas appeler le +initialize sur super, c'est expliqué pourquoi dans le lien que j'ai fourni plus haut.

    Les +initialize sont appelés lorsque les classes sont enregistrées dans le Runtime. Or, elles s'enregistre dans l'ordre logique, c'est à  dire d'abord la superclasse avant d'enregistrer les classes dérivées (qui ont besoin que leur classe parente soit enregistrée dans le runtime avant, bien sûr). Il y a tous les détails et les subtilités de cette méthode +initialize dans la doc, faut faire gaffe y'a de quoi se faire avoir avec les cas particuliers qu'elle engendre, celle-là  ;)
Connectez-vous ou Inscrivez-vous pour répondre.