D'accord, il y a des dizaines de billets sur StackOverflow à ce sujet, mais aucun n'est particulièrement clair sur la solution. J'aimerais créer un UIView
personnalisé avec un fichier xib qui l'accompagne. Les exigences sont:
UIViewController
- une classe complètement autonomeMon approche actuelle à cet égard est la suivante:
Remplacer -(id)initWithFrame:
-(id)initWithFrame:(CGRect)frame {
self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil] objectAtIndex:0];
self.frame = frame;
return self;
}
Instanciez par programme à l'aide de -(id)initWithFrame:
dans mon contrôleur de vue
MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
[self.view insertSubview:myCustomView atIndex:0];
Cela fonctionne bien (bien que ne jamais appeler [super init]
Et simplement définir l'objet à l'aide du contenu de la pointe chargée semble un peu suspect - il y a un conseil ici pour ajoutez une sous-vue dans ce cas qui également fonctionne bien). Cependant, j'aimerais pouvoir également instancier la vue à partir du storyboard. Donc je peux:
UIView
sur une vue parent dans le storyboardMyCustomView
Remplacer -(id)initWithCoder:
- le code que j'ai vu le plus souvent correspond à un modèle tel que celui-ci:
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self initializeSubviews];
}
return self;
}
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initializeSubviews];
}
return self;
}
-(void)initializeSubviews {
typeof(view) view = [[[NSBundle mainBundle]
loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil] objectAtIndex:0];
[self addSubview:view];
}
Bien sûr, cela ne fonctionne pas, que ce soit si j'utilise l'approche ci-dessus ou si j'instancie par programme, les deux finissent par appeler récursivement -(id)initWithCoder:
en entrant -(void)initializeSubviews
et en chargeant le nib à partir d'un fichier. .
Plusieurs autres SO questions traitent de) telles que ici , ici , ici et ici . Cependant, aucune des réponses données ne résout le problème de façon satisfaisante:
Quelqu'un pourrait-il donner des conseils sur la façon de résoudre ce problème et obtenir des prises de travail dans un UIView
personnalisé avec un encombrement minimal/sans encapsulation de contrôleur mince? Ou existe-t-il une autre façon de procéder, plus propre, avec un code passe-partout minimum?
Votre problème appelle loadNibNamed:
de (un descendant de) initWithCoder:
. loadNibNamed:
appelle en interne initWithCoder:
. Si vous souhaitez remplacer le codeur du storyboard et toujours charger votre implémentation xib, je suggère la technique suivante. Ajoutez une propriété à votre classe de vue et, dans le fichier xib, définissez-la sur une valeur prédéterminée (dans Attributs d'exécution définis par l'utilisateur). Maintenant, après avoir appelé [super initWithCoder:aDecoder];
vérifier la valeur de la propriété. Si c'est la valeur prédéterminée, n'appelez pas [self initializeSubviews];
.
Donc, quelque chose comme ça:
-(instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self && self._xibProperty != 666)
{
//We are in the storyboard code path. Initialize from the xib.
self = [self initializeSubviews];
//Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
//self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
}
return self;
}
-(instancetype)initializeSubviews {
id view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
return view;
}
Notez que ce QA (comme beaucoup) est vraiment juste d'intérêt historique.
Aujourd'hui Depuis des années et des années dans iOS, tout n’est qu’une vue conteneur. Tutoriel complet ici
(En effet Apple a finalement ajouté Références de Storyboard , il y a quelque temps maintenant, ce qui facilite grandement les choses.)
Voici un storyboard typique avec des vues de conteneur partout. Tout est une vue de conteneur. C'est juste comment vous créez des applications.
(Par curiosité, la réponse de KenC montre exactement comment, il était habituel de charger un xib dans une sorte de vue wrapper, car vous ne pouvez pas vraiment "assigner à soi-même".)
J'ajoute cela dans un article séparé pour mettre à jour la situation avec la sortie de Swift. L'approche décrite par LeoNatan fonctionne parfaitement dans Objective-C. Cependant, les contrôles de temps de compilation plus stricts empêchent que self
soit assigné lors du chargement depuis le fichier xib dans Swift.
En conséquence, il n'y a pas d'autre choix que d'ajouter la vue chargée à partir du fichier xib en tant que sous-vue de la sous-classe UIView personnalisée, plutôt que de se remplacer entièrement par self. Ceci est analogue à la deuxième approche décrite dans la question initiale. Voici un aperçu approximatif d’une classe dans Swift):
@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
// below doesn't work as returned class name is normally in project module scope
/*let viewName = NSStringFromClass(self.classForCoder)*/
let viewName = "ExampleView"
let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
owner: self, options: nil)[0] as! UIView
self.addSubview(view)
view.frame = self.bounds
}
}
L'inconvénient de cette approche est l'introduction d'une couche redondante supplémentaire dans la hiérarchie des vues, qui n'existe pas lorsque vous utilisez l'approche décrite par LeoNatan dans Objective-C. Cependant, cela pourrait être considéré comme un mal nécessaire et un produit de la manière fondamentale dont les choses sont conçues dans Xcode (il m’est toujours fou de penser qu’il est si difficile de lier une classe UIView personnalisée à une présentation d’UI de manière cohérente). à la fois sur les storyboards et dans le code) - remplacer self
en gros dans l’initialiseur n’était jamais apparu comme une façon de faire les choses particulièrement interprétable, bien qu’avoir essentiellement deux classes de vues par vue ne semble pas aussi génial.
Néanmoins, un résultat heureux de cette approche est qu'il n'est plus nécessaire de définir la classe personnalisée de la vue dans notre fichier de classe dans le générateur d'interface pour garantir le comportement correct lors de l'attribution à self
, et donc à l'appel récursif à init(coder aDecoder: NSCoder)
lors de l'émission de loadNibNamed()
est cassé (en ne définissant pas la classe personnalisée dans le fichier xib, la init(coder aDecoder: NSCoder)
de Plain Vanilla UIView plutôt que notre version personnalisée sera appelée).
Même si nous ne pouvons pas personnaliser directement les classes de la vue stockée dans xib, nous pouvons toujours lier la vue à notre sous-classe UIView 'parent' à l'aide de outlets/actions, etc. après avoir défini le propriétaire du fichier de la vue sur notre classe personnalisée:
Vous pouvez trouver une vidéo montrant étape par étape la mise en œuvre d'une telle classe de vues utilisant cette approche dans la vidéo suivante .
self
de StoryboardRemplacement de self
dans initWithCoder:
La méthode échouera avec l'erreur suivante.
'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Au lieu de cela, vous pouvez remplacer l'objet décodé par awakeAfterUsingCoder:
_ (pas awakeFromNib
). comme:
@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
@end
Bien sûr, cela cause également un problème d’appel récursif. (décodage du storyboard -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
Vous devez donc vérifier le courant awakeAfterUsingCoder:
est appelé dans le processus de décodage Storyboard ou XIB. Vous avez plusieurs façons de le faire:
@property
qui est défini dans NIB uniquement.@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
et définissez "Attributs d'exécution définis par l'utilisateur" uniquement dans "MyCustomView.xib".
Avantages:
Les inconvénients:
setXib:
sera appelé APRÈS awakeAfterUsingCoder:
self
a des sous-vuesNormalement, vous avez des sous-vues dans xib, mais pas dans le storyboard.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(self.subviews.count > 0) {
// loading xib
return self;
}
else {
// loading storyboard
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
}
Avantages:
Les inconvénients:
loadNibNamed:
appelstatic BOOL _loadingXib = NO;
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
if(_loadingXib) {
// xib
return self;
}
else {
// storyboard
_loadingXib = YES;
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
_loadingXib = NO;
return view;
}
}
Avantages:
Les inconvénients:
Par exemple, déclarez _NIB_MyCustomView
en tant que sous-classe de MyCustomView
. Et utilise _NIB_MyCustomView
au lieu de MyCustomView
dans votre XIB uniquement.
MyCustomView.h:
@interface MyCustomView : UIView
@end
MyCustomView.m:
#import "MyCustomView.h"
@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In Storyboard decoding path.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
}
@end
@interface _NIB_MyCustomView : MyCustomView
@end
@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In XIB decoding path.
// Block recursive call.
return self;
}
@end
Avantages:
if
explicite dans MyCustomView
Les inconvénients:
_NIB_
astuce dans xib Interface BuilderSemblable à d)
mais utilisez la sous-classe dans Storyboard, classe originale dans XIB.
Ici, nous déclarons MyCustomViewProto
comme une sous-classe de MyCustomView
.
@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
// In storyboard decoding
// Returns MyCustomView loaded from NIB.
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
owner:nil
options:nil] objectAtIndex:0];
}
@end
Avantages:
MyCustomView
.if
identique à d)
Les inconvénients:
Je pense e)
est la stratégie la plus sûre et la plus propre. Nous adoptons donc cela ici.
Après loadNibNamed:
dans 'awakeAfterUsingCoder:', vous devez copier plusieurs propriétés de self
, qui est une instance décodée du Storyboard. Les propriétés frame
et autolayout/autoresize sont particulièrement importantes.
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:nil
options:nil] objectAtIndex:0];
// copy layout properities.
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
// copy autolayout constraints
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in self.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == self) firstItem = view;
if(secondItem == self) secondItem = view;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
// move subviews
for(UIView *subview in self.subviews) {
[view addSubview:subview];
}
[view addConstraints:constraints];
// Copy more properties you like to expose in Storyboard.
return view;
}
Comme vous pouvez le constater, il s’agit d’un peu du code standard. Nous pouvons les implémenter en tant que "catégorie". Ici, j'étend les expressions couramment utilisées UIView+loadFromNib
code.
#import <UIKit/UIKit.h>
@interface UIView (loadFromNib)
@end
@implementation UIView (loadFromNib)
+ (id)loadFromNib {
return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
owner:nil
options:nil] objectAtIndex:0];
}
- (void)copyPropertiesFromPrototype:(UIView *)proto {
self.frame = proto.frame;
self.autoresizingMask = proto.autoresizingMask;
self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in proto.constraints) {
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
if(firstItem == proto) firstItem = self;
if(secondItem == proto) secondItem = self;
[constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant]];
}
for(UIView *subview in proto.subviews) {
[self addSubview:subview];
}
[self addConstraints:constraints];
}
En utilisant ceci, vous pouvez déclarer MyCustomViewProto
comme:
@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
MyCustomView *view = [MyCustomView loadFromNib];
[view copyPropertiesFromPrototype:self];
// copy additional properties as you like.
return view;
}
@end
XIB:
Storyboard:
Résultat:
Deux points importants:
Je suis venu plusieurs fois à cette page de questions-réponses tout en apprenant à créer des vues réutilisables. Oublier les points ci-dessus m'a fait perdre beaucoup de temps à chercher ce qui causait une récursion infinie. Ces points sont mentionnés dans d'autres réponses ici et ailleurs , mais je veux simplement les souligner à nouveau ici.
Ma réponse complète avec Swift avec étapes est ici .
Il existe une solution beaucoup plus propre que les solutions ci-dessus: https://www.youtube.com/watch?v=xP7YvdlnHfA
Pas de propriétés d'exécution, pas de problème d'appel récursif. Je l'ai essayé et cela a fonctionné comme un charme en utilisant du storyboard et de XIB avec les propriétés IBOutlet (iOS8.1, XCode6).
Bonne chance pour le codage!