Best Practice pour l'init d'un widget (classe dérivée de UIView)
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 ?
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 /> self = [super initWithFrame:frame];<br /> if (self != nil) {<br /> [self initVariables];<br /> }<br /> return self;<br />}<br />-(id)initWithCoder:(NSCoder*)aDecoder<br />{<br /> self = [super initWithCoder:aDecoder];<br /> if (self != nil) {<br /> [self initVariables];<br /> }<br /> return self;<br />}<br /><br />-(void)initVariables<br />{<br /> // ... mes inits à moi (valeurs par défaut des variables d'instances, etc.) ici<br />}<br />@end
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Oui, ça me paraà®t assez propre, peut-être le plus simple ? (pas besoin de la déclarer dans le .h du coup 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 ?
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à ?
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
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.
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 ? 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...
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...
Et... je n'ai pas de solution.
Donc il y a forcément une solution. CQFD
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.
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.
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.
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 ?
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.
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
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é.
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 ;-)
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
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.
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.
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à