OK, w StackOverflow są dziesiątki postów na ten temat, ale żaden nie jest szczególnie jasny na temat rozwiązania. Chciałbym utworzyć niestandardowy UIView
z towarzyszącym mu plikiem xib. Wymagania są następujące:
- Brak osobnych
UIViewController
- całkowicie samodzielna klasa - Gniazda w klasie, aby umożliwić mi ustawienie / pobranie właściwości widoku
Moje obecne podejście do tego jest następujące:
Nadpisanie
-(id)initWithFrame:
-(id)initWithFrame:(CGRect)frame { self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] objectAtIndex:0]; self.frame = frame; return self; }
Utwórz wystąpienie programowo, używając
-(id)initWithFrame:
w moim kontrolerze widokuMyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)]; [self.view insertSubview:myCustomView atIndex:0];
Działa to dobrze (chociaż nigdy nie wywołujesz [super init]
i proste ustawienie obiektu przy użyciu zawartości załadowanej stalówki wydaje się nieco podejrzane - jest tutaj rada, aby dodać podwidok w tym przypadku, który również działa dobrze). Chciałbym jednak móc również utworzyć wystąpienie widoku z serii ujęć. Więc mogę:
- Umieść
UIView
w widoku rodzica w serii ujęć - Ustaw jego klasę niestandardową na
MyCustomView
Zastąp
-(id)initWithCoder:
- kod, który widziałem najczęściej, pasuje do wzorca takiego jak:-(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]; }
Oczywiście to nie działa, ponieważ niezależnie od tego, czy używam powyższego podejścia, czy też tworzę wystąpienia programowo, oba kończą się rekurencyjnie wywołaniami -(id)initWithCoder:
po wprowadzeniu -(void)initializeSubviews
i załadowaniu nib z pliku.
Kilka innych pytań SO dotyczy tego, na przykład tutaj , tutaj , tutaj i tutaj . Jednak żadna z udzielonych odpowiedzi w sposób zadowalający nie rozwiązuje problemu:
- Powszechną sugestią wydaje się być osadzenie całej klasy w kontrolerze UIViewController i ładowanie końcówki w tym miejscu, ale wydaje mi się to nieoptymalne, ponieważ wymaga dodania kolejnego pliku jako opakowania
Czy ktokolwiek mógłby udzielić porady, jak rozwiązać ten problem i uzyskać działające gniazdka w niestandardowy sposób UIView
przy minimalnym zamieszaniu / bez cienkiego opakowania kontrolera? A może istnieje alternatywny, czystszy sposób robienia rzeczy z minimalnym kodem standardowym?
źródło
Odpowiedzi:
Twój problem dzwoni
loadNibNamed:
z (potomka)initWithCoder:
.loadNibNamed:
połączenia wewnętrzneinitWithCoder:
. Jeśli chcesz zastąpić koder scenorysów i zawsze ładować implementację xib, sugeruję następującą technikę. Dodaj właściwość do swojej klasy widoku iw pliku XIB ustaw ją na z góry określoną wartość (w atrybutach czasu wykonywania zdefiniowanych przez użytkownika). Teraz po wywołaniu[super initWithCoder:aDecoder];
sprawdź wartość właściwości. Jeśli jest to z góry określona wartość, nie dzwoń[self initializeSubviews];
.Więc coś takiego:
-(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; }
źródło
Zwróć uwagę, że ta kontrola jakości (jak wiele innych) ma tak naprawdę znaczenie historyczne.
ObecnieOd lat w iOS wszystko jest tylko widokiem kontenera. Pełny samouczek tutaj(Rzeczywiście, Apple w końcu dodało Referencje Storyboard , jakiś czas temu, co znacznie ułatwia.)
Oto typowa plansza z widokami kontenerów na całym świecie. Wszystko jest widokiem kontenera. Po prostu tworzysz aplikacje.
(Ciekawostką jest, że odpowiedź KenCa pokazuje dokładnie, w jaki sposób ładowano xib do pewnego rodzaju widoku opakowującego, ponieważ tak naprawdę nie można „przypisać do siebie”).
źródło
Dodam to jako osobny post, aby zaktualizować sytuację w wydaniu Swift. Podejście opisane przez LeoNatana doskonale sprawdza się w Objective-C. Jednak bardziej rygorystyczne kontrole czasu kompilacji zapobiegają
self
przypisywaniu go podczas ładowania z pliku xib w języku Swift.W rezultacie nie ma innego wyjścia, jak tylko dodać widok załadowany z pliku xib jako widok podklasy niestandardowej podklasy UIView, zamiast całkowicie zastępować siebie. Jest to analogiczne do drugiego podejścia przedstawionego w pierwotnym pytaniu. Ogólny zarys klasy w Swift korzystającej z tego podejścia jest następujący:
@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 } }
Wadą tego podejścia jest wprowadzenie dodatkowej nadmiarowej warstwy w hierarchii widoków, która nie istnieje w przypadku podejścia nakreślonego przez LeoNatana w Objective-C. Jednak może to być odebrane jako zło konieczne i produkt podstawowego sposobu projektowania rzeczy w Xcode (nadal wydaje mi się szalone, że tak trudno jest połączyć niestandardową klasę UIView z układem interfejsu użytkownika w sposób, który działa konsekwentnie na obu scenorysach iz kodu) - zastępowanie
self
hurtowego w inicjatorze nigdy wcześniej nie wydawało się szczególnie interpretowalnym sposobem robienia rzeczy, chociaż zasadniczo posiadanie dwóch klas widoku na widok też nie wydaje się takie świetne.Niemniej jednak szczęśliwym rezultatem tego podejścia jest to, że nie musimy już ustawiać niestandardowej klasy widoku na nasz plik klasy w konstruktorze interfejsu, aby zapewnić poprawne zachowanie podczas przypisywania do
self
, więc rekurencyjne wywołanieinit(coder aDecoder: NSCoder)
przy wydawaniuloadNibNamed()
jest zepsute (nie ustawiając niestandardowa klasa w pliku xib,init(coder aDecoder: NSCoder)
zamiast tego zostanie wywołana zwykła wanilia UIView, a nie nasza niestandardowa wersja).Mimo że nie możemy bezpośrednio dostosowywać klas do widoku przechowywanego w xib, nadal jesteśmy w stanie połączyć ten widok z naszą „nadrzędną” podklasą UIView za pomocą gniazd / akcji itp. Po ustawieniu właściciela pliku widoku na naszą niestandardową klasę:
Film demonstrujący implementację takiej klasy widoku krok po kroku przy użyciu tego podejścia można znaleźć na poniższym filmie .
źródło
UIView
. Zgadzam się, że to szaleństwo, że Apple nigdy nie ułatwiało tego, a teraz jest to praktycznie niemożliwe. Pojemnik nie zawsze jest odpowiedzią.KROK 1. Wymiana
self
z StoryboardWymiana
self
winitWithCoder:
sposób nie powiedzie się następujący błąd.'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'
Zamiast tego możesz zamienić zdekodowany obiekt na
awakeAfterUsingCoder:
(nieawakeFromNib
). lubić:@implementation MyCustomView - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder { return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil] objectAtIndex:0]; } @end
KROK 2. Zapobieganie wywołaniom rekurencyjnym
Oczywiście powoduje to również problem z połączeniami rekurencyjnymi. (dekodowanie scenorysu ->
awakeAfterUsingCoder:
->loadNibNamed:
->awakeAfterUsingCoder:
->loadNibNamed:
-> ...)Więc musisz sprawdzić, czy bieżący
awakeAfterUsingCoder:
jest wywoływany w procesie dekodowania Storyboard lub w procesie dekodowania XIB. Możesz to zrobić na kilka sposobów:a) Użyj prywatnego
@property
który jest ustawiony tylko w NIB.@interface MyCustomView : UIView @property (assign, nonatomic) BOOL xib @end
i ustaw „User Defined Runtime Attributes” tylko w „MyCustomView.xib”.
Plusy:
Cons:
setXib:
zostanie wywołany POawakeAfterUsingCoder:
b) Sprawdź, czy
self
ma jakieś podglądyZwykle masz podglądy w xib, ale nie w scenorysie.
- (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]; } }
Plusy:
Cons:
c) Ustaw statyczną flagę podczas
loadNibNamed:
rozmowystatic 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; } }
Plusy:
Cons:
d) Użyj prywatnej podklasy w XIB
Na przykład zadeklaruj
_NIB_MyCustomView
jako podklasę klasyMyCustomView
. I używaj_NIB_MyCustomView
zamiastMyCustomView
w swoim 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
Plusy:
if
wMyCustomView
Cons:
_NIB_
tricka w XIb konstruktora Interfacee) Użyj podklasy jako symbolu zastępczego w Storyboard
Podobnie jak w przypadku
d)
użycia podklasy w Storyboard, oryginalnej klasy w XIB.Tutaj deklarujemy
MyCustomViewProto
jako podklasę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
Plusy:
MyCustomView
.if
sprawdzenia tak samo jakd)
Cons:
Myślę, że
e)
to najbezpieczniejsza i najczystsza strategia. Więc przyjmujemy to tutaj.KROK 3. Kopiuj właściwości
Po
loadNibNamed:
w „awakeAfterUsingCoder:” musisz skopiować kilka właściwości, zself
których jest zdekodowana instancja Storyboard.frame
a właściwości autolayout / autoresize są szczególnie ważne.- (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; }
OSTATECZNE ROZWIĄZANIE
Jak widać, jest to fragment kodu standardowego. Możemy je zaimplementować jako „kategorię”. Tutaj rozszerzam powszechnie używany
UIView+loadFromNib
kod.#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]; }
Korzystając z tego, możesz zadeklarować
MyCustomViewProto
:@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:
Wynik:
źródło
Nie zapomnij
Dwie ważne kwestie:
Przeszedłem do tej strony z pytaniami i odpowiedziami kilka razy, ucząc się tworzenia widoków wielokrotnego użytku. Zapomnienie o powyższych punktach sprawiło, że zmarnowałem dużo czasu na próbę ustalenia, co powoduje nieskończoną rekurencję. Kwestie te są wspomniane w innych odpowiedziach tutaj i gdzie indziej , ale chcę je tutaj ponownie podkreślić.
Moja pełna odpowiedź Swift z krokami jest tutaj .
źródło
Istnieje rozwiązanie znacznie czystsze od powyższych: https://www.youtube.com/watch?v=xP7YvdlnHfA
Brak właściwości środowiska wykonawczego, żaden problem z połączeniami rekurencyjnymi. Wypróbowałem to i zadziałało jak urok, używając ze scenorysu i XIB z właściwościami IBOutlet (iOS8.1, XCode6).
Powodzenia w programowaniu!
źródło