Création D'une vue d'ensemble réutilisable avec xib (et chargement à partir de storyboard))
OK, il y a des douzaines de messages sur StackOverflow à ce sujet, mais aucun n'est particulièrement clair sur la solution. J'aimerais créer un fichier personnalisé UIView
accompagné d'un fichier xib. Les exigences sont les suivantes:
- Pas de
UIViewController
– un complètement autonome de la classe - sorties dans la classe pour me permettre de régler / obtenir des propriétés de la vue
Mon approche actuelle de la faire, c'est:
-
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; }
-
Instancier par programmation à l'aide de
-(id)initWithFrame:
de mon point de vue contrôleurMyCustomView *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 très bien (bien que ne jamais appeler [super init]
et le simple fait de définir l'objet en utilisant le contenu de la portée chargée semble un peu suspect – il y a des conseils ici à ajouter un sous-vue dans ce affaire qui fonctionne également très bien). Cependant, je voudrais être en mesure d'instancier la vue du storyboard aussi. Donc je peux:
- placez un
UIView
sur une vue de parent dans le storyboard - définit sa classe de coutume à
MyCustomView
-
Remplacer
-(id)initWithCoder:
– le code que j'ai vu le plus souvent correspond à un modèle tel que le suivant:-(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, car que j'utilise l'approche ci-dessus, ou que j'instancie programmatically, les deux finissent par appeler récursivement -(id)initWithCoder:
en entrant -(void)initializeSubviews
et en chargeant le nib à partir du fichier.
plusieurs autres questions ainsi traitent de ce que ici , ici , ici et ici . Cependant, aucune des réponses données de manière satisfaisante corrige le problème:
- une suggestion courante semble être d'intégrer toute la classe dans un UIViewController, et de faire le chargement nib là, mais cela me semble suboptimal car il nécessite l'ajout d'un autre fichier comme un wrapper
est-ce que quelqu'un pourrait donner des conseils sur la façon de résoudre ce problème, et obtenir des points de vente de travail dans un UIView
personnalisé avec un minimum de fuss/no thin controller wrapper? Ou y a-t-il une alternative, une manière plus propre de faire des choses avec un code minimum?
6 réponses
votre problème est d'appeler loadNibNamed:
de (un descendant de) initWithCoder:
. loadNibNamed:
appels internes initWithCoder:
. Si vous voulez Outrepasser le codeur 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 à une valeur prédéterminée (dans les attributs D'exécution définis par L'utilisateur). Maintenant, après avoir appelé [super initWithCoder:aDecoder];
, vérifiez la valeur de la propriété. Si c'est la valeur prédéterminée, ne pas appeler [self initializeSubviews];
.
ainsi, quelque chose comme ceci:
-(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 d'autres) est vraiment d'intérêt historique.
de nos jours depuis des années dans iOS tout est juste une vue de conteneur. tutoriel complet ici
(en effet Apple a finalement ajouté Storyboard References , Il ya quelque temps maintenant, ce qui le rend beaucoup plus facile.)
voici un storyboard typique avec des vues sur les conteneurs partout. Tout est une vue de conteneur. C'est juste la façon dont vous faites des apps.
(comme une curiosité, la réponse de KenC montre exactement comment, il était utilisé pour charger un xib à une sorte de vue d'emballage, puisque vous ne pouvez pas vraiment"attribuer à soi-même".)
j'ajoute ceci comme un post séparé pour mettre à jour la situation avec la sortie de Swift. L'approche décrite par LeoNatan fonctionne parfaitement dans Objective-C. Toutefois, les contrôles de temps de compilation plus stricts empêchent d'attribuer self
lors du chargement à partir du fichier xib dans Swift.
par conséquent, il n'y a pas d'autre option que d'ajouter la vue chargée à partir du fichier xib comme sous-vue de la sous-classe UIView personnalisée, plutôt que de se remplacer entièrement. Ceci est analogue à la deuxième approche, décrite dans la question d'origine. Voici un aperçu d'une classe de Swift utilisant cette approche:
@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 de la vue qui n'existe pas en utilisant L'approche décrite par LeoNatan dans L'objectif-C. Cependant, cela pourrait être considéré comme un mal nécessaire et un produit de la manière fondamentale les choses sont conçues dans Xcode (il me semble encore fou qu'il soit si difficile de relier une classe UIView personnalisée avec un layout D'UI d'une manière qui fonctionne de manière cohérente sur les deux storyboards et à partir du code) – remplacer self
wholesale dans le initialiseur avant n'a jamais semblé comme une façon particulièrement interprétable de faire les choses, bien qu'ayant essentiellement deux classes de vue par vue ne semble pas si grand non plus.
néanmoins, un heureux résultat de cette approche est que nous n'avons plus besoin de définir la classe personnalisée de la vue à notre classe file in interface builder pour assurer un comportement correct lors de l'affectation à 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, le init(coder aDecoder: NSCoder)
de simple vanilla UIView plutôt que notre version personnalisée sera appelé à la place).
même si nous ne pouvons pas faire de personnalisation de classe à la vue stockée dans le xib directement, nous sommes toujours en mesure de lier la vue à notre' parent ' sous-classe uivi utilisation de prises / actions, etc. après avoir défini le propriétaire du fichier de la vue de notre classe personnalisée:
une vidéo démontrant la mise en œuvre d'une telle classe de vue étape par étape en utilisant cette approche peut être trouvée dans la vidéo suivante .
Etape 1. Remplaçant self
par
"
remplacer self
dans initWithCoder:
méthode échouera avec l'erreur suivante.
'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
à la place, vous pouvez remplacer 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
STEP2. Prévention des appels récursifs
bien sûr, cela provoque également problème d'appel récursif. (storyboard décodage -> awakeAfterUsingCoder:
-> loadNibNamed:
-> awakeAfterUsingCoder:
-> loadNibNamed:
-> ...)
Donc, vous devez vérifier courant awakeAfterUsingCoder:
est appelé dans le processus de décodage Storyboard ou processus de décodage XIB.
Vous avez plusieurs façons de le faire:
a) utiliser Privé @property
qui est fixé en NIB seulement.
@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end
et définir" attribut Runtime défini par L'Utilisateur " uniquement dans 'MyCustomView.xib'.
Pour:
- néant
Inconvénients:
- ne fonctionne tout simplement pas:
setXib:
sera appelé aprèsawakeAfterUsingCoder:
b) vérifier si self
a des subviews
normalement, vous avez des subviews dans le 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];
}
}
Pour:
- pas de truc dans Interface Builder.
Inconvénients:
- Vous ne pouvez pas avoir les sous-vues dans votre Storyboard.
c) placer un drapeau statique pendant loadNibNamed:
call
static 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;
}
}
Pour:
- Simple
- pas de truc dans Interface Builder.
Inconvénients:
- pas sûr: drapeau partagé statique est dangereux
d) utiliser la sous-classe privée dans XIB
par exemple, déclarer _NIB_MyCustomView
comme sous-classe de MyCustomView
.
Et, utilisez _NIB_MyCustomView
au lieu de MyCustomView
dans votre XIB seulement.
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
Pour:
- Non explicite
if
dansMyCustomView
Inconvénients:
- Préfixer
_NIB_
truc en xib Interface Builder - relativement plus de codes
e) utiliser la sous-classe comme placeholder dans Storyboard
similaire à d)
mais utiliser la sous-classe dans Storyboard, classe d'origine dans XIB.
ici, nous déclarons MyCustomViewProto
en tant que 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
Pour:
- très sûr
- Propre; Pas de code supplémentaire dans
MyCustomView
. - Non explicite
if
cochez la cased)
Inconvénients:
- nécessité d'utiliser la sous-classe dans le storyboard.
je pense que e)
est la plus sûre et la plus propre stratégie. Donc on adopte ça ici.
ETAPE 3. Copier les propriétés
après loadNibNamed:
dans " awakeAfterUsingCoder:", vous devez copier plusieurs propriétés de self
qui est décodé instance f le 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;
}
SOLUTION FINALE
Comme vous pouvez le voir, c'est un peu passe-partout code. Nous pouvons les mettre en œuvre en tant que "catégorie".
Ici, j'étends le code UIView+loadFromNib
couramment utilisé.
#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:
N'oubliez pas
deux points importants:
- définit le propriétaire du fichier .xib à nom de classe de votre vue personnalisée.
- Ne pas la classe personnalisée nom de l'IB pour les .la vue racine de xib.
je suis venu à cette page de questions et réponses plusieurs fois tout en apprenant à faire des vues réutilisables. Oublier les points ci-dessus m'a fait perdre beaucoup de temps à essayer de trouver ce que était à l'origine une récursion infinie à se produire. Ces points sont mentionnés dans d'autres réponses ici et ailleurs , mais je veux juste souligner ici.
ma réponse rapide complète par étapes est ici .
il y a une solution qui est beaucoup plus propre que les solutions ci-dessus: https://www.youtube.com/watch?v=xP7YvdlnHfA
pas de propriétés Runtime, pas de problème d'appel récursif. Je l'ai essayé et il a fonctionné comme un charme en utilisant de storyboard et de XIB avec IBOutlet propriétés (iOS8.1, XCode6).
Bonne chance pour le codage!