web-dev-qa-db-fra.com

Charger la vue à partir d'un fichier xib externe dans le storyboard

Je souhaite utiliser une vue sur plusieurs contrôleurs de vue dans un scénario. Ainsi, j'ai pensé à concevoir la vue dans une xib externe afin que les modifications soient reflétées dans chaque contrôleur de vue. Mais comment charger une vue depuis un xib externe dans un storyboard et est-ce même possible? Si ce n’est pas le cas, quelles autres alternatives sont disponibles pour répondre à la situation ci-dessus?

92
Sebastian Hoffmann

Mon exemple complet est ici , mais je vais en fournir un résumé ci-dessous.

Disposition

Ajoutez un fichier .Swift et un fichier .xib portant chacun le même nom à votre projet. Le fichier .xib contient votre disposition de vue personnalisée (en utilisant de préférence des contraintes de disposition automatique). 

Faites du fichier Swift le propriétaire du fichier xib.

 enter image description here Code

Ajoutez le code suivant au fichier .Swift et connectez les prises et les actions à partir du fichier .xib.

import UIKit
class ResuableCustomView: UIView {

    let nibName = "ReusableCustomView"
    var contentView: UIView?

    @IBOutlet weak var label: UILabel!
    @IBAction func buttonTap(_ sender: UIButton) {
        label.text = "Hi"
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
    }

    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

Utilise le

Utilisez votre vue personnalisée n'importe où dans votre storyboard. Ajoutez simplement une UIView et définissez le nom de la classe sur votre nom de classe personnalisé.

 enter image description here

88
Suragch

Pendant un certain temps, { l'approche de Christopher Swasey } fut la meilleure approche que j'avais trouvée. J'ai interrogé quelques-uns des développeurs expérimentés de mon équipe et l'un d'entre eux avait la solution parfaite! Cela répond à toutes les préoccupations que Christopher Swasey a abordées avec tant d'éloquence et ne nécessite pas de code de sous-classe passe-partout (ma principale préoccupation concerne son approche). Il y a un gotcha, mais à part cela, il est assez intuitif et facile à mettre en œuvre. 

  1. Créez une classe UIView personnalisée dans un fichier .Swift pour contrôler votre xib. i.e. MyCustomClass.Swift
  2. Créez un fichier .xib et donnez-lui le style souhaité. i.e. MyCustomClass.xib
  3. Définissez le File's Owner du fichier .xib sur votre classe personnalisée (MyCustomClass)
  4. GOTCHA: laissez la valeur class (sous le identity Inspector) pour votre affichage personnalisé dans le fichier .xib vierge. Votre vue personnalisée n'aura donc pas de classe spécifiée, mais aura le propriétaire du fichier spécifié.
  5. Branchez vos prises comme vous le feriez normalement avec le Assistant Editor.
    • REMARQUE: Si vous examinez le Connections Inspector, vous remarquerez que vos prises de référencement ne font pas référence à votre classe personnalisée (c'est-à-dire MyCustomClass), mais plutôt à File's Owner. Étant donné que File's Owner est votre classe personnalisée, les points de vente se connecteront et fonctionneront. 
  6. Assurez-vous que votre classe personnalisée a @IBDesignable avant l'instruction de la classe.
  7. Rendez votre classe personnalisée conforme au protocole NibLoadable référencé ci-dessous.
    • REMARQUE: Si votre nom de fichier de classe .Swift personnalisé est différent de votre nom de fichier .xib, définissez la propriété nibName sur le nom de votre fichier .xib.
  8. Implémentez required init?(coder aDecoder: NSCoder) et override init(frame: CGRect) pour appeler setupFromNib() comme dans l'exemple ci-dessous.
  9. Ajoutez un UIView au storyboard de votre choix et définissez la classe sur votre nom de classe personnalisé (c'est-à-dire MyCustomClass).
  10. Observez IBDesignable en action, car il dessine votre .xib dans le scénarimage avec émerveillement et admiration.

Voici le protocole que vous souhaitez référencer: 

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    public static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
        view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
}

Et voici un exemple de MyCustomClass qui implémente le protocole (le fichier .xib étant nommé MyCustomClass.xib):

@IBDesignable
class MyCustomClass: UIView, NibLoadable {

    @IBOutlet weak var myLabel: UILabel!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }

}

REMARQUE: Si vous manquez le Gotcha et définissez la valeur class dans votre fichier .xib sur votre classe personnalisée, il ne sera pas dessiné dans le storyboard et vous obtiendrez une erreur EXC_BAD_ACCESS lors de l'exécution de l'application car elle reste bloquée dans une infinie boucle d'essayer d'initialiser la classe à partir du nib en utilisant la méthode init?(coder aDecoder: NSCoder) qui appelle ensuite Self.nib.instantiate et appelle à nouveau init.

42
Ben Patch

En supposant que vous ayez créé un xib que vous voulez utiliser:

1) Créez une sous-classe personnalisée d’UIView (vous pouvez aller dans Fichier -> Nouveau -> Fichier ... -> Classe de cacao tactile. Assurez-vous que la "sous-classe de:" est "UIView").

2) Ajoutez une vue basée sur xib en tant que sous-vue à cette vue lors de l'initialisation.

En Obj-C

-(id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
                                                              owner:self
                                                            options:nil] objectAtIndex:0];
        xibView.frame = self.bounds;
        xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [self addSubview: xibView];
    }
    return self;
}

Dans Swift 2

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    self.addSubview(xibView)
}

Dans Swift 3

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    self.addSubview(xibView)
}

3) Où que vous souhaitiez l'utiliser dans votre scénario, ajoutez un UIView comme vous le feriez normalement, sélectionnez la vue nouvellement ajoutée, accédez à l'Inspecteur de l'identité (la troisième icône en haut à droite qui ressemble à un rectangle avec des lignes), et entrez le nom de votre sous-classe dans "Classe" sous "Classe personnalisée".

28
user1021430

J'ai toujours trouvé la solution "Ajouter comme une sous-vue" peu satisfaisante, car elle se visse avec (1) autolayout, (2) @IBInspectable et (3) prises. Laissez-moi plutôt vous présenter la magie de awakeAfter:, une méthode NSObject.

awakeAfter vous permet d'échanger l'objet réellement réveillé d'un NIB/Storyboard avec un autre objet. Cet objet est ensuite soumis au processus d'hydratation, a été invoqué par awakeFromNib, est ajouté en tant que vue, etc. 

Nous pouvons l'utiliser dans une sous-classe "découpe de carton" de notre vue, dont le seul but sera de charger la vue à partir de la NIB et de la renvoyer pour utilisation dans le Storyboard. La sous-classe incorporable est ensuite spécifiée dans l'inspecteur d'identité de la vue Storyboard, plutôt que dans la classe d'origine. Pour que cela fonctionne, il n'est pas nécessaire qu'il s'agisse d'une sous-classe. Toutefois, en faire une sous-classe est ce qui permet à IB de voir toutes les propriétés IBInspectable/IBOutlet.

Remarque: la classe définie dans la vue dans le fichier NIB reste la même. La sous-classe incorporable est uniquement utilisée dans le storyboard. La sous-classe ne peut pas être utilisée pour instancier la vue dans le code, elle ne devrait donc pas avoir de logique supplémentaire. Il devrait contenir uniquement le crochet awakeAfter.

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
  }
}

L'inconvénient majeur est que, si vous définissez des contraintes de largeur, de hauteur ou de format d'image dans le storyboard qui ne sont pas liées à une autre vue, elles doivent être copiées manuellement. Les contraintes qui associent deux vues sont installées sur l'ancêtre commun le plus proche et les vues sont réveillées du storyboard de l'intérieur vers l'extérieur. Ainsi, au moment où ces contraintes sont hydratées dans la vue d'ensemble, l'échange a déjà eu lieu. Les contraintes qui impliquent uniquement la vue en question sont installées directement sur cette vue et sont donc renvoyées lorsque l'échange est effectué, à moins qu'elles ne soient copiées.

Notez que ce qui se passe ici, c'est que les contraintes installées sur la vue dans le storyboard sont copiées dans la vue nouvellement instanciée, qui peut déjà avoir ses propres contraintes, définies dans son fichier nib. Ceux-ci ne sont pas affectés.

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!

    for constraint in constraints {
      if constraint.secondItem != nil {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
      } else {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
      }
    }

    return newView as Any
  }
}  

instantiateViewFromNib est une extension de type sécurisé à UIView. Tout ce qu'il fait est de parcourir les objets de la carte réseau jusqu'à ce qu'il en trouve un qui correspond au type. Notez que le type générique est la valeur return, le type doit donc être spécifié sur le site de l'appel.

extension UIView {
  public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
    if let objects = bundle.loadNibNamed(nibName, owner: nil) {
      for object in objects {
        if let object = object as? T {
          return object
        }
      }
    }

    return nil
  }
}
22
Christopher Swasey

Je pense à alternative pour utiliser XIB views à utiliser View Controller dans un storyboard séparé

Ensuite, dans le storyboard principal au lieu de la vue personnalisée, utilisez container view avec Embed Segue et ayez StoryboardReference dans ce contrôleur de vue personnalisé quelle vue doit être placée dans une autre vue du scénario principal. 

Ensuite, nous pouvons configurer la délégation et la communication entre ce ViewController intégré et le contrôleur de la vue principale via prepare for segue . Cette approche est différente puis l’affichage de UIView, mais beaucoup plus simple et plus efficace (du point de vue de la programmation) peut être utilisé pour atteindre le même objectif, c’est-à-dire avoir une vue personnalisée réutilisable visible dans le storyboard principal.

L'avantage supplémentaire est que vous pouvez implémenter votre logique dans la classe CustomViewController et y configurer toutes les préparations de délégation et d'affichage sans créer de classes de contrôleurs distinctes (plus difficiles à trouver dans le projet) et sans placer de code passe-partout dans UIViewController principal à l'aide de Component. Je pense que c'est bon pour les composants réutilisables ex. Composant Music Player (comme un widget) pouvant être intégré dans d'autres vues. 

5
Michał Ziobro

La meilleure solution actuellement consiste simplement à utiliser un contrôleur de vue personnalisé avec sa vue définie dans une xib et à supprimer simplement la propriété "vue" créée par Xcode dans le storyboard lors de l'ajout du contrôleur de vue la classe personnalisée cependant).

Cela fera que le moteur d’exécution cherche automatiquement le fichier xib et le charge. Vous pouvez utiliser cette astuce pour tout type de vues de conteneur ou de contenu.

5
Ben G

Cette solution peut être utilisée même si votre classe ne porte pas le même nom que XIB . Par exemple, si vous avez une classe de contrôleur de vue de base controlA qui porte un nom XIB controllerA.xib et que vous sous-classez celui-ci avec controllerB et que vous voulez pour créer une instance de controllerB dans un storyboard, vous pouvez alors:

  • créer le contrôleur de vue dans le storyboard
  • définir la classe du contrôleur sur le contrôleurB
  • supprimer la vue du controllerB dans le storyboard
  • redéfinissez la vue de charge dans controllerA pour:

*

- (void) loadView    
{
        //according to the documentation, if a nibName was passed in initWithNibName or
        //this controller was created from a storyboard (and the controller has a view), then nibname will be set
        //else it will be nil
        if (self.nibName)
        {
            //a nib was specified, respect that
            [super loadView];
        }
        else
        {
            //if no nib name, first try a nib which would have the same name as the class
            //if that fails, force to load from the base class nib
            //this is convenient for including a subclass of this controller
            //in a storyboard
            NSString *className = NSStringFromClass([self class]);
            NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
            UINib *nib ;
            if (pathToNIB)
            {
                nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
            }
            else
            {
                //force to load from nib so that all subclass will have the correct xib
                //this is convenient for including a subclass
                //in a storyboard
                nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
            }

            self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
       }
}
0
otusweb

Voici la réponse que vous avez toujours recherchée. Vous pouvez simplement créer votre classe CustomView et en avoir l’instance principale dans un fichier xib avec toutes les sous-vues et tous les points de vente. Vous pouvez ensuite appliquer cette classe à toutes les instances de vos storyboards ou autres xibs.

Inutile de manipuler File's Owner, de connecter des prises à un proxy, de modifier le fichier xib de manière particulière, ou d'ajouter une instance de votre vue personnalisée en tant que sous-vue de celle-ci.

Faites juste ceci:

  1. Importer le framework BFWControls
  2. Changez votre super-classe de UIView à NibView (ou de UITableViewCell à NibTableViewCell)

C'est tout!

Il fonctionne même avec IBDesignable pour faire référence à votre vue personnalisée (y compris les sous-vues de xib) au moment de la conception dans le storyboard.

Vous pouvez en savoir plus à ce sujet ici: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

Et vous pouvez obtenir le framework open source BFWControls ici: https://github.com/BareFeetWare/BFWControls

Et voici un extrait simple du code NibReplaceable qui le pilote, au cas où vous seriez curieux: https://Gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

À M ????

0
barefeettom

Solution pour Objective-C selon les étapes décrites dans Réponse de Ben Patch .

Utiliser l'extension pour UIView:

@implementation UIView (NibLoadable)

- (UIView*)loadFromNib
{
    UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    xibView.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:xibView];
    [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    return xibView;
}

@end

Créez des fichiers MyView.h, MyView.m et MyView.xib.

Commencez par préparer votre MyView.xib en tant que La réponse de Ben Patch indique donc la classe MyView pour le propriétaire de File au lieu de la vue principale dans ce fichier XIB.

MyView.h:

#import <UIKit/UIKit.h>

IB_DESIGNABLE @interface MyView : UIView

@property (nonatomic, weak) IBOutlet UIView* someSubview;

@end

MyView.m:

#import "MyView.h"
#import "UIView+NibLoadable.h"

@implementation MyView

#pragma mark - Initializers

- (id)init
{
    self = [super init];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self loadFromNib];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self internalInit];
}

- (void)internalInit
{
    // Custom initialization.
}

@end

Et plus tard, créez simplement votre vue par programme:

MyView* view = [[MyView alloc] init];

Attention! L'aperçu de cette vue ne sera pas affiché dans Storyboard si vous utilisez WatchKit Extension à cause de ce bogue dans Xcode> = 9.2: https://forums.developer.Apple.com/thread/95616

0
Ariel Bogdziewicz

Bien que les réponses les plus populaires fonctionnent correctement, elles sont fausses. Ils utilisent tous File's owner comme connexion entre les prises de la classe et les composants de l'interface utilisateur, ce qui est très faux. File's owner est censé être utilisé uniquement pour les objets de niveau supérieur non UIViews. Départ document développeur Apple . Avoir UIView en tant que File's owner conduit à ces conséquences indésirables.

  1. Vous êtes obligé d'utiliser contentView alors que vous êtes censé n'utiliser que self. C'est moche.
  2. Vous ne pouvez avoir qu'un seul UIView par Xib. Xib est censé avoir plusieurs UIViews.

Il existe un moyen élégant de le faire sans utiliser File's owner. Veuillez vérifier ceci blog post . Il explique comment le faire correctement.

0
Ingun