158 votes

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

Je veux utiliser une vue à travers plusieurs viewcontrollers dans un storyboard. Ainsi, j'ai pensé à concevoir la vue dans un xib externe afin que les changements soient reflétés dans chaque viewcontroller. Mais comment peut-on charger une vue à partir d'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 ?

1 votes

Regardez cette vidéo: youtube.com/watch?v=o3MawJVxTgk

169voto

Suragch Points 197

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

Disposition

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

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

description de l'image ici Code

Ajoutez le code suivant au fichier .swift et connectez les outlets et actions 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 = "Salut"
    }

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

Utilisez-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 le nom de votre classe personnalisée.

description de l'image ici

105voto

Ben Patch Points 1037

Pendant un certain temps, l'approche de Christopher Swasey était la meilleure approche que j'avais trouvée. J'ai demandé à quelques-uns des développeurs seniors de mon équipe à ce sujet et l'un d'eux avait la solution parfaite! Elle satisfait chacune des préoccupations que Christopher Swasey a si élégamment abordées et ne nécessite pas de code de sous-classe de base (ma principale préoccupation avec son approche). Il y a un piège, mais à part ça, c'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. par exemple MyCustomClass.swift
  2. Créez un fichier .xib et stylisez-le comme vous le souhaitez. par exemple MyCustomClass.xib
  3. Définissez le File's Owner du fichier .xib comme étant votre classe personnalisée (MyCustomClass)
  4. PIÈGE: laissez la valeur class (sous le inspecteur d'identité) pour votre vue personnalisée dans le fichier .xib vide. Ainsi, votre vue personnalisée n'aura aucune classe spécifiée, mais aura un File's Owner spécifié.
  5. Connectez vos points de vente comme vous le feriez normalement en utilisant l'éditeur Assistant.
    • REMARQUE: Si vous regardez l'inspecteur de connexions, vous remarquerez que vos points de vente de référence ne font pas référence à votre classe personnalisée (c'est-à-dire MyCustomClass), mais font plutôt référence à File's Owner. Étant donné que File's Owner est spécifié comme étant votre classe personnalisée, les points de vente se connecteront et fonctionneront correctement.
  6. Assurez-vous que votre classe personnalisée comporte @IBDesignable avant l'instruction de classe.
  7. Veillez à ce que votre classe personnalisée se conforme au protocole NibLoadable référencé ci-dessous.
    • REMARQUE: Si le nom du fichier de votre classe personnalisée .swift est différent du nom de votre fichier .xib, définissez la propriété nibName comme étant 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 une UIView à votre storyboard désiré et définissez la classe comme étant le nom de votre classe personnalisée (c'est-à-dire MyCustomClass).
  10. Observez IBDesignable en action alors qu'il dessine votre .xib dans le storyboard avec toute sa splendeur et sa merveille.

Voici le protocole auquel vous voudrez vous référer:

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

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // se reporte au nom de la classe implémentant ce protocole par défaut.
    }

    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 la MyCustomClass qui implémente le protocole (avec le fichier .xib 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 Piège et définissez la valeur class à l'intérieur de votre fichier .xib comme étant votre classe personnalisée, alors il ne se dessinera pas dans le storyboard et vous obtiendrez une erreur EXC_BAD_ACCESS lorsque vous exécutez l'application car elle reste bloquée dans une boucle infinie en essayant d'initialiser la classe à partir du xib en utilisant la méthode init?(coder aDecoder: NSCoder) qui appelle ensuite Self.nib.instantiate et appelle à nouveau le init.

32voto

user1021430 Points 278

Supposons que vous avez créé un xib que vous voulez utiliser :

1) Créez une sous-classe personnalisée de UIView (vous pouvez aller dans Fichier -> Nouveau -> Fichier... -> Classe Cocoa Touch. Assurez-vous que "Sous-classe de :" est "UIView").

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

En Obj-C

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

En Swift 2

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

En Swift 3

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

3) Partout où vous voulez l'utiliser dans votre storyboard, ajoutez une UIView comme vous le feriez normalement, sélectionnez la vue nouvellement ajoutée, allez à l'inspecteur d'identité (le troisième icône en haut à droite qui ressemble à un rectangle avec des lignes), et saisissez le nom de la sous-classe en tant que "Classe" sous "Classe Personnalisée".

31voto

Christopher Swasey Points 5107

J'ai toujours trouvé la solution "l'ajouter en tant que sous-vue" insatisfaisante, car elle interfère avec (1) autolayout, (2) @IBInspectable, et (3) les outlets. À la place, laissez-moi vous présenter la magie de awakeAfter:, une méthode de l'objet NSObject.

awakeAfter vous permet de remplacer l'objet réellement réveillé à partir d'un NIB/Storyboard par un tout autre objet. Cet objet est alors soumis au processus d'hydratation, a awakeFromNib appelé sur lui, est ajouté en tant que vue, etc.

Nous pouvons utiliser cela dans une sous-classe "découpe en carton" de notre vue, dont le seul but sera de charger la vue à partir du NIB et de la renvoyer pour une utilisation dans le Storyboard. La sous-classe imbriquée est ensuite spécifiée dans l'inspecteur d'identité de la vue du Storyboard, plutôt que la classe d'origine. Cela n'a pas besoin d'être réellement une sous-classe pour que cela fonctionne, mais faire de cette sous-classe une sous-classe est ce qui permet à IB de voir toutes les propriétés IBInspectable/IBOutlet.

Cette étape supplémentaire peut sembler suboptimale—et c'est le cas, dans un sens, car idéalement UIStoryboard gérerait cela de façon transparente—mais cela a l'avantage de laisser le NIB d'origine et la classe UIView complètement non modifiés. Son rôle est essentiellement celui d'une classe adaptatrice ou de pont, et est parfaitement valide, du point de vue de la conception, en tant que classe supplémentaire, même si elle est regrettable. D'un autre côté, si vous préférez être économe avec vos classes, la solution de @BenPatch fonctionne en implémentant un protocole avec quelques autres changements mineurs. La question de laquelle des deux solutions est meilleure se résume à une question de style de programmation : que préférez-vous entre la composition d'objets ou l'héritage multiple.

Remarque : la classe définie sur la vue dans le fichier NIB reste la même. La sous-classe imbriquée est uniquement utilisée dans le Storyboard. La sous-classe ne peut pas être utilisée pour instancier la vue en code, elle ne doit donc pas avoir de logique supplémentaire en soi. Elle ne doit uniquement contenir le crochet awakeAfter.

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

⚠️ Le principal inconvénient ici est que si vous définissez des contraintes de largeur, de hauteur ou de rapport d'aspect dans le storyboard qui ne sont pas liées à une autre vue, elles doivent être copiées manuellement. Les contraintes liées à deux vues sont installées sur l'ancêtre commun le plus proche, et les vues sont réveillées à partir du storyboard de l'intérieur vers l'extérieur, donc à ce moment-là, ces contraintes sont hydratées sur la super vue, le remplacement est déjà survenu. Les contraintes qui concernent uniquement la vue en question sont installées directement sur cette vue, et donc sont jetées lorsque le remplacement survient à moins qu'elles ne soient copiées.

Remarquez bien que ce qui se passe ici, c'est que les contraintes installées sur la vue dans le storyboard sont copiées sur la nouvelle vue instantiée, qui peut déjà avoir ses propres contraintes, définies dans son fichier NIB. Celles-ci ne sont pas affectées.

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 sûre au niveau des types de UIView. Tout ce qu'elle fait, c'est parcourir les objets du NIB jusqu'à en trouver un qui correspond au type. Notez que le type générique est la valeur de retour, donc le type doit être spécifié à l'appel.

extension UIView {
  public class func instantiateViewFromNib(_ 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
  }
}

9voto

Ingun전인건 Points 416

Bien que les réponses les plus populaires en haut fonctionnent bien, elles sont conceptuellement erronées. Elles utilisent toutes File's owner comme connexion entre les points de vente de la classe et les composants d'interface utilisateur. File's owner est censé être utilisé uniquement pour les objets de niveau supérieur, pas les UIView. Consultez le document de développement d'Apple. Avoir une UIView comme File's owner entraîne ces conséquences indésirables.

  1. Vous êtes obligé d'utiliser contentView là où vous êtes censé utiliser self. Ce n'est pas seulement laid, mais aussi structurellement faux car la vue intermédiaire conserve la structure de données qui transmet sa structure d'interface utilisateur. C'est comme aller à l'encontre du concept d'interface utilisateur déclarative.
  2. Vous ne pouvez avoir qu'une seule UIView par Xib. Un Xib est censé avoir plusieurs UIViews.

Il y a une façon élégante de le faire sans utiliser File's owner. Veuillez consulter ce article de blog. Il explique comment le faire de la bonne manière.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X