Comment créer une classe de vue iOS personnalisée et en instancier plusieurs copies (dans IB)?

114

Je crée actuellement une application qui aura plusieurs minuteries, qui sont fondamentalement toutes identiques.

Je veux créer une classe personnalisée qui utilise tout le code pour les minuteries ainsi que la mise en page / animations, afin que je puisse avoir 5 minuteries identiques qui fonctionnent indépendamment les unes des autres.

J'ai créé la mise en page en utilisant IB (xcode 4.2) et tout le code pour les minuteries est actuellement juste dans la classe viewcontroller.

J'ai du mal à comprendre comment tout encapsuler dans une classe personnalisée puis l'ajouter au viewcontroller, toute aide serait très appréciée.

Michael Campsall
la source
Pour tous ceux qui recherchent ici sur Google ... cinq ans après l'arrivée des vues de conteneurs sur iOS! Les vues de conteneur sont l'idée de base et centrale de la façon dont vous faites tout dans iOS maintenant. Ils sont incroyablement simples à utiliser, et il existe un certain nombre d'excellents didacticiels (vieux de cinq ans!) Sur les vues de conteneurs, exemple , exemple
Fattie
2
@JoeBlow Sauf que vous ne pouvez pas utiliser une vue conteneur dans un fichier UITableViewCellou UICollectionViewCell. Dans mon cas, j'ai besoin d'une vue petite mais assez complexe que je peux utiliser plusieurs fois dans les contrôleurs et les vues de collection. Et les concepteurs continuent de le retravailler, donc je veux un seul endroit où la mise en page est définie. Par conséquent, une plume.
Echelon le

Réponses:

142

Eh bien, pour répondre conceptuellement, votre minuterie devrait probablement être une sous-classe de UIViewau lieu de NSObject.

Pour instancier une instance de votre minuterie dans IB, faites-la simplement glisser et UIViewdéposez-la sur la vue de votre contrôleur de vue et définissez sa classe sur le nom de classe de votre minuterie.

entrez la description de l'image ici

N'oubliez pas #importvotre classe de minuterie dans votre contrôleur de vue.

Edit: pour la conception IB (pour l'instanciation du code, voir l'historique des révisions)

Je ne suis pas du tout familier avec le storyboard, mais je sais que vous pouvez construire votre interface dans IB en utilisant un .xibfichier qui est presque identique à l'utilisation de la version du storyboard; Vous devriez même pouvoir copier et coller vos vues dans leur ensemble depuis votre interface existante vers le .xibfichier.

Pour tester cela, j'ai créé un nouveau vide .xibnommé "MyCustomTimerView.xib". Ensuite, j'ai ajouté une vue, et à cela j'ai ajouté une étiquette et deux boutons. Ainsi:

entrez la description de l'image ici

J'ai créé une nouvelle sous-classe de classe objective-C UIViewnommée "MyCustomTimer". Dans mon, .xibj'ai défini la classe Propriétaire de mon fichier sur MyCustomTimer . Maintenant, je suis libre de connecter des actions et des prises comme n'importe quelle autre vue / contrôleur. Le .hfichier résultant ressemble à ceci:

@interface MyCustomTimer : UIView
@property (strong, nonatomic) IBOutlet UILabel *displayLabel;
@property (strong, nonatomic) IBOutlet UIButton *startButton;
@property (strong, nonatomic) IBOutlet UIButton *stopButton;
- (IBAction)startButtonPush:(id)sender;
- (IBAction)stopButtonPush:(id)sender;
@end

Le seul obstacle qui reste à sauter est d'obtenir cela .xibsur ma UIViewsous - classe. L'utilisation d'un .xibréduit considérablement la configuration requise. Et puisque vous utilisez des storyboards pour charger les minuteries, nous savons que -(id)initWithCoder:c'est le seul initialiseur qui sera appelé. Voici donc à quoi ressemble le fichier d'implémentation:

#import "MyCustomTimer.h"
@implementation MyCustomTimer
@synthesize displayLabel;
@synthesize startButton;
@synthesize stopButton;
-(id)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super initWithCoder:aDecoder])){
        [self addSubview:
         [[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" 
                                        owner:self 
                                      options:nil] objectAtIndex:0]];
    }
    return self;
}
- (IBAction)startButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor greenColor];
}
- (IBAction)stopButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor redColor];
}
@end

La méthode nommée loadNibNamed:owner:options:fait exactement ce qu'elle ressemble. Il charge le Nib et définit la propriété "File's Owner" sur self. Nous extrayons le premier objet du tableau et c'est la vue racine du Nib. Nous ajoutons la vue en tant que sous-vue et voilà, c'est à l'écran.

Évidemment, cela ne fait que changer la couleur d'arrière-plan de l'étiquette lorsque vous appuyez sur les boutons, mais cet exemple devrait vous aider à bien démarrer.

Notes basées sur des commentaires:

Il est à noter que si vous rencontrez des problèmes de récursions infinis, vous avez probablement raté l'astuce subtile de cette solution. Il ne fait pas ce que vous pensez faire. La vue placée dans le storyboard n'est pas vue, mais charge une autre vue en tant que sous-vue. Cette vue qu'il charge est la vue qui est définie dans la pointe. Le "propriétaire du fichier" dans la pointe est cette vue invisible. Ce qui est cool, c'est que cette vue invisible est toujours une classe Objective-C qui peut être utilisée comme une sorte de contrôleur de vue pour la vue qu'elle apporte depuis la pointe. Par exemple, les IBActionméthodes de la MyCustomTimerclasse sont quelque chose que vous attendez plus dans un contrôleur de vue que dans une vue.

En passant, certains peuvent soutenir que cela rompt MVCet je suis quelque peu d'accord. De mon point de vue, c'est plus étroitement lié à une coutume UITableViewCell, qui doit aussi parfois faire partie du contrôleur.

Il convient également de noter que cette réponse était de fournir une solution très spécifique; créez une pointe qui peut être instanciée plusieurs fois sur la même vue que celle présentée sur un storyboard. Par exemple, vous pouvez facilement imaginer six de ces minuteries sur un écran iPad à la fois. Si vous avez seulement besoin de spécifier une vue pour un contrôleur de vue qui doit être utilisé plusieurs fois dans votre application, la solution fournie par jyavenard à cette question est presque certainement une meilleure solution pour vous.

NJones
la source
Cela semble en fait une question distincte. Mais je pense qu'on peut y répondre rapidement, alors voilà. Chaque fois que vous créez une nouvelle notification, il s'agit d'une nouvelle notification, mais si vous en avez besoin pour avoir le même nom, vous voulez ajouter quelque chose pour les différencier, utilisez la userInfopropriété sur la notification@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
J'obtiens une récursion infinie de initWithCoder. Des idées?
disparu le
6
Désolé de relancer un ancien thread, en m'assurant que File's Ownerle nom de votre classe est défini, mon problème de récursivité infinie a été résolu.
themanatuf
20
Une autre raison de la récursivité infinie est de définir la vue racine dans le xib sur votre classe personnalisée. Vous ne voulez pas faire cela, laissez-le par défaut "UIView"
XY
11
Il y a juste une chose qui attire mon attention à propos de cette approche ... Cela ne dérange personne que 2 vues soient dans la même classe maintenant? le fichier d'origine .h / .m héritant de UIView étant le premier et en faisant [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Ajouter le deuxième UIView ... Cela semble étrange pour moi, personne d'autre ne ressent la même chose? Si tel est le cas, est-ce quelque chose de conçu par Apple? Ou est-ce que quelqu'un a trouvé la solution en faisant cela?
SH
137

Exemple Swift

Mis à jour pour Xcode 10 et Swift 4

Voici un aperçu de base. J'ai appris beaucoup de choses à l'origine en regardant cette série de vidéos Youtube . Plus tard, j'ai mis à jour ma réponse en fonction de cet article .

Ajouter des fichiers d'affichage personnalisés

Les deux fichiers suivants formeront votre vue personnalisée:

  • fichier .xib pour contenir la mise en page
  • Fichier .swift comme UIViewsous-classe

Les détails pour les ajouter sont ci-dessous.

Fichier xib

Ajoutez un fichier .xib à votre projet (Fichier> Nouveau> Fichier ...> Interface utilisateur> Affichage) . J'appelle le mien ReusableCustomView.xib.

Créez la mise en page que vous souhaitez que votre vue personnalisée ait. A titre d'exemple, je vais faire une mise en page avec a UILabelet a UIButton. C'est une bonne idée d'utiliser la mise en page automatique pour que les choses se redimensionnent automatiquement, quelle que soit la taille que vous définissez plus tard. (J'ai utilisé Freeform pour la taille xib dans l'inspecteur d'attributs afin de pouvoir ajuster les métriques simulées, mais ce n'est pas nécessaire.)

entrez la description de l'image ici

Fichier Swift

Ajoutez un fichier .swift à votre projet (Fichier> Nouveau> Fichier ...> Source> Fichier Swift) . C'est une sous-classe de UIViewet j'appelle la mienne ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Faire du fichier Swift le propriétaire

Revenez à votre fichier .xib et cliquez sur "Propriétaire du fichier" dans la structure du document. Dans l'inspecteur d'identité, écrivez le nom de votre fichier .swift comme nom de classe personnalisé.

entrez la description de l'image ici

Ajouter un code de vue personnalisé

Remplacez le ReusableCustomView.swiftcontenu du fichier par le code suivant:

import UIKit

@IBDesignable
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)
        commonInit()
    }

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

    func commonInit() {
        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
    }
}

Assurez-vous que l'orthographe du nom de votre fichier .xib est correcte.

Branchez les prises et les actions

Connectez les prises et les actions en contrôlant le glissement de l'étiquette et du bouton dans la mise en page xib vers le code de vue personnalisé rapide.

Utilisez votre vue personnalisée

Votre vue personnalisée est maintenant terminée. Tout ce que vous avez à faire est d'ajouter un UIViewoù vous le souhaitez dans votre storyboard principal. Définissez le nom de classe de la vue sur ReusableCustomViewdans l'inspecteur d'identité.

entrez la description de l'image ici

Suragch
la source
31
Il est important de ne pas définir la classe de vue Xib comme ResuableCustomView, mais comme propriétaire de ce Xib. Sinon, vous aurez une erreur.
RealNmae
5
Oui, bon rappel. Je ne sais pas combien de temps j'ai perdu à essayer de résoudre des problèmes de récursivité infinis causés par la définition de la vue Xib (plutôt que du propriétaire du fichier) sur le nom de la classe.
Suragch le
2
@TomCalmon, j'ai refait ce projet en Xcode 8 avec Swift 3 et Auto Layout fonctionnait bien pour moi.
Suragch
6
Si quelqu'un d'autre a un problème avec le rendu de la vue dans IB, j'ai trouvé que changer Bundle.mainen Bundle(for: ...)fait l'affaire. Apparemment, Bundle.mainn'est pas disponible au moment de la conception.
crizzis
4
La raison pour laquelle cela ne fonctionne pas avec @IBDesignable est que vous ne l'avez pas implémenté init(frame:). Une fois que vous ajoutez ceci (et tirez tout le code d'initialisation dans une commonInit()fonction), cela fonctionne bien et s'affiche dans IB. Voir plus d'informations ici: stackoverflow.com/questions/31265906/…
Dimitris
39

Réponse pour les contrôleurs de vue, pas pour les vues :

Il existe un moyen plus simple de charger un xib à partir d'un storyboard. Supposons que votre contrôleur soit du type MyClassController qui hérite de UIViewController.

Vous ajoutez un UIViewControllerIB utilisant dans votre storyboard; changez le type de classe en MyClassController. Supprimez la vue qui avait été automatiquement ajoutée dans le storyboard.

Assurez-vous que le XIB que vous souhaitez appeler s'appelle MyClassController.xib.

Lorsque la classe sera instanciée lors du chargement du storyboard, le xib sera automatiquement chargé. La raison en est due à l'implémentation par défaut de UIViewControllerlaquelle appelle le XIB nommé avec le nom de la classe.

jyavenard
la source
1
Je gonfle des vues personnalisées tout le temps, mais je n'ai jamais su cette petite astuce pour la charger dans le storyboard. Très appréciée!
MrTristan
38
La question concerne les vues, pas les contrôleurs de vue.
Ricardo
Titre ajouté pour clarification, "Réponse pour les contrôleurs de vue, pas pour les vues:"
nacho4d
1
Ne semble pas fonctionner sous iOS 9.1. Si je supprime la vue créée automatiquement (par défaut), elle se bloque après le retour de viewDidLoad du VC. Je ne pense pas que la vue dans le XIB se connecte à la place de la vue par défaut.
Jeff
1
L'exception que je reçois est 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Notez le whacko nibName. J'utilise le nom de classe correct pour le dans la plume.
Jeff
16

Ce n'est pas vraiment une réponse, mais je pense qu'il est utile de partager cette approche.

Objectif c

  1. Importez CustomViewWithXib.h et CustomViewWithXib.m dans votre projet
  2. Créez les fichiers de vue personnalisée avec le même nom (.h / .m / .xib)
  3. Héritez votre classe personnalisée de CustomViewWithXib

Rapide

  1. Importez CustomViewWithXib.swift dans votre projet
  2. Créez les fichiers de vue personnalisée avec le même nom (.swift et .xib)
  3. Héritez votre classe personnalisée de CustomViewWithXib

Optionnel :

  1. Accédez à votre fichier xib, définissez le propriétaire avec votre nom de classe personnalisé si vous devez connecter certains éléments (pour plus de détails, voir la partie Faire du fichier Swift le propriétaire des réponses @Suragch)

C'est tout, maintenant vous pouvez ajouter votre vue personnalisée dans votre storyboard et elle sera affichée :)

CustomViewWithXib.h :

 #import <UIKit/UIKit.h>

/**
 *  All classes inherit from CustomViewWithXib should have the same xib file name and class name (.h and .m)
 MyCustomView.h
 MyCustomView.m
 MyCustomView.xib
 */

// This allows seeing how your custom views will appear without building and running your app after each change.
IB_DESIGNABLE
@interface CustomViewWithXib : UIView

@end

CustomViewWithXib.m :

#import "CustomViewWithXib.h"

@implementation CustomViewWithXib

#pragma mark - init methods

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

#pragma mark - setup view

- (UIView *)loadViewFromNib {
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];

    //  An exception will be thrown if the xib file with this class name not found,
    UIView *view = [[bundle loadNibNamed:NSStringFromClass([self class])  owner:self options:nil] firstObject];
    return view;
}

- (void)commonSetup {
    UIView *nibView = [self loadViewFromNib];
    nibView.frame = self.bounds;
    // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
    nibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    // Adding nibView on the top of our view
    [self addSubview:nibView];
}

@end

CustomViewWithXib.swift :

import UIKit

@IBDesignable
class CustomViewWithXib: UIView {

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

        commonSetup()
    }

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

        commonSetup()
    }

    // MARK: setup view 

    private func loadViewFromNib() -> UIView {
        let viewBundle = NSBundle(forClass: self.dynamicType)
        //  An exception will be thrown if the xib file with this class name not found,
        let view = viewBundle.loadNibNamed(String(self.dynamicType), owner: self, options: nil)[0]
        return view as! UIView
    }

    private func commonSetup() {
        let nibView = loadViewFromNib()
        nibView.frame = bounds
        // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
        nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        // Adding nibView on the top of our view
        addSubview(nibView)
    }
}

Vous pouvez trouver quelques exemples ici .

J'espère que cela pourra aider.

HamzaGhazouani
la source
alors comment nous pouvons ajouter iboutlet pour la vue uicontrols
Amr Angry
1
Hey @AmrAngry, pour connecter les vues, vous devez faire la 4 étape: Allez dans votre fichier xib, dans "File's Owner" définissez votre classe, et faites glisser et déposez vos vues sur l'interface en appuyant sur la touche "ctrl", je mets à jour le source du code avec cet exemple, n'hésitez pas si vous avez des questions, espérons que cela vous aidera
HamzaGhazouani
merci beaucoup pour votre rediffusion, je le fais déjà mais je suppose que mon problème n'est pas dans cette partie. mon problème est que j'ajoute un UIDatePIcker et j'essaie d'ajouter une action cible pour ce contrôle à partir du ViewController à définir et que l'événement uiControleer est modifié, mais la méthode n'est jamais appelée / fire
Amr Angry
1
@AmrAngry Je mets à jour l'exemple en ajoutant la vue du sélecteur, peut-être que cela vous aidera :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Merci je cherchais ça depuis longtemps.
MH175