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 ?
Réponses
Trop de publicités?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.
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.
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.
- Créez une classe UIView personnalisée dans un fichier .swift pour contrôler votre xib. par exemple
MyCustomClass.swift
- Créez un fichier .xib et stylisez-le comme vous le souhaitez. par exemple
MyCustomClass.xib
- Définissez le
File's Owner
du fichier .xib comme étant votre classe personnalisée (MyCustomClass
) - PIÈGE: laissez la valeur
class
(sous leinspecteur 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é. - 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-à-direMyCustomClass
), mais font plutôt référence àFile's Owner
. Étant donné queFile's Owner
est spécifié comme étant votre classe personnalisée, les points de vente se connecteront et fonctionneront correctement.
- REMARQUE: Si vous regardez l'
- Assurez-vous que votre classe personnalisée comporte @IBDesignable avant l'instruction de classe.
- 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
.
- REMARQUE: Si le nom du fichier de votre classe personnalisée
- Implémentez
required init?(coder aDecoder: NSCoder)
etoverride init(frame: CGRect)
pour appelersetupFromNib()
comme dans l'exemple ci-dessous. - 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
). - 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
.
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".
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
}
}
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.
- Vous êtes obligé d'utiliser
contentView
là où vous êtes censé utiliserself
. 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. - 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.
- Réponses précédentes
- Plus de réponses
2 votes
Possible duplicate de Comment créer une classe de vue iOS personnalisée et instancier plusieurs copies de celle-ci (dans IB) ?
1 votes
Regardez cette vidéo: youtube.com/watch?v=o3MawJVxTgk