Jak utworzyć niestandardową klasę widoku systemu iOS i utworzyć jej wystąpienie wielu kopii (w IB)?

114

Obecnie tworzę aplikację, która będzie miała wiele timerów, które w zasadzie są takie same.

Chcę utworzyć niestandardową klasę, która używa całego kodu timerów, a także układu / animacji, więc mogę mieć 5 identycznych timerów, które działają niezależnie od siebie.

Układ utworzyłem za pomocą IB (xcode 4.2), a cały kod timerów jest obecnie tylko w klasie viewcontroller.

Mam trudność w zrozumieniu tego, jak zawrzeć wszystko w niestandardowej klasie, a następnie dodać to do kontrolera widoku, każda pomoc byłaby bardzo mile widziana.

Michael Campsall
źródło
Dla każdego, kto szuka go w Google ... minęło już pięć lat od pojawienia się widoków kontenerów na iOS! Widoki kontenerów to podstawowa, centralna koncepcja tego, jak teraz robisz wszystko w iOS. Są bardzo proste w użyciu, a istnieją dowolną liczbę doskonałych (pięć latek!) Tutoriali na widoki wokół pojemników, przykład , przykład
fattie
2
@JoeBlow Z wyjątkiem tego, że nie możesz użyć widoku kontenera w UITableViewCelllub UICollectionViewCell. W moim przypadku potrzebuję małego, ale dość złożonego widoku, którego mogę używać wielokrotnie w kontrolerach i widokach kolekcji. Projektanci ciągle go przerabiają, więc chcę, aby układ był zdefiniowany w jednym miejscu. Stąd stalówka.
Echelon,

Odpowiedzi:

142

Cóż, odpowiadając koncepcyjnie, twój licznik czasu powinien być prawdopodobnie podklasą UIViewzamiast NSObject.

Aby utworzyć instancję timera w IB, po prostu przeciągnij i UIViewupuść ją w widoku kontrolera widoku i ustaw jej klasę na nazwę klasy timera.

wprowadź opis obrazu tutaj

Pamiętaj by #import klasie licznika czasu w kontrolerze widoku.

Edycja: dla projektu IB (dla tworzenia instancji kodu zobacz historię wersji)

W ogóle nie jestem zaznajomiony ze scenorysami, ale wiem, że możesz skonstruować swój interfejs w IB, używając .xibpliku, który jest prawie identyczny z używaniem wersji storyboardowej; Powinieneś nawet być w stanie skopiować i wkleić swoje widoki jako całość z istniejącego interfejsu do.xib pliku.

Aby to przetestować, utworzyłem nowy pusty o .xibnazwie „MyCustomTimerView.xib”. Następnie dodałem widok, a do tego dodałem etykietę i dwa przyciski. Tak jak tak:

wprowadź opis obrazu tutaj

Stworzyłem nową podklasę klasy celu C o UIViewnazwie „MyCustomTimer”. W moim .xibustawiłem klasę właściciela mojego pliku na MyCustomTimer . Teraz mogę łączyć akcje i gniazda, tak jak każdy inny widok / kontroler. Wynikowy .hplik wygląda następująco:

@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

Jedyną przeszkodą, jaka mi pozostała, jest uzyskanie tego .xibw mojej UIViewpodklasie. Używanie a .xibradykalnie zmniejsza wymaganą konfigurację. A ponieważ używasz scenorysów do ładowania timerów, o których wiemy, że -(id)initWithCoder:jest to jedyny inicjator, który zostanie wywołany. Oto jak wygląda plik implementacji:

#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

Metoda o nazwie loadNibNamed:owner:options: robi dokładnie to, na co wygląda. Ładuje końcówkę i ustawia właściwość „Właściciel pliku” na siebie. Wyodrębniamy pierwszy obiekt w tablicy i jest to widok główny stalówki. Dodajemy widok jako podwidok i Voila jest na ekranie.

Oczywiście zmienia to tylko kolor tła etykiety, gdy naciskane są przyciski, ale ten przykład powinien pomóc Ci dobrze działać.

Uwagi na podstawie komentarzy:

Warto zauważyć, że jeśli masz problemy z nieskończoną rekurencją, prawdopodobnie przegapiłeś subtelną sztuczkę tego rozwiązania. Nie robi tego, co myślisz, że robi. Widok umieszczony w serii ujęć nie jest widoczny, ale zamiast tego ładuje inny widok jako widok podrzędny. Widok, który ładuje, jest widokiem zdefiniowanym w końcówce. „Właścicielem pliku” w końcówce jest ten niewidoczny widok. Fajne jest to, że ten niewidoczny widok jest nadal klasą Objective-C, która może być używana jako swego rodzaju kontroler widoku dla widoku, który przynosi ze stalówki. Na przykład IBActionmetody w MyCustomTimerklasie są czymś, czego można oczekiwać bardziej w kontrolerze widoku niż w widoku.

Na marginesie, niektórzy mogą twierdzić, że to się psuje MVCi częściowo się z tym zgadzam. Z mojego punktu widzenia jest to bardziej związane z niestandardowym UITableViewCell, który czasami musi być częściowo kontrolerem.

Warto też zauważyć, że ta odpowiedź miała dostarczyć bardzo konkretnego rozwiązania; utwórz jedną końcówkę, której wystąpienie można utworzyć wiele razy w tym samym widoku, co w scenorysie. Na przykład, możesz łatwo wyobrazić sobie sześć z tych timerów jednocześnie na ekranie iPada. Jeśli potrzebujesz tylko określić widok dla kontrolera widoku, który ma być używany wiele razy w Twojej aplikacji, to rozwiązanie dostarczone przez jyavenard na to pytanie jest prawie na pewno lepszym rozwiązaniem dla Ciebie.

NJones
źródło
Wydaje się, że to osobne pytanie. Ale myślę, że można na to szybko odpowiedzieć, więc proszę. Za każdym razem, gdy tworzysz nowe powiadomienie, jest to nowe powiadomienie, ale jeśli potrzebujesz, aby miały tę samą nazwę, chcesz dodać coś, aby je odróżnić, użyj userInfowłaściwości w powiadomieniu@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
Otrzymuję nieskończoną rekursję z initWithCoder. Jakieś pomysły?
zniknął
6
Przepraszam, że przywracam stary wątek, upewnienie się, że File's Ownerjest ustawiona na nazwę twojej klasy, rozwiązało mój problem z nieskończoną rekurencją.
themanatuf
20
Innym powodem nieskończonej rekurencji jest ustawienie widoku katalogu głównego w xib na klasę niestandardową. Nie chcesz tego robić, pozostaw to jako domyślne „UIView”
XY,
11
Jest tylko jedna rzecz, która zwraca moją uwagę w tym podejściu ... Czy nikomu nie przeszkadza, że ​​2 widoki są teraz w tej samej klasie? oryginalny plik .h / .m dziedziczący z UIView będącego pierwszym i robiąc [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Dodanie drugiego UIView ... To trochę wygląda dziwnie dla mnie, nikt inny nie czuje tego samego? Jeśli tak, to czy jest to coś stworzonego przez Apple? Czy może ktoś znalazł rozwiązanie, robiąc to?
SH
137

Szybki przykład

Zaktualizowano dla Xcode 10 i Swift 4

Oto podstawowy przewodnik. Pierwotnie wiele się tego nauczyłem, oglądając serię filmów na Youtube . Później zaktualizowałem moją odpowiedź na podstawie tego artykułu .

Dodaj pliki widoku niestandardowego

Poniższe dwa pliki utworzą widok niestandardowy:

  • Plik .xib zawierający układ
  • .swift jako UIViewpodklasa

Szczegóły dotyczące ich dodawania znajdują się poniżej.

Plik Xib

Dodaj plik .xib do projektu (Plik> Nowy> Plik ...> Interfejs użytkownika> Widok) . Dzwonię do siebie ReusableCustomView.xib.

Utwórz układ, który ma mieć niestandardowy widok. Jako przykład zrobię układ z a UILabeli a UIButton. Dobrym pomysłem jest użycie automatycznego układu, aby rzeczy zmieniały się automatycznie, niezależnie od tego, jaki rozmiar ustawisz później. (Użyłem Freeform dla rozmiaru xib w Inspektorze atrybutów, aby móc dostosować symulowane metryki, ale nie jest to konieczne.)

wprowadź opis obrazu tutaj

Szybki plik

Dodaj plik .swift do projektu (Plik> Nowy> Plik ...> Źródło> Swift Plik) . To jest podklasa UIViewi nazywam swoją ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Ustaw plik Swift jako właściciela

Wróć do pliku .xib i kliknij opcję „Właściciel pliku” w konspekcie dokumentu. W Inspektorze tożsamości wpisz nazwę swojego pliku .swift jako nazwę klasy niestandardowej.

wprowadź opis obrazu tutaj

Dodaj kod widoku niestandardowego

Zastąp ReusableCustomView.swiftzawartość pliku następującym kodem:

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

Upewnij się, że pisownia nazwy Twojego pliku .xib jest poprawna.

Podłącz gniazda i akcje

Podłącz gniazda i akcje, przeciągając kontrolki z etykiety i przycisku w układzie XIB do szybkiego kodu widoku niestandardowego.

Użyj własnego widoku

Twój widok niestandardowy jest teraz zakończony. Wszystko, co musisz zrobić, to dodać UIViewdowolne miejsce w swojej głównej planszy. Ustaw nazwę klasy widoku na ReusableCustomVieww Inspektorze tożsamości.

wprowadź opis obrazu tutaj

Suragch
źródło
31
Ważne, aby nie ustawiać klasy widoku Xib jako ResuableCustomView, ale właściciel tego Xib. W przeciwnym razie wystąpi błąd.
RealNmae
5
Tak, dobre przypomnienie. Nie wiem, ile czasu zmarnowałem, próbując rozwiązać problemy z nieskończoną rekurencją, spowodowane ustawieniem widoku Xib (a raczej właściciela pliku) na nazwę klasy.
Suragch
2
@TomCalmon, przerobiłem ten projekt w Xcode 8 z Swift 3 i Auto Layout działał dla mnie dobrze.
Suragch
6
Jeśli ktoś inny ma problem z renderowaniem widoku w IB, stwierdziłem, że zmiana Bundle.mainna Bundle(for: ...)załatwia sprawę. Najwyraźniej Bundle.mainjest niedostępny w czasie projektowania.
crizzis
4
Powodem, dla którego to nie działa z @IBDesignable, jest to, że nie zaimplementowałeś init(frame:). Po dodaniu tego (i wciągnięciu całego kodu inicjalizacyjnego do jakiejś commonInit()funkcji) działa dobrze i wyświetla się w IB. Zobacz więcej informacji tutaj: stackoverflow.com/questions/31265906/…
Dimitris
39

Odpowiedź dla kontrolerów widoku, a nie widoków :

Jest łatwiejszy sposób na załadowanie xib z storyboardu. Powiedzmy, że twój kontroler jest typu MyClassController, który dziedziczy po UIViewController.

Dodajesz UIViewControllerużywając IB w swoim storyboardzie; zmień typ klasy na MyClassController. Usuń widok, który został automatycznie dodany do serii ujęć.

Upewnij się, że XIB, który chcesz wywołać, nazywa się MyClassController.xib.

Gdy klasa zostanie utworzona podczas ładowania scenorysu, xib zostanie automatycznie załadowany. Przyczyną tego jest domyślna implementacja, UIViewControllerktóra wywołuje XIB o nazwie z nazwą klasy.

jyavenard
źródło
1
cały czas nadmuchuję niestandardowe widoki, ale nigdy nie znałem tej małej sztuczki, aby załadować ją do storyboardu. BARDZO docenione!
MrTristan
38
Pytanie dotyczy widoków, a nie kontrolerów widoku.
Ricardo
Dodano tytuł dla wyjaśnienia, „Odpowiedź dla kontrolerów widoku, a nie widoków:”
nacho4d
1
Wygląda na to, że nie działa w iOS 9.1. Jeśli usunę automatycznie utworzony (domyślny) widok, zawiesza się po powrocie z viewDidLoad VC. Nie sądzę, aby widok w XIB był łączony zamiast widoku domyślnego.
Jeff
1
Wyjątkiem jest 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Zwróć uwagę na whacko nibName. Używam poprawnej nazwy klasy dla stalówki w końcówce.
Jeff
16

To nie jest tak naprawdę odpowiedź, ale myślę, że warto podzielić się tym podejściem.

Cel C

  1. Importuj CustomViewWithXib.h i CustomViewWithXib.m do swojego projektu
  2. Utwórz pliki widoku niestandardowego o tej samej nazwie (.h / .m / .xib)
  3. Dziedzicz swoją klasę niestandardową z CustomViewWithXib

Szybki

  1. Importuj CustomViewWithXib.swift do swojego projektu
  2. Utwórz pliki widoku niestandardowego o tej samej nazwie (.swift i .xib)
  3. Dziedzicz swoją klasę niestandardową z CustomViewWithXib

Opcjonalny :

  1. Przejdź do pliku xib, ustaw właściciela z niestandardową nazwą klasy, jeśli chcesz połączyć niektóre elementy (aby uzyskać więcej informacji, zobacz część Uczyń plik Swift właścicielem odpowiedzi @Suragch)

To wszystko, teraz możesz dodać swój niestandardowy widok do swojego storyboardu i zostanie on pokazany :)

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

Można znaleźć kilka przykładów tutaj .

Mam nadzieję, że to pomoże.

HamzaGhazouani
źródło
więc jak możemy dodać iboutlet do widoku uicontrols
Amr Angry
1
Hej @AmrAngry, aby połączyć widoki, wykonaj 4 krok: przejdź do pliku xib, w polu „Właściciel pliku” ustaw swoją klasę, a następnie przeciągnij i upuść widoki do interfejsu, dotykając przycisku „ctrl”, aktualizuję kod źródłowy z tym przykładem, nie wahaj się, jeśli masz jakiekolwiek pytania, mam nadzieję, że to pomoże
HamzaGhazouani
Wielkie dzięki za powtórkę, już to robię, ale myślę, że mój problem nie dotyczy tej części. moim problemem jest to, że dodaję UIDatePIcker i próbuję dodać akcję docelową dla tej kontrolki z ViewController, aby ustawić, a zdarzenie uiControleer ma zmienioną wartość, ale metoda nigdy nie jest wywoływana / fire
Amr Angry
1
@AmrAngry Aktualizuję przykład dodając widok selektora, może to ci pomoże :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Dziękuję, szukałem tego przez długi czas.
MH175