web-dev-qa-db-fra.com

Comment incorporer une vue personnalisée xib dans une scène de storyboard?

Je suis relativement nouveau dans le monde XCode/iOS; J'ai créé des applications basées sur un storyboard de taille décente, mais je ne me suis jamais fait les dents pour tout ce qui concerne nib/xib. Je souhaite utiliser les mêmes outils pour les scènes afin de concevoir/mettre en forme une vue/un contrôle réutilisable. J'ai donc créé mon tout premier xib pour ma sous-classe de vue et l'ai peint:

enter image description here

J'ai mes prises connectées et la configuration des contraintes, comme je suis habitué à le faire dans le storyboard. Je mets la classe de mon File Owner à celle de ma sous-classe UIView personnalisée. Donc, je suppose que je peux instancier cette sous-classe de vue avec une API, et elle sera configurée/connectée comme indiqué.

De retour dans mon story-board, je veux intégrer/réutiliser ceci. Je le fais dans une cellule prototype de vue tableau:

enter image description here

J'ai une vue. J'en ai rangé la classe dans ma sous-classe. J'ai créé un point de vente pour que je puisse le manipuler.

La question à 64 $ est où/comment puis-je indiquer qu'il ne suffit pas de placer une instance vide/non configurée de ma sous-classe view, mais d'utiliser le fichier .xib que j'ai créé pour le configurer/l'instancier? Ce serait vraiment bien si, dans XCode6, je pouvais simplement entrer le fichier XIB à utiliser pour un UIView donné, mais je ne vois pas de champ pour le faire, alors je suppose que je dois faire quelque chose dans le code quelque part.

(Je vois d'autres questions comme celle-ci sur SO, mais je n'ai trouvé aucune question concernant uniquement cette partie du puzzle ou une mise à jour avec XCode6/2015)

Mettre à jour

Je peux obtenir ce genre de travail en implémentant la variable awakeFromNib de ma cellule de table comme suit:

- (void)awakeFromNib
{
    // gather all of the constraints pointing to the uncofigured instance
    NSArray* progressConstraints = [self.contentView.constraints filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id each, NSDictionary *_) {
        return (((NSLayoutConstraint*)each).firstItem == self.progressControl) || (((NSLayoutConstraint*)each).secondItem == self.progressControl);
    }]];
    // fetch the fleshed out variant
    ProgramProgressControl *fromXIB = [[[NSBundle mainBundle] loadNibNamed:@"ProgramProgressControl" owner:self options:nil] objectAtIndex:0];
    // ape the current placeholder's frame
    fromXIB.frame = self.progressControl.frame;
    // now swap them
    [UIView transitionFromView: self.progressControl toView: fromXIB duration: 0 options: 0 completion: nil];
    // recreate all of the constraints, but for the new guy
    for (NSLayoutConstraint *each in progressConstraints) {
        id firstItem = each.firstItem == self.progressControl ? fromXIB : each.firstItem;
        id secondItem = each.secondItem == self.progressControl ? fromXIB : each.secondItem;
        NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: firstItem attribute: each.firstAttribute relatedBy: each.relation toItem: secondItem attribute: each.secondAttribute multiplier: each.multiplier constant: each.constant];
        [self.contentView addConstraint: constraint];
    }
    // update our outlet
    self.progressControl = fromXIB;
}

Est-ce aussi facile que cela devient alors? Ou est-ce que je travaille trop dur pour ça?

41
Travis Griggs

Tu y es presque. Vous devez remplacer initWithCoder dans votre classe personnalisée à laquelle vous avez attribué la vue.

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"ViewYouCreated" owner:self options:nil] objectAtIndex:0]];
    }
    return self; }

Une fois que cela sera fait, StoryBoard saura charger le xib à l'intérieur de cette UIView.

Voici une explication plus détaillée:

Voici à quoi ressemble votre UIViewController sur votre story-board: enter image description here

L'espace bleu est fondamentalement un UIView qui "tiendra" votre xib. 

Ceci est votre xib: 

enter image description here

Une action est associée à un bouton qui imprimera du texte. 

et voici le résultat final: 

enter image description here

La différence entre le premier clickMe et le second est que le premier a été ajouté à UIViewController à l'aide de StoryBoard. Le second a été ajouté en utilisant du code.

26
Segev

Vous devez implémenter awakeAfterUsingCoder: dans votre sous-classe UIView personnalisée. Cette méthode vous permet d'échanger l'objet décodé (du storyboard) avec un objet différent (de votre xib réutilisable), comme ceci:

- (id) awakeAfterUsingCoder: (NSCoder *) aDecoder
{
    // without this check you'll end up with a recursive loop - we need to know that we were loaded from our view xib vs the storyboard.
    // set the view tag in the MyView xib to be -999 and anything else in the storyboard.
    if ( self.tag == -999 )
    {
        return self;
    }

    // make sure your custom view is the first object in the nib
    MyView* v = [[[UINib nibWithNibName: @"MyView" bundle: nil] instantiateWithOwner: nil options: nil] firstObject];

    // copy properties forward from the storyboard-decoded object (self)
    v.frame = self.frame;
    v.autoresizingMask = self.autoresizingMask;
    v.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
    v.tag = self.tag;

    // copy any other attribtues you want to set in the storyboard

    // possibly copy any child constraints for width/height

    return v;
}

Il y a une assez bonne écriture ici traitant de cette technique et de quelques alternatives.

De plus, si vous ajoutez IB_DESIGNABLE à votre déclaration @interface et que vous fournissez une méthode initWithFrame:, vous pouvez obtenir l'aperçu au moment du design pour qu'il fonctionne dans IB (Xcode 6 requis!):

IB_DESIGNABLE @interface MyView : UIView
@end

@implementation MyView

- (id) initWithFrame: (CGRect) frame
{
    self = [[[UINib nibWithNibName: @"MyView"
                            bundle: [NSBundle bundleForClass: [MyView class]]]

             instantiateWithOwner: nil
             options: nil] firstObject];

    self.frame = frame;

    return self;
}
16
TomSwift

Une façon plutôt sympa et réutilisable de faire ceci Interface Builder et Swift 4:

  1. Créez une nouvelle classe comme ceci:

    import Foundation
    import UIKit
    
    @IBDesignable class XibView: UIView {
    
        @IBInspectable var xibName: String?
    
        override func awakeFromNib() { 
            guard let name = self.xibName, 
                  let xib = Bundle.main.loadNibNamed(name, owner: self), 
                  let view = xib.first as? UIView else { return }    
            self.addSubview(view)
        }
    
    }
    
  2. Dans votre storyboard, ajoutez un UIView qui servira de conteneur pour le Xib. Donnez-lui un nom de classe de XibView:

  3. Dans l'inspecteur de propriétés de cette nouvelle XibView, définissez le nom de votre fichier .xib (sans l'extension de fichier) dans le champ IBInspectable:

  4. Ajoutez une nouvelle vue Xib à votre projet et, dans l'inspecteur de propriétés, définissez le "Propriétaire du fichier" de Xib sur XibView (assurez-vous que vous avez uniquement défini le "Propriétaire du fichier" dans votre classe personnalisée. NE PAS sous-classer la vue du contenu. plantera), puis définissez à nouveau le champ IBInspectable:

Une chose à noter: Cela suppose que vous faites correspondre le cadre .xib à son conteneur. Si vous ne le souhaitez pas ou si vous avez besoin qu'il soit redimensionnable, vous devrez ajouter des contraintes de programmation ou modifier le cadre de la sous-vue pour l'adapter. J'utilise snapkit pour rendre les choses faciles:

xibView.snp_makeConstraints(closure: { (make) -> Void in
    make.edges.equalTo(self)
})

Points bonus

Vous pouvez utiliser prepareForInterfaceBuilder() pour rendre ces vues réutilisables visibles dans Interface Builder, mais je n’ai pas eu beaucoup de chance. Ce blog suggère d'ajouter une propriété contentView et d'appeler ce qui suit:

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    xibSetup()
    contentView?.prepareForInterfaceBuilder()
}
15
brandonscript
  • Créer un fichier xib File > New > New File > iOS > User Interface > View
  • Créer une classe UIView personnalisée File > New > New File > iOS > Source > CocoaTouch
  • Attribuer l'identité du fichier xib à la classe de vue personnalisée
  • Dans viewDidLoad du contrôleur de vue, initialisez la xib et son fichier associé en utilisant loadNibNamed: sur NSBundle.mainBundle et la première vue renvoyée peut être ajoutée en tant que sous-vue de self.view.
  • La vue personnalisée chargée depuis le nib peut être enregistrée dans une propriété pour définir le cadre dans viewDidLayoutSubviews. Il suffit de définir le cadre sur le cadre de self.view, sauf si vous devez le rendre plus petit que self.view.

    class ViewController: UIViewController {
    
        weak var customView: MyView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.customView = NSBundle.mainBundle().loadNibNamed("MyView", owner: self, options: nil)[0] as! MyView
            self.view.addSubview(customView)
            addButtonHandlerForCustomView()
        }
    
        private func addButtonHandlerForCustomView() {
            customView.buttonHandler = {
                [weak self] (sender:UIButton) in
                guard let welf = self else {
                    return
                }
                welf.buttonTapped(sender)
            }
        }
    
        override func viewDidLayoutSubviews() {
            self.customView.frame = self.view.frame
        }
    
        private func buttonTapped(button:UIButton) {
    
        }
    }
    
  • De même, si vous souhaitez que votre xib réponde à votre instance UIViewController, créez une propriété faible sur la classe de la vue personnalisée.

    class MyView: UIView {
    
        var buttonHandler:((sender:UIButton)->())!
    
        @IBAction func buttonTapped(sender: UIButton) {
            buttonHandler(sender:sender)
        }
    }
    

Voici le projet sur GitHub

2
smileBot

Il vous suffit de faire glisser et de déposer UIView dans votre IB et de le placer

yourUIViewClass  *yourView =   [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]

enter image description here

Étape

  1. Ajouter un nouveau fichier => Interface utilisateur => UIView 
  2. Définir une classe personnalisée - yourUIViewClass
  3. Définir l'ID de restauration - yourUIViewClass
  4. yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject]; [self.view addSubview:yourView]

Vous pouvez maintenant personnaliser la vue à votre guise.

2
Yuyutsu

Version un peu plus rapide de l’idée de @brandonscript avec retour rapide:

override func awakeFromNib() {

    guard let xibName = xibName,
          let xib = Bundle.main.loadNibNamed(xibName, owner: self, options: nil),
          let views = xib as? [UIView] else {
        return
    }

    if views.count > 0 {
        self.addSubview(views[0])
    }

}
1
Sasha Prokhorenko

J'utilise cet extrait de code depuis des années. Si vous envisagez d'avoir des vues de classe personnalisées dans votre XIB, déposez-les simplement dans le fichier .m de votre classe personnalisée.

Comme effet secondaire, awakeFromNib est appelé et vous pouvez y laisser tout votre code init/setup.

- (id)awakeAfterUsingCoder:(NSCoder*)aDecoder {
    if ([[self subviews] count] == 0) {
        UIView *view = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil][0];
        view.frame = self.frame;
        view.autoresizingMask = self.autoresizingMask;
        view.alpha = self.alpha;
        view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
        return view;
    }
    return self;
}
1
alexgophermix

Bien que je ne recommande pas le chemin que vous prenez, vous pouvez le faire en plaçant une "vue de contrôleur de vue intégrée" à l'endroit où vous souhaitez que la vue apparaisse.

Intégrez un contrôleur de vue contenant une seule vue - la vue que vous souhaitez réutiliser.

0
Dustin

Cela fait un moment sur ce sujet, et j'ai vu passer plusieurs réponses. Je l'ai récemment revisitée parce que je venais d'utiliser l'intégration UIViewController. Ce qui fonctionne jusqu'à ce que vous souhaitiez insérer quelque chose dans "un élément répété au moment de l'exécution" (par exemple, UICollectionViewCell ou UITableViewCell). Le lien fourni par @TomSwift m'a amené à suivre le modèle de

A) Plutôt que de faire de la classe de vue parent le type de classe personnalisé, faites de FileOwner la classe cible (dans mon exemple, CycleControlsBar)

B) Tout lien de sortie/action des widgets imbriqués va à celui

C) Implémentez cette méthode simple sur CycleControlsBar:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    if let container = (Bundle.main.loadNibNamed("CycleControlsBar", owner: self, options: nil) as? [UIView])?.first {
        self.addSubview(container)
        container.constrainToSuperview() // left as an excise for the student
    }
}

Fonctionne comme un charme et tellement plus simple que les autres approches.

0
Travis Griggs