Frage Erstellen einer wiederverwendbaren UIView mit xib (und Laden vom Storyboard)


OK, es gibt Dutzende von Posts auf StackOverflow, aber keine sind besonders klar in der Lösung. Ich möchte eine benutzerdefinierte erstellen UIView mit einer begleitenden XIB-Datei. Die Anforderungen sind:

  • Nicht getrennt UIViewController - eine vollständig in sich geschlossene Klasse
  • Outlets in der Klasse, mit denen ich Eigenschaften der Ansicht festlegen / abrufen kann

Mein aktueller Ansatz dafür ist:

  1. Überschreiben -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Instanziieren Sie programmgesteuert mit -(id)initWithFrame: aus meiner Sicht Controller

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

Dies funktioniert gut (obwohl nie angerufen wird [super init] und das Setzen des Objekts mit dem Inhalt der geladenen Feder scheint ein wenig verdächtig - hier gibt es Ratschläge Fügen Sie in diesem Fall eine Unteransicht hinzu was auch gut funktioniert). Allerdings möchte ich auch die Ansicht vom Storyboard instanziieren können. Also kann ich:

  1. Platziere a UIView in einer übergeordneten Ansicht im Storyboard
  2. Setzen Sie die benutzerdefinierte Klasse auf MyCustomView
  3. Überschreiben -(id)initWithCoder: - Der Code, den ich am häufigsten gesehen habe, entspricht einem Muster wie dem folgenden:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Natürlich funktioniert das nicht, da, ob ich den obigen Ansatz verwende oder ob ich programmatisch instanziiere, beide rekursiv aufrufen -(id)initWithCoder: bei der Einreise -(void)initializeSubviews und Laden der Feder aus der Datei.

Mehrere andere SO Fragen beschäftigen sich damit wie Hier, Hier, Hier und Hier. Keine der angegebenen Antworten behebt das Problem jedoch:

  • Ein allgemeiner Vorschlag scheint zu sein, die gesamte Klasse in einen UIViewController einzubetten und die Nib dort zu laden, aber das scheint mir suboptimal zu sein, da es das Hinzufügen einer anderen Datei nur als Wrapper erfordert

Könnte jemand Ratschläge geben, wie man dieses Problem lösen kann und wie man in einer Gewohnheit arbeitet UIView mit minimalem Aufwand / kein dünner Controller-Wrapper? Oder gibt es eine alternative, sauberere Vorgehensweise mit minimalem Standardcode?


75
2018-02-20 04:33


Ursprung


Antworten:


Dein Problem ruft an loadNibNamed: von (einem Nachkommen von) initWithCoder:. loadNibNamed: intern ruft an initWithCoder:. Wenn Sie den Storyboard-Coder außer Kraft setzen und Ihre xib-Implementierung immer laden möchten, empfehle ich die folgende Technik. Fügen Sie Ihrer Ansichtsklasse eine Eigenschaft hinzu, und legen Sie sie in der xib-Datei auf einen vorgegebenen Wert fest (in benutzerdefinierten Laufzeitattributen). Jetzt, nach dem Anruf [super initWithCoder:aDecoder]; Überprüfen Sie den Wert der Eigenschaft. Wenn es der vorbestimmte Wert ist, rufen Sie nicht an [self initializeSubviews];.

Also, in etwa so:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

12
2017-09-11 14:57



Beachten Sie, dass diese QA (wie viele) wirklich nur von historischem Interesse ist.

Heutzutage Seit Jahren ist in iOS alles nur noch eine Containeransicht. Vollständiges Tutorial hier

(Tatsächlich hat Apple schließlich hinzugefügt Storyboard-Referenzen, vor einiger Zeit, macht es jetzt viel einfacher.)

Hier ist ein typisches Storyboard mit Containeransichten überall. Alles ist eine Containeransicht. So machen Sie Apps.

enter image description here

(Als eine Wissbegierde zeigt KenCs ​​Antwort genau, wie es gemacht wurde, um einen xib in eine Art Wrapper View zu laden, da man nicht wirklich "sich selbst zuordnen" kann.)


26
2017-09-18 11:00



Ich füge das als separaten Beitrag hinzu, um die Situation mit der Veröffentlichung von Swift zu aktualisieren. Der von LeoNatan beschriebene Ansatz funktioniert perfekt in Objective-C. Die strengeren Kompilierzeitprüfungen verhindern jedoch self beim Laden aus der XIB-Datei in Swift zugewiesen werden.

Daher gibt es keine andere Möglichkeit, als die aus der XIB-Datei geladene Ansicht als Unteransicht der benutzerdefinierten UIView-Unterklasse hinzuzufügen, anstatt self vollständig zu ersetzen. Dies ist analog zu dem in der ursprünglichen Frage skizzierten zweiten Ansatz. Ein grober Überblick über eine Klasse in Swift, die diesen Ansatz verwendet, ist wie folgt:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initializeSubviews()
    }

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

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

Die Kehrseite dieses Ansatzes ist die Einführung einer zusätzlichen redundanten Schicht in der Ansichtshierarchie, die bei Verwendung des von LeoNatan in Objective-C beschriebenen Ansatzes nicht existiert. Dies könnte jedoch als notwendiges Übel und Produkt der fundamentalen Art und Weise angesehen werden, wie Dinge in Xcode entworfen werden (es erscheint mir immer noch verrückt, dass es so schwierig ist, eine benutzerdefinierte UIView-Klasse mit einem UI-Layout auf konsistente Weise zu verknüpfen über beide Storyboards und vom Code) - ersetzen self Großhandel im Initialisierer zuvor schien nie eine besonders interpretierbare Art und Weise zu sein, Dinge zu tun, obwohl es auch nicht so toll ist, im Grunde zwei Sichtklassen pro Sicht zu haben.

Nichtsdestoweniger ist es ein erfreuliches Ergebnis dieses Ansatzes, dass wir die benutzerdefinierte Klasse der Ansicht nicht mehr in unserer Klassendatei im Interface Builder festlegen müssen, um das korrekte Verhalten beim Zuweisen zu gewährleisten selfund so der rekursive Aufruf an init(coder aDecoder: NSCoder) bei der Ausgabe loadNibNamed() ist kaputt (indem Sie die benutzerdefinierte Klasse in der XIB - Datei nicht festlegen, init(coder aDecoder: NSCoder) von plain vanilla UIView anstatt unserer benutzerdefinierten Version wird stattdessen aufgerufen).

Obwohl wir keine Klassenanpassungen für die in der xib gespeicherte Ansicht vornehmen können, können wir die Ansicht immer noch mit der UIView-Unterklasse 'parent' verknüpfen, indem wir outlets / actions usw. verwenden, nachdem wir den Dateieigner der Ansicht auf unsere benutzerdefinierte Klasse gesetzt haben:

Setting the file owner property of the custom view

Ein Video, das Schritt für Schritt die Implementierung einer solchen View-Klasse demonstriert, kann mit diesem Ansatz gefunden werden im folgenden Video.


22
2017-12-25 12:59



SCHRITT 1. Ersetzen self vom Storyboard

Ersetzen self im initWithCoder: Methode wird mit folgendem Fehler fehlschlagen.

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

Stattdessen können Sie das decodierte Objekt durch ersetzen awakeAfterUsingCoder: (nicht awakeFromNib). mögen:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

SCHRITT 2. Rekursiven Aufruf verhindern

Natürlich verursacht dies auch ein rekursives Aufrufproblem. (Storyboard Dekodierung -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...)
Sie müssen also den aktuellen Stand prüfen awakeAfterUsingCoder: wird im Storyboard-Dekodierungsprozess oder XIB-Dekodierungsprozess aufgerufen. Sie haben mehrere Möglichkeiten, dies zu tun:

a) Verwenden Sie privat @property Dies wird nur in NIB festgelegt.

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

und legen Sie "Benutzerdefinierte Laufzeitattribute" nur in "MyCustomView.xib" fest.

Vorteile:

  • Keiner

Nachteile:

  • Funktioniert einfach nicht: setXib: wird angerufen werden NACH  awakeAfterUsingCoder:

b) Überprüfen Sie, ob self hat irgendwelche Unteransichten

Normalerweise haben Sie Subviews in der Xib, aber nicht im Storyboard.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

Vorteile:

  • Kein Trick im Interface Builder.

Nachteile:

  • Sie können keine Unteransichten in Ihrem Storyboard haben.

c) Setzen Sie eine statische Flagge während loadNibNamed: Anruf

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

Vorteile:

  • Einfach
  • Kein Trick im Interface Builder.

Nachteile:

  • Nicht sicher: Die statische Gemeinschaftsflagge ist gefährlich

d) Verwenden Sie private Unterklasse in XIB

Zum Beispiel deklarieren _NIB_MyCustomView als Unterklasse von MyCustomView. Und, benutze _NIB_MyCustomView Anstatt von MyCustomView nur in Ihrem XIB.

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

Vorteile:

  • Nicht explizit if im MyCustomView

Nachteile:

  • Vorfixierung _NIB_ Trick in XIB Interface Builder
  • relativ mehr Codes

e) Verwenden Sie die Unterklasse als Platzhalter im Storyboard

Ähnlich zu d) aber benutze Unterklasse in Storyboard, ursprüngliche Klasse in XIB.

Hier erklären wir MyCustomViewProto als Unterklasse von MyCustomView.

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

Vorteile:

  • Sehr sicher
  • Reinigen; Kein zusätzlicher Code in MyCustomView.
  • Nicht explizit if Überprüfen Sie das gleiche wie d)

Nachteile:

  • Sie müssen die Unterklasse im Storyboard verwenden.

Ich denke e) ist die sicherste und sauberste Strategie. Also nehmen wir das hier an.

SCHRITT 3. Eigenschaften kopieren

Nach loadNibNamed: In 'awakeAfterUsingCoder:' müssen Sie mehrere Eigenschaften kopieren self Das ist decodierte Instanz des Storyboards. frame und Autolayout / Autoresize-Eigenschaften sind besonders wichtig.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

ENDGÜLTIGE LÖSUNG

Wie Sie sehen können, ist dies ein bisschen Standardcode. Wir können sie als "Kategorie" implementieren. Hier dehne ich mich häufig aus UIView+loadFromNib Code.

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

Mit diesem können Sie deklarieren MyCustomViewProto mögen:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

XIB screenshot

Storyboard:

Storyboard

Ergebnis:

enter image description here


16
2017-09-14 18:41



Vergiss nicht

Zwei wichtige Punkte:

  1. Setzen Sie den Eigentümer der Datei der .xib auf den Klassennamen Ihrer benutzerdefinierten Ansicht.
  2. Nicht Legen Sie den benutzerdefinierten Klassennamen in IB für die Stammansicht der .xib fest.

Ich kam mehrmals zu dieser Frage-und-Antwort-Seite, während ich lernte, wiederverwendbare Ansichten zu machen. Das Vergessen der oben genannten Punkte ließ mich viel Zeit verschwenden, um herauszufinden, was eine unendliche Rekursion ausgelöst hat. Diese Punkte werden hier und in anderen Antworten erwähnt anderswo, aber ich möchte sie hier nur noch einmal betonen.

Meine volle schnelle Antwort mit Schritten ist Hier.


12
2018-01-02 05:38



Es gibt eine Lösung, die viel sauberer ist als die obigen Lösungen: https://www.youtube.com/watch?v=xP7YvdlnHfA

Keine Laufzeiteigenschaften, kein rekursives Aufrufproblem. Ich habe es ausprobiert und es funktionierte wie ein Zauberspruch vom Storyboard und von XIB mit IBOutlet Eigenschaften (iOS8.1, XCode6).

Viel Glück beim Codieren!


2
2018-01-10 09:05