OK, es gibt Dutzende von Posts auf StackOverflow, aber keiner ist besonders klar über die Lösung. Ich möchte eine benutzerdefinierte UIView
Datei mit einer zugehörigen xib-Datei erstellen. Die Anforderungen sind:
- Keine separate
UIViewController
- eine völlig eigenständige Klasse - Outlets in der Klasse, damit ich Eigenschaften der Ansicht festlegen / abrufen kann
Mein aktueller Ansatz dazu ist:
Ü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; }
Programmgesteuert mit
-(id)initWithFrame:
meinem View Controller instanziierenMyCustomView *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 einwandfrei (obwohl das Aufrufen [super init]
und einfache Festlegen des Objekts mithilfe des Inhalts der geladenen Schreibfeder etwas verdächtig erscheint - hier wird empfohlen, in diesem Fall eine Unteransicht hinzuzufügen, die ebenfalls einwandfrei funktioniert). Ich möchte jedoch auch die Ansicht vom Storyboard aus instanziieren können. Also kann ich:
- Platzieren Sie eine
UIView
übergeordnete Ansicht im Storyboard - Setzen Sie die benutzerdefinierte Klasse auf
MyCustomView
Überschreiben
-(id)initWithCoder:
- Der Code, den ich am häufigsten gesehen habe, passt zu 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]; }
Dies funktioniert natürlich nicht. Unabhängig davon, ob ich den obigen Ansatz verwende oder programmgesteuert instanziiere, werden beide -(id)initWithCoder:
beim Eingeben -(void)initializeSubviews
und Laden der Schreibfeder aus der Datei rekursiv aufgerufen.
Einige andere SO-Fragen befassen sich damit, wie hier , hier , hier und hier . Keine der gegebenen Antworten behebt das Problem jedoch zufriedenstellend:
- Ein häufiger Vorschlag scheint darin zu bestehen, die gesamte Klasse in einen UIViewController einzubetten und dort die Schreibfeder zu laden. Dies scheint mir jedoch nicht optimal zu sein, da eine weitere Datei nur als Wrapper hinzugefügt werden muss
Könnte jemand Ratschläge geben, wie man dieses Problem löst und Arbeitssteckdosen in einem benutzerdefinierten UIView
System mit minimalem Aufwand / ohne Thin Controller-Wrapper erhält ? Oder gibt es eine alternative, sauberere Methode, um Dinge mit minimalem Boilerplate-Code zu erledigen?
quelle
Antworten:
Ihr Problem ist das Anrufen
loadNibNamed:
von (ein Nachkomme von)initWithCoder:
.loadNibNamed:
intern anruftinitWithCoder:
. Wenn Sie den Storyboard-Codierer überschreiben und Ihre xib-Implementierung immer laden möchten, empfehle ich die folgende Technik. Fügen Sie Ihrer Ansichtsklasse eine Eigenschaft hinzu, und setzen Sie sie in der xib-Datei auf einen vorgegebenen Wert (unter Benutzerdefinierte Laufzeitattribute).[super initWithCoder:aDecoder];
Überprüfen Sie nun nach dem Aufruf den Wert der Eigenschaft. Wenn es sich um den vorgegebenen Wert handelt, rufen Sie nicht an[self initializeSubviews];
.Also so etwas:
-(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; }
quelle
Beachten Sie, dass diese Qualitätssicherung (wie viele andere auch) nur von historischem Interesse ist.
HeutzutageSeit Jahren ist in iOS alles nur noch eine Containeransicht. Vollständiges Tutorial hier(In der Tat hat Apple vor einiger Zeit endlich Storyboard-Referenzen hinzugefügt , was es weitaus einfacher macht.)
Hier ist ein typisches Storyboard mit Containeransichten überall. Alles ist eine Containeransicht. So erstellen Sie Apps.
(Aus Neugier zeigt die Antwort von KenC genau, wie es früher gemacht wurde, eine xib in eine Art Wrapper-Ansicht zu laden, da man sich nicht wirklich "selbst zuweisen" kann.)
quelle
Ich füge dies als separaten Beitrag hinzu, um die Situation mit der Veröffentlichung von Swift zu aktualisieren. Der von LeoNatan beschriebene Ansatz funktioniert in Objective-C perfekt. Die strengeren Überprüfungen der Kompilierungszeit verhindern jedoch,
self
dass sie beim Laden aus der xib-Datei in Swift zugewiesen werden.Daher bleibt keine andere Wahl, 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 zweiten Ansatz, der in der ursprünglichen Frage beschrieben wurde. Ein grober Überblick über eine Klasse in Swift, die diesen Ansatz verwendet, lautet 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 } }
Der Nachteil 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 vorhanden ist. Dies könnte jedoch als notwendiges Übel und als Produkt der grundlegenden Art und Weise angesehen werden, wie Dinge in Xcode entworfen werden (es scheint mir immer noch verrückt zu sein, dass es so schwierig ist, eine benutzerdefinierte UIView-Klasse mit einem UI-Layout auf eine Weise zu verknüpfen, die konsistent funktioniert über beide Storyboards und aus Code) - das Ersetzen des
self
Großhandels im Initialisierer zuvor schien nie eine besonders interpretierbare Methode zu sein, obwohl es auch nicht so toll erscheint, im Wesentlichen zwei Ansichtsklassen pro Ansicht zu haben.Ein erfreuliches Ergebnis dieses Ansatzes ist jedoch, dass wir die benutzerdefinierte Klasse der Ansicht im Interface Builder nicht mehr auf unsere Klassendatei festlegen müssen, um ein korrektes Verhalten bei der Zuweisung sicherzustellen. Daher wird
self
der rekursive Aufrufinit(coder aDecoder: NSCoder)
bei der AusgabeloadNibNamed()
unterbrochen (indem Sie die nicht festlegen Die benutzerdefinierte Klasse in der xib-Datei, dieinit(coder aDecoder: NSCoder)
von einfachem Vanille-UIView anstelle unserer benutzerdefinierten Version verwendet wird, wird stattdessen aufgerufen.Auch wenn wir keine Klassenanpassungen an der in xib direkt gespeicherten Ansicht vornehmen können, können wir die Ansicht mithilfe von Outlets / Aktionen usw. mit unserer übergeordneten UIView-Unterklasse verknüpfen, nachdem der Dateieigentümer der Ansicht auf unsere benutzerdefinierte Klasse festgelegt wurde:
Ein Video, das die Implementierung einer solchen Ansichtsklasse Schritt für Schritt unter Verwendung dieses Ansatzes demonstriert, finden Sie im folgenden Video .
quelle
UIView
. Ich stimme zu, es ist verrückt, dass Apple dies noch nie so einfach gemacht hat, und jetzt ist es praktisch unmöglich. Ein Container ist nicht immer die Antwort.SCHRITT 1. Ersetzen
self
aus dem StoryboardErsetzen
self
ininitWithCoder:
Methode fehl mit folgendem Fehler.'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Stattdessen können Sie dekodierte Objekte durch
awakeAfterUsingCoder:
(nichtawakeFromNib
) ersetzen . 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
Dies verursacht natürlich auch ein Problem mit rekursiven Aufrufen. (Storyboard-Dekodierung ->
awakeAfterUsingCoder:
->loadNibNamed:
->awakeAfterUsingCoder:
->loadNibNamed:
-> ...)Sie müssen also überprüfen, ob der aktuelle
awakeAfterUsingCoder:
Wert im Storyboard-Dekodierungsprozess oder im XIB-Dekodierungsprozess aufgerufen wird. Sie haben mehrere Möglichkeiten, dies zu tun:a) Verwenden Sie private,
@property
die nur in der NIB festgelegt ist.@interface MyCustomView : UIView @property (assign, nonatomic) BOOL xib @end
und setzen Sie "Benutzerdefinierte Laufzeitattribute" nur in "MyCustomView.xib".
Vorteile:
Nachteile:
setXib:
wird NACHher aufgerufenawakeAfterUsingCoder:
b) Überprüfen Sie, ob
self
Unteransichten vorhanden sindNormalerweise haben Sie Unteransichten 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:
Nachteile:
c) Setzen Sie während des
loadNibNamed:
Anrufs ein statisches Flagstatic 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:
Nachteile:
d) Verwenden Sie eine private Unterklasse in XIB
Deklarieren Sie beispielsweise
_NIB_MyCustomView
als Unterklasse vonMyCustomView
. Und verwenden Sie_NIB_MyCustomView
stattMyCustomView
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:
if
inMyCustomView
Nachteile:
_NIB_
Trick in xib Interface Buildere) Verwenden Sie die Unterklasse als Platzhalter im Storyboard
Ähnlich,
d)
aber Unterklasse in Storyboard verwenden, Originalklasse in XIB.Hier deklarieren wir
MyCustomViewProto
als Unterklasse vonMyCustomView
.@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:
MyCustomView
.if
Prüfung wied)
Nachteile:
Ich denke, das
e)
ist die sicherste und sauberste Strategie. Also übernehmen wir das hier.SCHRITT 3. Eigenschaften kopieren
Nach
loadNibNamed:
'awakeAfterUsingCoder:' müssen Sie mehrere Eigenschaften kopieren, ausself
denen die Instanz des Storyboards dekodiert wird.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 Boilerplate-Code. Wir können sie als "Kategorie" implementieren. Hier erweitere ich häufig verwendeten
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
MyCustomViewProto
wie folgt deklarieren :@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:
Storyboard:
Ergebnis:
quelle
Vergiss nicht
Zwei wichtige Punkte:
Ich bin mehrmals auf diese Q & A-Seite gekommen, als ich gelernt habe, wiederverwendbare Ansichten zu erstellen. Wenn ich die oben genannten Punkte vergaß, verschwendete ich viel Zeit damit, herauszufinden, was zu einer unendlichen Rekursion führte. Diese Punkte werden in anderen Antworten hier und anderswo erwähnt , aber ich möchte sie hier nur noch einmal hervorheben.
Meine vollständige schnelle Antwort mit Schritten ist hier .
quelle
Es gibt eine Lösung, die viel sauberer ist als die oben genannten: https://www.youtube.com/watch?v=xP7YvdlnHfA
Keine Runtime-Eigenschaften, überhaupt kein Problem mit rekursiven Aufrufen. Ich habe es versucht und es hat wie ein Zauber funktioniert, wenn ich Storyboard und XIB mit IBOutlet-Eigenschaften (iOS8.1, XCode6) verwendet habe.
Viel Glück beim Codieren!
quelle