Chcę użyć widoku w wielu kontrolerach widoku w scenorysie. Dlatego pomyślałem o zaprojektowaniu widoku w zewnętrznym xib, aby zmiany były odzwierciedlane w każdym kontrolerze widoku. Ale jak można załadować widok z zewnętrznego XIB w scenorysie i czy jest to w ogóle możliwe? Jeśli tak nie jest, jakie inne alternatywy są dostępne, aby dopasować się do powyższej sytuacji?
ios
uiview
storyboard
xib
Sebastian Hoffmann
źródło
źródło
Odpowiedzi:
Mój pełny przykład jest tutaj , ale poniżej przedstawię podsumowanie.
Układ
Dodaj do projektu pliki .swift i .xib o tej samej nazwie. Plik .xib zawiera niestandardowy układ widoku (najlepiej przy użyciu ograniczeń automatycznego układu).
Spraw, aby plik Swift był właścicielem pliku xib.
Kod
Dodaj następujący kod do pliku .swift i podłącz wyjścia i akcje z pliku .xib.
import UIKit 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) 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 } }
Użyj tego
Użyj swojego niestandardowego widoku w dowolnym miejscu w swojej serii ujęć. Po prostu dodaj
UIView
i ustaw nazwę klasy na własną nazwę klasy.źródło
init(frame:)
. Więcej informacji znajdziesz w tym samouczku .Przez chwilę podejście Christophera Swaseya było najlepszym, jakie znalazłem. Zapytałem o to kilku starszych programistów z mojego zespołu i jeden z nich znalazł idealne rozwiązanie ! Spełnia wszystkie obawy, do których tak elokwentnie odniósł się Christopher Swasey, i nie wymaga standardowego kodu podklasy (mój główny problem związany z jego podejściem). Jest jedna gotcha , ale poza tym jest dość intuicyjna i łatwa do wdrożenia.
MyCustomClass.swift
MyCustomClass.xib
File's Owner
plik .xib jako klasę niestandardową (MyCustomClass
)class
wartość (podidentity Inspector
) dla własnego widoku w pliku .xib pustą. Twój widok niestandardowy nie będzie miał określonej klasy, ale będzie miał określonego właściciela pliku.Assistant Editor
.Connections Inspector
, zauważysz, że twoje referencyjne punkty sprzedaży nie odnoszą się do twojej niestandardowej klasy (tj.MyCustomClass
), Ale raczej do odniesieniaFile's Owner
. PonieważFile's Owner
jest określona jako klasa niestandardowa, gniazda będą się podłączać i działać prawidłowo.NibLoadable
protokołem wymienionym poniżej..swift
nazwa pliku klasy niestandardowej różni się od.xib
nazwy pliku, ustawnibName
właściwość na nazwę.xib
pliku.required init?(coder aDecoder: NSCoder)
ioverride init(frame: CGRect)
zadzwońsetupFromNib()
jak w przykładzie poniżej.MyCustomClass
.).Oto protokół, do którego chcesz się odwołać:
public protocol NibLoadable { static var nibName: String { get } } public extension NibLoadable where Self: UIView { public static var nibName: String { return String(describing: Self.self) // defaults to the name of the class implementing this protocol. } public static var nib: UINib { let bundle = Bundle(for: Self.self) return UINib(nibName: Self.nibName, bundle: bundle) } func setupFromNib() { guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") } addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true } }
A oto przykład
MyCustomClass
implementacji protokołu (z nazwą pliku .xibMyCustomClass.xib
):@IBDesignable class MyCustomClass: UIView, NibLoadable { @IBOutlet weak var myLabel: UILabel! required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupFromNib() } override init(frame: CGRect) { super.init(frame: frame) setupFromNib() } }
UWAGA: Jeśli przegapisz Gotcha i ustawisz
class
wartość w pliku .xib jako klasę niestandardową, nie będzie ona rysowana w scenorysie i pojawi sięEXC_BAD_ACCESS
błąd po uruchomieniu aplikacji, ponieważ utknie w nieskończonej pętli próbuje zainicjować klasę z nib przy użyciuinit?(coder aDecoder: NSCoder)
metody, która następnie wywołujeSelf.nib.instantiate
iinit
ponownie wywołuje .źródło
setupFromNib()
, wydaje się naprawiać pewne dziwne problemy z automatycznym układem z automatycznym dopasowywaniem rozmiaru komórek widoku tabeli zawierających widoki utworzone przez XIB.@IBDesignable
kompatybilność. Nie mogę uwierzyć, dlaczego Xcode lub UIKit nie zapewniają czegoś takiego domyślnie podczas dodawania pliku UIView.Zakładając, że utworzyłeś xib, którego chcesz użyć:
1) Utwórz niestandardową podklasę UIView (możesz przejść do Plik -> Nowy -> Plik ... -> Klasa Cocoa Touch. Upewnij się, że „Podklasa:” to „UIView”).
2) Dodaj widok oparty na xib jako podwidok do tego widoku podczas inicjalizacji.
W Obj-C
-(id)initWithCoder:(NSCoder *)aDecoder{ if (self = [super initWithCoder:aDecoder]) { UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename" owner:self options:nil] objectAtIndex:0]; xibView.frame = self.bounds; xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview: xibView]; } return self; }
W Swift 2
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView xibView.frame = self.bounds xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] self.addSubview(xibView) }
W Swift 3
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView xibView.frame = self.bounds xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.addSubview(xibView) }
3) Gdziekolwiek chcesz go użyć w swoim storyboardzie, dodaj UIView jak zwykle, wybierz nowo dodany widok, przejdź do Inspektora tożsamości (trzecia ikona w prawym górnym rogu, która wygląda jak prostokąt z liniami), i wprowadź nazwę swojej podklasy w polu „Klasa” w sekcji „Klasa niestandardowa”.
źródło
xibView.frame = self.frame;
powinien byćxibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
, w przeciwnym razie xibView będzie miał przesunięcie, gdy widok zostanie dodany do scenorysu.Zawsze uważałem, że rozwiązanie „dodaj to jako podwidok” nie jest satysfakcjonujące, biorąc pod uwagę, że wkręca się w (1) autoukładanie, (2)
@IBInspectable
i (3) gniazda. Zamiast tego, pozwól mi przedstawić Wam magięawakeAfter:
, oNSObject
metodzie.awakeAfter
pozwala całkowicie zamienić obiekt faktycznie obudzony z NIB / Storyboard na inny obiekt. Ten obiekt jest następnie poddawany procesowi hydratacji,awakeFromNib
wywoływany, dodawany jako widok itp.Możemy to wykorzystać w podklasie "wycinanej tektury" naszego widoku, której jedynym celem będzie załadowanie widoku z NIB i zwrócenie go do użycia w Storyboard. Podklasa do osadzania jest następnie określana w inspektorze tożsamości widoku Storyboard, a nie w oryginalnej klasie. W rzeczywistości nie musi to być podklasa, aby to zadziałało, ale uczynienie z niej podklasy jest tym, co pozwala IB zobaczyć wszystkie właściwości IBInspectable / IBOutlet.
Ten dodatkowy szablon może wydawać się nieoptymalny - iw pewnym sensie tak jest, ponieważ idealnie
UIStoryboard
by sobie z tym poradził - ale ma tę zaletę, że pozostawia oryginalny NIB iUIView
podklasę całkowicie niezmienioną. Rola, jaką odgrywa, jest zasadniczo rolą klasy adaptera lub mostka i jest całkowicie słuszna, jeśli chodzi o projekt, jako klasa dodatkowa, nawet jeśli jest to godne pożałowania. Z drugiej strony, jeśli wolisz być oszczędny w swoich klasach, rozwiązanie @ BenPatch działa poprzez implementację protokołu z kilkoma innymi drobnymi zmianami. Pytanie, które rozwiązanie jest lepsze, sprowadza się do kwestii stylu programisty: czy preferuje się kompozycję obiektów, czy wielokrotne dziedziczenie.Uwaga: klasa ustawiona w widoku w pliku NIB pozostaje taka sama. Podklasa osadzalna jest używana tylko w scenorysie. Podklasy nie można użyć do utworzenia wystąpienia widoku w kodzie, więc sama nie powinna mieć żadnej dodatkowej logiki. Powinien zawierać tylko
awakeAfter
haczyk.class MyCustomEmbeddableView: MyCustomView { override func awakeAfter(using aDecoder: NSCoder) -> Any? { return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any } }
⚠️ Jedyną istotną wadą jest to, że jeśli zdefiniujesz ograniczenia szerokości, wysokości lub proporcji w scenorysie, które nie odnoszą się do innego widoku, musisz je ręcznie skopiować. Ograniczenia, które odnoszą się do dwóch widoków, są instalowane na najbliższym wspólnym przodku, a widoki są budzone z serii ujęć od wewnątrz na zewnątrz, więc zanim te ograniczenia zostaną uwodnione w nadzorze, zamiana już nastąpiła. Ograniczenia, które dotyczą tylko danego widoku, są instalowane bezpośrednio w tym widoku, a zatem są odrzucane, gdy nastąpi zamiana, chyba że zostaną skopiowane.
Zauważ, że to, co się tutaj dzieje, to ograniczenia zainstalowane w widoku w scenorysie są kopiowane do nowo utworzonego widoku , który może już mieć własne ograniczenia, zdefiniowane w pliku końcówki. Te są nienaruszone.
class MyCustomEmbeddableView: MyCustomView { override func awakeAfter(using aDecoder: NSCoder) -> Any? { let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! for constraint in constraints { if constraint.secondItem != nil { newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant)) } else { newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant)) } } return newView as Any } }
instantiateViewFromNib
jest bezpiecznym rozszerzeniem doUIView
. Wszystko, co robi, to pętla po obiektach NIB, dopóki nie znajdzie takiego, który pasuje do typu. Zauważ, że typ rodzajowy jest powrót wartość, więc typ musi być określona w miejscu wywołania.extension UIView { public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? { if let objects = bundle.loadNibNamed(nibName, owner: nil) { for object in objects { if let object = object as? T { return object } } } return nil } }
źródło
instantiateViewFromNib
nic nie wróci. W każdym razie nie jest to wielka sprawa IMO, podklasa to tylko urządzenie do podłączenia do scenorysu, cały kod powinien znajdować się w oryginalnej klasie.MyCustomView
. W moim xib domyślnie brakowało lewego wewnętrznego paska bocznego; aby go włączyć, znajduje się przycisk obok opcji „Wyświetl jako: iPhone 7” w pobliżu dolnej / lewej strony.Myślę o
alternative
za korzystanieXIB views
aby używaćView Controller
w osobnej serii ujęć .Następnie w głównym ujęć zamiast widoku niestandardowego użycia
container view
zEmbed Segue
i mająStoryboardReference
do tego niestandardowego widoku kontrolera , który widok powinien być umieszczony wewnątrz innego widoku w głównym ujęć.Następnie możemy skonfigurować delegowanie i komunikację między tym osadzonym ViewController i głównym kontrolerem widoku poprzez przygotowanie do płynności . To podejście różni się od wyświetlania UIView, ale znacznie prostsze i wydajniejsze (z punktu widzenia programowania) można wykorzystać do osiągnięcia tego samego celu, tj. Uzyskania widoku niestandardowego wielokrotnego użytku, który jest widoczny w głównym scenariuszu
Dodatkową zaletą jest to, że można zaimplementować logikę w klasie CustomViewController i tam skonfigurować całe delegowanie i przygotowanie widoku bez tworzenia oddzielnych (trudniejszych do znalezienia w projekcie) klas kontrolera i bez umieszczania standardowego kodu w głównym UIViewController przy użyciu Component. Myślę, że jest to dobre dla komponentów wielokrotnego użytku, np. Komponent Music Player (podobny do widżetu), który można osadzać w innych widokach.
źródło
Obecnie najlepszym rozwiązaniem jest użycie niestandardowego kontrolera widoku z jego widokiem zdefiniowanym w xib i po prostu usunięcie właściwości „widoku”, którą Xcode tworzy wewnątrz serii ujęć podczas dodawania do niego kontrolera widoku (nie zapomnij ustawić nazwy klasa niestandardowa).
Spowoduje to, że środowisko wykonawcze automatycznie wyszuka plik xib i załaduje go. Możesz użyć tej sztuczki do dowolnego widoku kontenera lub widoku zawartości.
źródło
Chociaż najpopularniejsze odpowiedzi działają dobrze, są koncepcyjnie błędne. Wszystkie używają
File's owner
jako połączenia między gniazdami klasy a komponentami interfejsu użytkownika.File's owner
ma być używany tylko dla obiektów najwyższego poziomu,UIView
a nie s. Sprawdź dokument programisty Apple . Posiadanie UIView jakoFile's owner
prowadzi do tych niepożądanych konsekwencji.contentView
tam, gdzie powinieneś używaćself
. Jest nie tylko brzydki, ale także strukturalnie niepoprawny, ponieważ widok pośredni uniemożliwia strukturze danych przekazywanie struktury interfejsu użytkownika. To jest jak przeciwieństwo deklaratywnego interfejsu użytkownika.Jest elegancki sposób na zrobienie tego bez użycia
File's owner
. Sprawdź ten wpis na blogu . Wyjaśnia, jak to zrobić we właściwy sposób.źródło
Rozwiązanie dla celu C zgodnie z krokami opisanymi w odpowiedzi Bena Patcha .
Użyj rozszerzenia dla UIView:
@implementation UIView (NibLoadable) - (UIView*)loadFromNib { UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject]; xibView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:xibView]; [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES; [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES; [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES; [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES; return xibView; } @end
Tworzenie plików
MyView.h
,MyView.m
aMyView.xib
.Najpierw przygotuj się
MyView.xib
tak, jak mówi odpowiedź Bena Patcha, więc ustaw klasęMyView
dla właściciela pliku zamiast głównego widoku wewnątrz tego XIB.MyView.h
:#import <UIKit/UIKit.h> IB_DESIGNABLE @interface MyView : UIView @property (nonatomic, weak) IBOutlet UIView* someSubview; @end
MyView.m
:#import "MyView.h" #import "UIView+NibLoadable.h" @implementation MyView #pragma mark - Initializers - (id)init { self = [super init]; if (self) { [self loadFromNib]; [self internalInit]; } return self; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self loadFromNib]; [self internalInit]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self loadFromNib]; } return self; } - (void)awakeFromNib { [super awakeFromNib]; [self internalInit]; } - (void)internalInit { // Custom initialization. } @end
A później po prostu utwórz widok programowo:
MyView* view = [[MyView alloc] init];
Ostrzeżenie! Podgląd tego widoku nie będzie wyświetlany w Storyboard, jeśli używasz rozszerzenia WatchKit z powodu tego błędu w Xcode> = 9.2: https://forums.developer.apple.com/thread/95616
źródło
Oto odpowiedź, której szukałeś przez cały czas. Możesz po prostu utworzyć swoją
CustomView
klasę, umieścić jej główną instancję w pliku xib ze wszystkimi widokami podrzędnymi i gniazdami. Następnie możesz zastosować tę klasę do dowolnych wystąpień w swoich scenorysach lub innych plikach xib.Nie trzeba majstrować przy właścicielu pliku, podłączać gniazd do serwera proxy lub modyfikować xib w szczególny sposób ani dodawać instancji własnego widoku jako widoku podrzędnego samego siebie.
Po prostu zrób to:
UIView
naNibView
(lub zUITableViewCell
naNibTableViewCell
)Otóż to!
Działa nawet z IBDesignable, aby odnieść swój niestandardowy widok (w tym podglądy z xib) w czasie projektowania do scenorysu.
Możesz przeczytać więcej na ten temat tutaj: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155
Możesz pobrać platformę BFWControls typu open source tutaj: https://github.com/BareFeetWare/BFWControls
A oto prosty fragment
NibReplaceable
kodu, który go napędza, na wypadek, gdybyś był ciekawy: https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4Tom 👣
źródło
To rozwiązanie może być użyte, nawet jeśli twoja klasa nie ma takiej samej nazwy jak XIB. Na przykład, jeśli masz kontroler klasy kontrolera A widoku bazowego, który ma nazwę kontrolera XIB controllerA.xib i podklasowałeś to z kontroleremB i chcesz utworzyć wystąpienie kontroleraB w scenorysie, możesz:
*
- (void) loadView { //according to the documentation, if a nibName was passed in initWithNibName or //this controller was created from a storyboard (and the controller has a view), then nibname will be set //else it will be nil if (self.nibName) { //a nib was specified, respect that [super loadView]; } else { //if no nib name, first try a nib which would have the same name as the class //if that fails, force to load from the base class nib //this is convenient for including a subclass of this controller //in a storyboard NSString *className = NSStringFromClass([self class]); NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"]; UINib *nib ; if (pathToNIB) { nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]]; } else { //force to load from nib so that all subclass will have the correct xib //this is convenient for including a subclass //in a storyboard nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]]; } self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0]; } }
źródło