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:

  1. 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;
    }
    
  2. Instancier par programmation à l'aide de -(id)initWithFrame: de mon point de vue contrôleur

    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 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:

  1. placez un UIView sur une vue de parent dans le storyboard
  2. définit sa classe de coutume à MyCustomView
  3. 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?

76
demandé sur Community 2014-02-20 08:33:37

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;
}
12
répondu Leo Natan 2014-09-11 14:57:59

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.

enter image description here

(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".)

26
répondu Fattie 2017-05-23 12:34:19

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:

Setting the file owner property of the custom view

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 .

22
répondu Ken Chatfield 2015-11-12 02:37:27

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ès awakeAfterUsingCoder:

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 dans MyCustomView

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 case d)

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:

XIB screenshot

Storyboard:

Storyboard

résultat:

enter image description here

16
répondu rintaro 2014-09-16 04:08:32

N'oubliez pas

deux points importants:

  1. définit le propriétaire du fichier .xib à nom de classe de votre vue personnalisée.
  2. 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 .

12
répondu Suragch 2017-05-23 11:47:11

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!

2
répondu ingaham 2015-01-10 09:05:47