web-dev-qa-db-fra.com

Créer un UIView réutilisable avec xib (et charger à partir du storyboard)

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:

  • Non séparé UIViewController - une classe complètement autonome
  • Points de vente dans la classe pour me permettre de définir/obtenir les propriétés de la vue

Mon approche actuelle à cet égard est la suivante:

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

  1. Placez un UIView sur une vue parent dans le storyboard
  2. Définissez sa classe personnalisée sur MyCustomView
  3. 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:

  • Une suggestion commune semble être d’intégrer la classe entière dans un UIViewController et d’y charger le nib, mais cela me semble sous-optimal car cela nécessite l’ajout d’un autre fichier juste comme un wrapper.

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?

78
Ken Chatfield

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;
}
13
Leo Natan

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.

enter image description here

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

26
Fattie

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:

Setting the file owner property of the custom view

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 .

22
Ken Chatfield

ÉTAPE 1. Remplacement de self de Storyboard

Remplacement 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

ÉTAPE 2. Prévenir les appels récursifs

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:

a) Utiliser privé @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:

  • None

Les inconvénients:

  • Tout simplement ne fonctionne pas: setXib: sera appelé APRÈS awakeAfterUsingCoder:

b) Vérifiez si self a des sous-vues

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

  • Aucune astuce dans Interface Builder.

Les inconvénients:

  • Vous ne pouvez pas avoir de sous-vues dans votre Storyboard.

c) Placez un drapeau statique pendant loadNibNamed: appel

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;
    }
}

Avantages:

  • Facile
  • Aucune astuce dans Interface Builder.

Les inconvénients:

  • Pas sûr: le drapeau partagé statique est dangereux

d) Utiliser une sous-classe privée dans XIB

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:

  • Pas de if explicite dans MyCustomView

Les inconvénients:

  • Préfixe _NIB_ astuce dans xib Interface Builder
  • relativement plus de codes

e) Utiliser la sous-classe comme espace réservé dans Storyboard

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

  • Très sécuritaire
  • Nettoyer; Pas de code supplémentaire dans MyCustomView.
  • Pas de contrôle explicite if identique à d)

Les inconvénients:

  • Besoin d'utiliser une sous-classe dans le storyboard.

Je pense e) est la stratégie la plus sûre et la plus propre. Nous adoptons donc cela ici.

ÉTAPE 3. Copier les propriétés

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;
}

SOLUTION FINALE

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:

XIB screenshot

Storyboard:

Storyboard

Résultat:

enter image description here

16
rintaro

N'oublie pas

Deux points importants:

  1. Définissez le propriétaire du fichier .xib sur le nom de classe de votre vue personnalisée.
  2. Ne pas définir le nom de la classe personnalisée dans IB pour la vue racine du fichier .xib.

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 .

13
Suragch

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!

2
ingaham