Wie erstelle ich eine benutzerdefinierte iOS-Ansichtsklasse und instanziiere mehrere Kopien davon (in IB)?

114

Ich mache gerade eine App, die mehrere Timer haben wird, die im Grunde alle gleich sind.

Ich möchte eine benutzerdefinierte Klasse erstellen, die den gesamten Code für die Timer sowie das Layout / die Animationen verwendet, damit ich 5 identische Timer haben kann, die unabhängig voneinander arbeiten.

Ich habe das Layout mit IB (xcode 4.2) erstellt und der gesamte Code für die Timer befindet sich derzeit nur in der Viewcontroller-Klasse.

Ich habe Schwierigkeiten, mein Gehirn darum zu wickeln, wie man alles in eine benutzerdefinierte Klasse kapselt und es dann dem Viewcontroller hinzufügt. Jede Hilfe wäre sehr dankbar.

Michael Campsall
quelle
Für alle, die hierher googeln ... es sind nun fünf Jahre , bis die Containeransichten auf iOS kommen! Containeransichten sind die grundlegende, zentrale Idee, wie Sie jetzt alles in iOS tun . Sie sind unglaublich einfach zu bedienen, und es gibt eine beliebige Anzahl von sehr gut (5 Jahre alt!) Tutorials auf Container Ansichten um, Beispiel , Beispiel
Fattie
2
@JoeBlow Außer Sie können keine Containeransicht in einem UITableViewCelloder verwenden UICollectionViewCell. In meinem Fall benötige ich eine kleine, aber ziemlich komplexe Ansicht, die ich in Controllern und Sammlungsansichten häufig verwenden kann. Und die Designer überarbeiten es ständig, daher möchte ich einen einzigen Ort, an dem das Layout definiert wird. Daher eine Feder.
Echelon

Antworten:

142

Um konzeptionell zu antworten, sollte Ihr Timer wahrscheinlich eine Unterklasse von UIViewstatt sein NSObject.

Um eine Instanz Ihres Timers in IB zu instanziieren, ziehen Sie sie einfach UIViewin die Ansicht Ihres View Controllers und setzen Sie ihre Klasse auf den Klassennamen Ihres Timers.

Geben Sie hier die Bildbeschreibung ein

Denken Sie an #importIhre Timer-Klasse in Ihrem View Controller.

Bearbeiten: für IB-Design (Code-Instanziierung siehe Revisionsverlauf)

Ich bin mit Storyboard überhaupt nicht sehr vertraut, aber ich weiß, dass Sie Ihre Schnittstelle in IB mithilfe einer .xibDatei erstellen können, die fast identisch mit der Verwendung der Storyboard-Version ist. Sie sollten sogar in der Lage sein, Ihre Ansichten als Ganzes von Ihrer vorhandenen Oberfläche in die .xibDatei zu kopieren und einzufügen .

Um dies zu testen, habe ich ein neues .xibLeerzeichen mit dem Namen "MyCustomTimerView.xib" erstellt. Dann habe ich eine Ansicht hinzugefügt und dazu eine Beschriftung und zwei Schaltflächen hinzugefügt. Gefällt mir So:

Geben Sie hier die Bildbeschreibung ein

Ich habe eine neue Ziel-C-Klasse-Unterklasse mit dem UIViewNamen "MyCustomTimer" erstellt. In meinem .xibhabe ich die Owner- Klasse meiner Datei auf MyCustomTimer festgelegt . Jetzt kann ich Aktionen und Ausgänge wie jede andere Ansicht / Steuerung verbinden. Die resultierende .hDatei sieht folgendermaßen aus:

@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

Die einzige Hürde, die noch zu springen ist, besteht darin, dies .xibin meine UIViewUnterklasse aufzunehmen. Durch die Verwendung eines .xibwird das erforderliche Setup drastisch reduziert. Und da Sie Storyboards zum Laden der Timer verwenden, wissen wir, dass dies -(id)initWithCoder:der einzige Initialisierer ist, der aufgerufen wird. So sieht die Implementierungsdatei aus:

#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

Die genannte Methode loadNibNamed:owner:options:macht genau das, wonach es sich anhört. Es lädt die Schreibfeder und setzt die Eigenschaft "File's Owner" auf self. Wir extrahieren das erste Objekt im Array und das ist die Stammansicht der Feder. Wir fügen die Ansicht als Unteransicht hinzu und Voila wird auf dem Bildschirm angezeigt.

Natürlich ändert dies nur die Hintergrundfarbe des Etiketts, wenn die Tasten gedrückt werden, aber dieses Beispiel sollte Sie auf Ihrem Weg gut machen.

Anmerkungen basierend auf Kommentaren:

Es ist erwähnenswert, dass Sie bei unendlichen Rekursionsproblemen wahrscheinlich den subtilen Trick dieser Lösung verpasst haben. Es macht nicht das, was du denkst. Die Ansicht, die in das Storyboard eingefügt wird, wird nicht angezeigt, sondern lädt eine andere Ansicht als Unteransicht. Diese Ansicht, die es lädt, ist die Ansicht, die in der Feder definiert ist. Der "Eigentümer der Datei" in der Schreibfeder ist diese unsichtbare Ansicht. Der coole Teil ist, dass diese unsichtbare Ansicht immer noch eine Objective-C-Klasse ist, die als eine Art Ansichts-Controller für die Ansicht verwendet werden kann, die sie von der Schreibfeder einbringt. Zum Beispiel sind die IBActionMethoden in der MyCustomTimerKlasse etwas, das Sie in einem Ansichts-Controller mehr erwarten würden als in einer Ansicht.

Als Randnotiz können einige argumentieren, dass dies bricht MVCund ich stimme etwas zu. Aus meiner Sicht ist es enger mit einem Brauch verbunden UITableViewCell, der manchmal auch Teilesteuerung sein muss.

Es ist auch erwähnenswert, dass diese Antwort eine sehr spezifische Lösung bieten sollte; Erstellen Sie eine Feder, die in derselben Ansicht wie in einem Storyboard mehrfach instanziiert werden kann . Sie können sich beispielsweise leicht vorstellen, dass sechs dieser Timer gleichzeitig auf einem iPad-Bildschirm angezeigt werden. Wenn Sie nur eine Ansicht für einen Ansichtscontroller angeben müssen, der in Ihrer Anwendung mehrmals verwendet werden soll, ist die von jyavenard für diese Frage bereitgestellte Lösung mit ziemlicher Sicherheit eine bessere Lösung für Sie.

NJones
quelle
Dies scheint tatsächlich eine separate Frage zu sein. Aber ich denke, es kann schnell beantwortet werden, also geht es weiter. Jedes Mal, wenn Sie eine neue Benachrichtigung erstellen, handelt es sich um eine neue Benachrichtigung. Wenn Sie jedoch möchten, dass sie denselben Namen haben, möchten Sie etwas hinzufügen, um sie zu unterscheiden. Verwenden Sie dann die userInfoEigenschaft für die Benachrichtigung@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
Ich bekomme eine unendliche Rekursion von initWithCoder. Irgendwelche Ideen?
verschwunden
6
Es tut mir leid, einen alten Thread wiederzubeleben. Wenn Sie sicherstellen, dass File's OwnerIhr Klassenname festgelegt ist, ist mein Problem mit der unendlichen Rekursion gelöst.
themanatuf
20
Ein weiterer Grund für die unendliche Rekursion besteht darin, die Stammansicht in der xib auf Ihre benutzerdefinierte Klasse festzulegen. Sie möchten das nicht tun, lassen Sie es als Standard "UIView"
XY
11
Es gibt nur eine Sache, die meine Aufmerksamkeit auf diesen Ansatz lenkt ... Stört es niemanden, dass 2 Ansichten jetzt in derselben Klasse sind? Die Originaldatei .h / .m, die von UIView erbt, ist die erste und durch [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Hinzufügen der zweiten UIView ... Das sieht für mich irgendwie komisch aus. Niemand sonst fühlt sich genauso? Wenn ja, ist dies etwas Apfel von Design gemacht? Oder ist die Lösung jemand, der dies findet?
SH
137

Schnelles Beispiel

Aktualisiert für Xcode 10 und Swift 4

Hier ist ein grundlegender Spaziergang durch. Ich habe ursprünglich viel davon gelernt, als ich mir diese Youtube-Videoserie angesehen habe . Später habe ich meine Antwort basierend auf diesem Artikel aktualisiert .

Fügen Sie benutzerdefinierte Ansichtsdateien hinzu

Die folgenden zwei Dateien bilden Ihre benutzerdefinierte Ansicht:

  • .xib-Datei, die das Layout enthält
  • .swift-Datei als UIViewUnterklasse

Die Details zum Hinzufügen sind unten.

Xib-Datei

Fügen Sie Ihrem Projekt eine .xib-Datei hinzu (Datei> Neu> Datei ...> Benutzeroberfläche> Ansicht) . Ich rufe meine an ReusableCustomView.xib.

Erstellen Sie das Layout, das Ihre benutzerdefinierte Ansicht haben soll. Als Beispiel werde ich ein Layout mit a UILabelund a erstellen UIButton. Es ist eine gute Idee, das automatische Layout zu verwenden, damit die Größe automatisch geändert wird, unabhängig davon, auf welche Größe Sie sie später einstellen. (Ich habe Freeform für die xib-Größe im Attributinspektor verwendet, damit ich die simulierten Metriken anpassen kann, dies ist jedoch nicht erforderlich.)

Geben Sie hier die Bildbeschreibung ein

Schnelle Datei

Fügen Sie Ihrem Projekt eine .swift-Datei hinzu (Datei> Neu> Datei ...> Quelle> Swift-Datei) . Es ist eine Unterklasse von UIViewund ich rufe meine an ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Machen Sie die Swift-Datei zum Eigentümer

Gehen Sie zurück zu Ihrer .xib-Datei und klicken Sie in der Dokumentübersicht auf " Dateibesitzer ". Schreiben Sie im Identitätsinspektor den Namen Ihrer .swift-Datei als benutzerdefinierten Klassennamen.

Geben Sie hier die Bildbeschreibung ein

Fügen Sie einen benutzerdefinierten Ansichtscode hinzu

Ersetzen Sie den ReusableCustomView.swiftInhalt der Datei durch den folgenden Code:

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

Stellen Sie sicher, dass die Schreibweise für den Namen Ihrer .xib-Datei richtig ist.

Schließen Sie die Steckdosen und Aktionen an

Schließen Sie die Steckdosen und Aktionen an, indem Sie das Ziehen von der Beschriftung und der Schaltfläche im xib-Layout zum schnellen benutzerdefinierten Ansichtscode steuern.

Verwenden Sie Ihre benutzerdefinierte Ansicht

Ihre benutzerdefinierte Ansicht ist jetzt fertig. Alles, was Sie tun müssen, ist, ein UIViewbeliebiges Element in Ihr Haupt-Storyboard einzufügen. Setzen Sie den Klassennamen der Ansicht ReusableCustomViewim Identitätsinspektor auf.

Geben Sie hier die Bildbeschreibung ein

Suragch
quelle
31
Es ist wichtig, die Xib-Ansichtsklasse nicht als ResuableCustomView festzulegen, sondern als Eigentümer dieser Xib. Andernfalls wird ein Fehler angezeigt.
RealNmae
5
Ja, gute Erinnerung. Ich weiß nicht, wie viel Zeit ich damit verschwendet habe, unendliche Rekursionsprobleme zu lösen, die durch das Setzen der Xib-Ansicht (und nicht des Dateibesitzers) auf den Klassennamen verursacht wurden.
Suragch
2
@ TomCalmon, ich habe dieses Projekt in Xcode 8 mit Swift 3 überarbeitet und Auto Layout hat für mich in Ordnung funktioniert.
Suragch
6
Wenn jemand anderes ein Problem mit dem Rendern von Ansichten in IB hat, habe ich festgestellt, dass das Ändern Bundle.mainzu Bundle(for: ...)den Trick macht. Anscheinend Bundle.mainist zur Entwurfszeit nicht verfügbar.
Crizzis
4
Der Grund, warum dies mit @IBDesignable nicht funktioniert, ist, dass Sie nicht implementiert haben init(frame:). Sobald Sie dies hinzugefügt haben (und den gesamten Initialisierungscode in eine commonInit()Funktion gezogen haben), funktioniert es einwandfrei und wird in IB angezeigt. Weitere Informationen finden Sie hier: stackoverflow.com/questions/31265906/…
Dimitris
39

Antwort für Ansichts-Controller, nicht für Ansichten :

Es gibt eine einfachere Möglichkeit, eine XIB aus einem Storyboard zu laden. Angenommen, Ihr Controller ist vom Typ MyClassController, von dem erbt UIViewController.

Sie fügen UIViewControllerIhrem Storyboard einen using IB hinzu. Ändern Sie den Klassentyp in MyClassController. Löschen Sie die Ansicht, die automatisch im Storyboard hinzugefügt wurde.

Stellen Sie sicher, dass die gewünschte XIB MyClassController.xib heißt.

Wenn die Klasse während des Ladens des Storyboards instanziiert wird, wird die xib automatisch geladen. Der Grund dafür liegt in der Standardimplementierung, bei UIViewControllerder die XIB mit dem Klassennamen aufgerufen wird.

Jyavenard
quelle
1
Ich blase die ganze Zeit benutzerdefinierte Ansichten auf, kannte aber nie diesen kleinen Trick, um ihn in das Storyboard zu laden. Sehr geschätzt!
MrTristan
38
Die Frage bezieht sich auf Ansichten, nicht auf Ansichtssteuerungen.
Ricardo
Titel zur Verdeutlichung hinzugefügt, "Antwort für Ansichts-Controller, nicht Ansichten:"
nacho4d
1
Scheint in iOS 9.1 nicht zu funktionieren. Wenn ich die automatisch erstellte (Standard-) Ansicht entferne, stürzt sie ab, nachdem ich von viewDidLoad des VC zurückgekehrt bin. Ich glaube nicht, dass die Ansicht in der XIB anstelle der Standardansicht verbunden wird.
Jeff
1
Die Ausnahme, die ich bekomme, ist 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Beachten Sie den whacko nibName. Ich verwende den richtigen Klassennamen für die in der Feder.
Jeff
16

Dies ist keine wirkliche Antwort, aber ich denke, es ist hilfreich, diesen Ansatz zu teilen.

Ziel c

  1. Importieren Sie CustomViewWithXib.h und CustomViewWithXib.m in Ihr Projekt
  2. Erstellen Sie die benutzerdefinierten Ansichtsdateien mit demselben Namen (.h / .m / .xib).
  3. Erben Sie Ihre benutzerdefinierte Klasse von CustomViewWithXib

Schnell

  1. Importieren Sie CustomViewWithXib.swift in Ihr Projekt
  2. Erstellen Sie die benutzerdefinierten Ansichtsdateien mit demselben Namen (.swift und .xib).
  3. Erben Sie Ihre benutzerdefinierte Klasse von CustomViewWithXib

Optional :

  1. Gehen Sie zu Ihrer xib-Datei und legen Sie den Eigentümer mit Ihrem benutzerdefinierten Klassennamen fest, wenn Sie einige Elemente verbinden müssen (weitere Informationen finden Sie im Teil Machen Sie die Swift-Datei zum Eigentümer der @ Suragch-Antworten).

Jetzt können Sie Ihre benutzerdefinierte Ansicht in Ihr Storyboard einfügen und sie wird angezeigt :)

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

Hier finden Sie einige Beispiele finden Sie hier .

Hoffentlich hilft das.

HamzaGhazouani
quelle
Also, wie können wir Iboutlet für die Ansicht uicontrols hinzufügen
Amr Angry
1
Hey @AmrAngry, um die Ansichten zu verbinden, sollten Sie die folgenden 4 Schritte ausführen: Gehen Sie zu Ihrer xib-Datei, legen Sie in "File's Owner" Ihre Klasse fest und ziehen Sie Ihre Ansichten per Drag & Drop auf die Benutzeroberfläche, indem Sie auf "Strg" tippen Code-Quelle mit diesem Beispiel, zögern Sie nicht, wenn Sie Fragen haben, hoffe, das hilft
HamzaGhazouani
Vielen Dank für Ihre Wiederholung, ich mache das bereits, aber ich denke, mein Problem nicht in diesem Teil. Mein Problem ist, dass ich einen UIDatePIcker hinzufüge und versuche, eine Zielaktion für dieses Steuerelement aus dem ViewController hinzuzufügen, um das uiControleer-Ereignis zu ändern. Die Methode wird jedoch nie / fire aufgerufen
Amr Angry
1
@ AmrAngry Ich aktualisiere das Beispiel durch Hinzufügen der Auswahlansicht, möglicherweise hilft es Ihnen :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Danke, ich habe lange danach gesucht.
MH175