Come posso creare una classe di visualizzazione iOS personalizzata e istanziarne più copie (in IB)?


114

Attualmente sto realizzando un'app che avrà più timer, che sono praticamente tutti uguali.

Voglio creare una classe personalizzata che utilizzi tutto il codice per i timer così come il layout / animazioni, così posso avere 5 timer identici che funzionano indipendentemente l'uno dall'altro.

Ho creato il layout usando IB (xcode 4.2) e tutto il codice per i timer è attualmente solo nella classe viewcontroller.

Ho difficoltà a comprendere come incapsulare tutto in una classe personalizzata e quindi aggiungerlo al viewcontroller, qualsiasi aiuto sarebbe molto apprezzato.


Per chiunque cerchi su Google fino a qui ... sono passati cinque anni dalle visualizzazioni container in arrivo su iOS! Le visualizzazioni contenitore sono l'idea di base e centrale di come fai tutto in iOS ora. Sono incredibilmente semplici da usare e ci sono un numero qualsiasi di eccellenti tutorial (di cinque anni!) Sulle visualizzazioni dei contenitori in giro, esempio , esempio
Fattie

2
@ JoeBlow Tranne che non è possibile utilizzare una visualizzazione contenitore in un UITableViewCello UICollectionViewCell. Nel mio caso ho bisogno di una vista piccola ma abbastanza complessa che posso usare molte volte nei controller e nelle viste della raccolta. E i designer continuano a rielaborarlo, quindi voglio un unico posto in cui il layout è definito. Quindi, un pennino.
Echelon

Risposte:


142

Bene, per rispondere concettualmente, il tuo timer dovrebbe probabilmente essere una sottoclasse di UIViewinvece di NSObject.

Per istanziare un'istanza del tuo timer in IB, trascinala semplicemente e UIViewrilasciala nella vista del controller di visualizzazione e imposta la sua classe sul nome della classe del tuo timer.

inserisci qui la descrizione dell'immagine

Ricorda #importla classe del timer nel controller della vista.

Modifica: per la progettazione IB (per l'istanza del codice vedere la cronologia delle revisioni)

Non ho molta familiarità con lo storyboard, ma so che puoi costruire la tua interfaccia in IB usando un .xibfile che è quasi identico all'uso della versione storyboard; Dovresti anche essere in grado di copiare e incollare le visualizzazioni nel loro insieme dall'interfaccia esistente al .xibfile.

Per testarlo ho creato un nuovo vuoto .xibdenominato "MyCustomTimerView.xib". Quindi ho aggiunto una vista e a quella ho aggiunto un'etichetta e due pulsanti. Così:

inserisci qui la descrizione dell'immagine

Ho creato una nuova sottoclasse di classe C dell'obiettivo UIViewdenominata "MyCustomTimer". Nel mio .xibho impostato la classe Proprietario del mio file su MyCustomTimer . Ora sono libero di collegare azioni e punti vendita proprio come qualsiasi altra visualizzazione / controller. Il .hfile risultante ha questo aspetto:

@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

L'unico ostacolo rimasto per saltare è ottenere questo .xibnella mia UIViewsottoclasse. L'uso di un .xibriduce drasticamente la configurazione richiesta. E poiché stai usando gli storyboard per caricare i timer, sappiamo che -(id)initWithCoder:è l'unico inizializzatore che verrà chiamato. Quindi ecco come appare il file di implementazione:

#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

Il metodo denominato loadNibNamed:owner:options:fa esattamente quello che sembra. Carica il pennino e imposta la proprietà "File's Owner" su self. Estraiamo il primo oggetto nell'array e questa è la vista principale del pennino. Aggiungiamo la vista come una vista secondaria e Voilà è sullo schermo.

Ovviamente questo cambia solo il colore di sfondo dell'etichetta quando i pulsanti vengono premuti, ma questo esempio dovrebbe portarti sulla buona strada.

Note basate sui commenti:

Vale la pena notare che se stai ottenendo infiniti problemi di ricorsione probabilmente ti sei perso il sottile trucco di questa soluzione. Non sta facendo quello che pensi stia facendo. La vista inserita nello storyboard non viene visualizzata, ma carica invece un'altra vista come vista secondaria. Quella vista che carica è la vista definita nel pennino. Il "proprietario del file" nel pennino è quella vista invisibile. La parte interessante è che questa vista invisibile è ancora una classe Objective-C che può essere usata come una sorta di controller della vista per la vista che porta dal pennino. Ad esempio, i IBActionmetodi nella MyCustomTimerclasse sono qualcosa che ti aspetteresti di più in un controller di visualizzazione che in una visualizzazione.

Come nota a margine, alcuni potrebbero obiettare che questo si rompe MVCe io sono in qualche modo d'accordo. Dal mio punto di vista è più strettamente correlato a una custom UITableViewCell, che a volte deve anche essere in parte controller.

Vale anche la pena notare che questa risposta doveva fornire una soluzione molto specifica; creare un pennino che può essere istanziato più volte nella stessa vista come disposto su uno storyboard. Ad esempio, potresti facilmente immaginare sei di questi timer tutti su uno schermo di iPad contemporaneamente. Se hai solo bisogno di specificare una vista per un controller di visualizzazione che deve essere utilizzato più volte nella tua applicazione, la soluzione fornita da jyavenard a questa domanda è quasi certamente una soluzione migliore per te.


Questa in realtà sembra una domanda a parte. Ma penso che si possa rispondere rapidamente, quindi ecco qui. Ogni volta che crei una nuova notifica è una nuova notifica, ma se hai bisogno che abbiano lo stesso nome, aggiungi qualcosa per differenziarli, quindi utilizza la userInfoproprietà sulla notifica@property(nonatomic,copy) NSDictionary *userInfo;
NJones

28
Ottengo una ricorsione infinita da initWithCoder. Qualche idea?
scomparso il

6
Mi dispiace far rivivere un vecchio thread, assicurandomi che File's Ownersia impostato sul nome della tua classe ha risolto il mio problema di ricorsione infinita.
themanatuf

20
Un altro motivo per la ricorsione infinita è l'impostazione della vista radice in xib sulla classe personalizzata. Non vuoi farlo, lascia come predefinito "UIView"
XY

11
C'è solo una cosa che attira la mia attenzione su questo approccio ... Non dà fastidio a nessuno che 2 Views siano nella stessa classe ora? il file originale .h / .m eredita da UIView essendo il primo e facendo [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Adding the Second UIView ... Questo sembra strano per me nessun altro si sente allo stesso modo? Se è così, è qualcosa di Apple fatto dal design? O qualcuno ha trovato la soluzione in questo modo?
SH

137

Un rapido esempio

Aggiornato per Xcode 10 e Swift 4

Ecco una panoramica di base. Inizialmente ho imparato molto di questo guardando questa serie di video di Youtube . Successivamente ho aggiornato la mia risposta sulla base di questo articolo .

Aggiungi file di visualizzazione personalizzati

I seguenti due file formeranno la tua visualizzazione personalizzata:

  • .xib per contenere il layout
  • .swift file come UIViewsottoclasse

I dettagli per aggiungerli sono di seguito.

File Xib

Aggiungi un file .xib al tuo progetto (File> Nuovo> File ...> Interfaccia utente> Visualizza) . Sto chiamando il mio ReusableCustomView.xib.

Crea il layout che desideri abbia la tua visualizzazione personalizzata. Ad esempio, creerò un layout con a UILabele a UIButton. È una buona idea utilizzare il layout automatico in modo che le cose vengano ridimensionate automaticamente indipendentemente dalle dimensioni che le imposti in seguito. (Ho usato Freeform per la dimensione xib nell'ispettore Attributi in modo da poter regolare le metriche simulate, ma questo non è necessario.)

inserisci qui la descrizione dell'immagine

File Swift

Aggiungi un file .swift al tuo progetto (File> Nuovo> File ...> Sorgente> File Swift) . È una sottoclasse di UIViewe io chiamo mia ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Rendi il file Swift il proprietario

Torna al tuo file .xib e fai clic su "Proprietario del file" nella struttura del documento. In Identity Inspector scrivi il nome del tuo file .swift come nome della classe personalizzata.

inserisci qui la descrizione dell'immagine

Aggiungi codice visualizzazione personalizzato

Sostituisci il ReusableCustomView.swiftcontenuto del file con il codice seguente:

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

Assicurati di ottenere l'ortografia corretta per il nome del tuo file .xib.

Collega i punti vendita e le azioni

Collega i punti vendita e le azioni controllando il trascinamento dall'etichetta e dal pulsante nel layout xib al codice di visualizzazione personalizzata rapida.

Usa la tua visualizzazione personalizzata

La tua visualizzazione personalizzata è terminata ora. Tutto quello che devi fare è aggiungere un posto UIViewdove vuoi nel tuo storyboard principale. Impostare il nome della classe della vista su ReusableCustomViewin Identity Inspector.

inserisci qui la descrizione dell'immagine


31
Importante non impostare la classe di visualizzazione Xib come ResuableCustomView, ma Proprietario di questo Xib. Altrimenti avrai un errore.
RealNmae

5
Sì, buon promemoria. Non so quanto tempo ho sprecato cercando di risolvere infiniti problemi di ricorsione causati dall'impostazione della vista di Xib (piuttosto che del proprietario del file) al nome della classe.
Suragch

2
@TomCalmon, ho rifatto questo progetto in Xcode 8 con Swift 3 e Auto Layout funzionava bene per me.
Suragch

6
Se qualcun altro ha un problema con il rendering della vista in IB, ho scoperto che il passaggio Bundle.maina Bundle(for: ...)fa il trucco. A quanto pare, Bundle.mainnon è disponibile in fase di progettazione.
crizzis

4
Il motivo per cui questo non funziona con @IBDesignable è che non l'hai implementato init(frame:). Dopo averlo aggiunto (e aver inserito tutto il codice di inizializzazione in qualche commonInit()funzione) funziona bene e viene visualizzato in IB. Vedi maggiori informazioni qui: stackoverflow.com/questions/31265906/…
Dimitris

39

Risposta per controller di visualizzazione, non per visualizzazioni :

C'è un modo più semplice per caricare un xib da uno storyboard. Supponiamo che il tuo controller sia di tipo MyClassController che eredita da UIViewController.

Aggiungi un UIViewControllerIB usando nel tuo storyboard; modificare il tipo di classe in MyClassController. Elimina la vista che era stata aggiunta automaticamente nello storyboard.

Assicurati che lo XIB che vuoi chiamare sia chiamato MyClassController.xib.

Quando verrà creata un'istanza della classe durante il caricamento dello storyboard, xib verrà caricato automaticamente. La ragione di ciò è dovuta all'implementazione predefinita di UIViewControllercui chiama lo XIB denominato con il nome della classe.


1
Gonfio le visualizzazioni personalizzate continuamente, ma non ho mai conosciuto questo piccolo trucco per caricarlo nello storyboard. Molto apprezzato!
MrTristan

38
La domanda riguarda le visualizzazioni, non i controller di visualizzazione.
Ricardo

Aggiunto titolo per chiarimenti, "Risposta per controller di visualizzazione, non visualizzazioni:"
nacho4d

1
Non sembra funzionare in iOS 9.1. Se rimuovo la vista creata automaticamente (predefinita), si arresta in modo anomalo dopo essere tornato dal viewDidLoad di VC. Non credo che la visualizzazione in XIB venga collegata al posto della visualizzazione predefinita.
Jeff

1
L'eccezione che ricevo è 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Nota il whacko nibName. Sto usando il nome della classe corretto per il pennino.
Jeff

16

Questa non è davvero una risposta, ma penso che sia utile condividere questo approccio.

Objective-C

  1. Importa CustomViewWithXib.h e CustomViewWithXib.m al progetto
  2. Crea i file di visualizzazione personalizzati con lo stesso nome (.h / .m / .xib)
  3. Eredita la tua classe personalizzata da CustomViewWithXib

veloce

  1. Importa CustomViewWithXib.swift nel tuo progetto
  2. Crea i file di visualizzazione personalizzati con lo stesso nome (.swift e .xib)
  3. Eredita la tua classe personalizzata da CustomViewWithXib

Opzionale:

  1. Vai al tuo file xib, imposta il proprietario con il nome della tua classe personalizzata se devi connettere alcuni elementi (per maggiori dettagli vedi la parte Rendi il file Swift il proprietario delle risposte di @Suragch)

È tutto, ora puoi aggiungere la tua visualizzazione personalizzata nel tuo storyboard e verrà mostrata :)

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

Puoi trovare alcuni esempi qui .

Spero che aiuti.


quindi come possiamo aggiungere iboutlet per la vista uicontrols
Amr Angry

1
Ehi @AmrAngry, per connettere le visualizzazioni dovresti fare il 4 passaggio: Vai al tuo file xib, in "File's Owner" imposta la tua classe, e trascina le tue visualizzazioni nell'interfaccia toccando il tocco "ctrl", aggiorno il codice sorgente con questo esempio, non esitare se hai qualche domanda, spero che aiuti
HamzaGhazouani

grazie mille per il tuo replay, lo faccio già ma immagino che il mio problema non sia in questa parte. il mio problema è che aggiungo un UIDatePIcker e provo ad aggiungere un'azione di destinazione per questo controllo dal ViewController da impostare e l'evento uiControleer viene modificato, ma il metodo non viene mai chiamato / fire
Amr Angry

1
@AmrAngry Aggiorno l'esempio aggiungendo la visualizzazione del selettore, forse ti aiuterà :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani

1
Grazie, cercavo questo da molto tempo.
MH175
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.