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?
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.
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é.
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.
MyCustomClass.Swift
MyCustomClass.xib
File's Owner
du fichier .xib sur votre classe personnalisée (MyCustomClass
)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é.Assistant Editor
.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. NibLoadable
référencé ci-dessous. .Swift
personnalisé est différent de votre nom de fichier .xib
, définissez la propriété nibName
sur le nom de votre fichier .xib
.required init?(coder aDecoder: NSCoder)
et override init(frame: CGRect)
pour appeler setupFromNib()
comme dans l'exemple ci-dessous.MyCustomClass
).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
.
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".
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
}
}
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.
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.
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:
*
- (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];
}
}
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:
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 ????
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
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 UIView
s. Départ document développeur Apple . Avoir UIView en tant que File's owner
conduit à ces conséquences indésirables.
contentView
alors que vous êtes censé n'utiliser que self
. C'est moche.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.