Jak używać jednego kontrolera scenorysu uiviewcontroller dla wielu podklas

118

Powiedzmy, że mam scenorys, który zawiera UINavigationControllerjako początkowy kontroler widoku. Jego główny kontroler widoku jest podklasą UITableViewController, czyli BasicViewController. Ma to, IBActionco jest połączone z prawym przyciskiem nawigacyjnym paska nawigacji.

Stamtąd chciałbym użyć storyboardu jako szablonu dla innych widoków bez konieczności tworzenia dodatkowych scenorysów. Powiedzmy, że te widoki będą miały dokładnie ten sam interfejs, ale z głównym kontrolerem widoku klasy SpecificViewController1i SpecificViewController2które są podklasami BasicViewController.
Te dwa kontrolery widoku miałyby taką samą funkcjonalność i interfejs, z wyjątkiem IBActionmetody.
Wyglądałoby to tak:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Czy mogę coś takiego zrobić?
Czy mogę po prostu utworzyć instancję storyboardu, BasicViewControllerale mam główny kontroler widoku do podklasy SpecificViewController1i SpecificViewController2?

Dzięki.

verdy
źródło
3
Warto zauważyć, że można to zrobić za pomocą stalówki. Ale jeśli jesteś podobny do mnie, który chcesz mieć fajne funkcje, które ma tylko storyboard (na przykład komórka statyczna / prototypowa), to chyba nie mamy szczęścia.
Joseph Lin

Odpowiedzi:

57

świetne pytanie - ale niestety tylko kiepska odpowiedź. Nie wierzę, że obecnie jest możliwe zrobienie tego, co proponujesz, ponieważ w UIStoryboard nie ma inicjatorów, które pozwalają na zastąpienie kontrolera widoku powiązanego ze scenorysem, jak określono w szczegółach obiektu w scenorysie podczas inicjalizacji. Podczas inicjalizacji wszystkie elementy interfejsu użytkownika w stoarybicy są połączone z ich właściwościami w kontrolerze widoku.

Domyślnie zainicjuje się za pomocą kontrolera widoku określonego w definicji scenorysu.

Jeśli próbujesz ponownie wykorzystać elementy interfejsu użytkownika, które utworzyłeś w scenorysie, nadal muszą być one połączone lub powiązane z właściwościami, w których kontroler widoku kiedykolwiek używa ich, aby móc „powiedzieć” kontrolerowi widoku o zdarzeniach.

Kopiowanie układu scenorysu nie jest aż tak wielkim problemem, zwłaszcza jeśli potrzebujesz podobnego projektu tylko dla 3 widoków, jednak jeśli to zrobisz, musisz upewnić się, że wszystkie poprzednie skojarzenia są wyczyszczone, w przeciwnym razie wystąpi awaria podczas próby komunikować się z poprzednim kontrolerem widoku. Będziesz mógł rozpoznać je jako komunikaty o błędach KVO w danych wyjściowych dziennika.

Kilka podejść, które możesz zastosować:

  • przechowuj elementy interfejsu użytkownika w UIView - w pliku xib i utwórz jego wystąpienie z klasy bazowej i dodaj jako widok podrzędny w widoku głównym, zwykle self.view. Następnie po prostu użyjesz układu scenorysu z zasadniczo pustymi kontrolerami widoku utrzymującymi swoje miejsce w serii ujęć, ale z przypisaną do nich poprawną podklasą kontrolera widoku. Ponieważ odziedziczyliby po bazie, uzyskaliby ten pogląd.

  • utwórz układ w kodzie i zainstaluj go z kontrolera widoku podstawowego. Oczywiście takie podejście jest sprzeczne z celem korzystania ze scenorysu, ale może być dobrym rozwiązaniem w Twoim przypadku. Jeśli masz inne części aplikacji, które skorzystałyby na podejściu do scenorysu, możesz w razie potrzeby odejść tu i tam. W takim przypadku, podobnie jak powyżej, wystarczy użyć kontrolerów widoku banku z przypisaną podklasą i pozwolić kontrolerowi widoku podstawowego zainstalować interfejs użytkownika.

Byłoby miło, gdyby Apple wymyślił sposób na zrobienie tego, co proponujesz, ale kwestia posiadania elementów graficznych wstępnie połączonych z podklasą kontrolera nadal byłaby problemem.

Miłego Nowego Roku !! Bywaj

CocoaEv
źródło
To było szybkie. Tak jak myślałem, nie byłoby to możliwe. Obecnie opracowuję rozwiązanie, mając tylko tę klasę BasicViewController i mam dodatkową właściwość wskazującą, która „klasa” / „tryb” będzie działać. W każdym razie dzięki.
verdy
2
szkoda :( Chyba muszę skopiować i wkleić ten sam kontroler widoku i zmienić jego klasę jako obejście.
Hlung
1
I dlatego nie lubię Storyboardów ... w jakiś sposób nie działają one tak naprawdę, gdy robisz trochę więcej niż standardowe widoki ...
TheEye
Tak smutno, kiedy to powiedziałeś. Szukam rozwiązania
Tony
2
Istnieje inne podejście: określ logikę niestandardową w różnych delegatach, a następnie w programie PreparatForSegue przypisz odpowiedniego delegata. W ten sposób utworzysz 1 UIViewController + 1 UIViewController w Storyboard, ale masz wiele wersji implementacji.
plam4u
45

Kod szukanej linii to:

object_setClass(AnyObject!, AnyClass!)

W Storyboard -> dodaj UIViewController, nadaj mu nazwę klasy ParentVC.

class ParentVC: UIViewController {

    var type: Int?

    override func awakeFromNib() {

        if type = 0 {

            object_setClass(self, ChildVC1.self)
        }
        if type = 1 {

            object_setClass(self, ChildVC2.self)
        }  
    }

    override func viewDidLoad() {   }
}

class ChildVC1: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 0
    }
}

class ChildVC2: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // Console prints out 1
    }
}
Jiří Zahálka
źródło
5
Dzięki, po prostu działa, na przykład:class func instantiate() -> SubClass { let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)! object_setClass(instance, SubClass.self) return (instance as? SubClass)! }
CocoaBob
3
Nie jestem pewien, czy rozumiem, jak to ma działać. Rodzic ustawia swoją klasę jako dziecko? Jak więc możesz mieć wiele dzieci ?!
user1366265
2
panie, zrobiłem mój dzień
jere
2
OK, więc pozwólcie, że wyjaśnię to trochę bardziej szczegółowo: co chcemy osiągnąć? Chcemy podklasować nasz ParentViewController, abyśmy mogli używać jego Storyboard dla większej liczby klas. Więc magiczna linia, która to wszystko robi, jest wyróżniona w moim rozwiązaniu i musi być używana w awakeFromNib w ParentVC. To, co się wtedy dzieje, polega na tym, że używa wszystkich metod z nowo ustawionego ChildVC1, który staje się podklasą. Jeśli chcesz go używać do większej liczby ChildVC? Po prostu wykonaj swoją logikę w awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Powodzenia.
Jiří Zahálka
10
Zachowaj ostrożność podczas korzystania z tego! Normalnie nie powinno to być w ogóle używane ... To po prostu zmienia wskaźnik isa danego wskaźnika i nie zmienia przydziału pamięci, aby pomieścić np. Różne właściwości. Jednym ze wskaźników jest to, że wskaźnik selfnie zmienia się. Zatem inspekcja obiektu (np. Odczyt wartości _ivar / property) później object_setClassmoże spowodować awarie.
Patrik
15

Zgodnie z przyjętą odpowiedzią nie wygląda na to, aby można było zrobić z storyboardami.

Moim rozwiązaniem jest użycie końcówek - tak jak deweloperzy używali ich przed storyboardami. Jeśli chcesz mieć kontroler widoku wielokrotnego użytku, z możliwością podklasy (lub nawet widok), zalecam użycie końcówek.

SubclassMyViewController *myViewController = [[SubclassMyViewController alloc] initWithNibName:@"MyViewController" bundle:nil]; 

Kiedy łączysz wszystkie swoje gniazda z "Właścicielem pliku" w polu MyViewController.xibNIE określasz, w jakiej klasie powinien być ładowany Nib, po prostu określasz pary klucz-wartość: " ten widok powinien być połączony z nazwą zmiennej instancji ." Podczas wywoływania [SubclassMyViewController alloc] initWithNibName:procesu inicjalizacji określa, jaki kontroler widoku będzie używany do „ sterowania ” widokiem utworzonym w końcówce.

kgaidis
źródło
Zaskakująco wydaje się, że jest to możliwe w przypadku scenorysów, dzięki bibliotece wykonawczej ObjC. Sprawdź moją odpowiedź tutaj: stackoverflow.com/a/57622836/7183675
Adam Tucholski
9

Możliwe jest, aby scenorys tworzył wystąpienie różnych podklas niestandardowego kontrolera widoku, chociaż wymaga to nieco niekonwencjonalnej techniki: przesłaniania allocmetody kontrolera widoku. Po utworzeniu kontrolera widoku niestandardowego, zastąpiona metoda alokacji w rzeczywistości zwraca wynik działania allocw podklasie.

Odpowiedź powinienem poprzedzić z zastrzeżeniem, że chociaż testowałem go w różnych scenariuszach i nie otrzymałem żadnych błędów, nie mogę zapewnić, że poradzi sobie z bardziej złożonymi konfiguracjami (ale nie widzę powodu, dla którego nie miałoby działać) . Ponadto nie przesłałem żadnych aplikacji przy użyciu tej metody, więc istnieje zewnętrzna szansa, że ​​może ona zostać odrzucona w procesie recenzji Apple (chociaż znowu nie widzę powodu, dla którego miałoby to robić).

Dla celów demonstracyjnych mam podklasę UIViewControllernazwaną TestViewController, która ma UILabel IBOutlet i IBAction. W moim storyboardzie dodałem kontroler widoku i zmieniłem jego klasę na TestViewControlleroraz podłączyłem IBOutlet do UILabel, a IBAction do UIButton. Przedstawiam TestViewController za pomocą modalnego segue wyzwalanego przez UIButton na poprzednim viewController.

Obraz scenorysu

Aby kontrolować, która klasa jest tworzona, dodałem zmienną statyczną i powiązane metody klas, więc pobierz / ustaw podklasę, która ma być używana (myślę, że można przyjąć inne sposoby określania, która podklasa ma zostać utworzona):

TestViewController.m:

#import "TestViewController.h"

@interface TestViewController ()
@end

@implementation TestViewController

static NSString *_classForStoryboard;

+(NSString *)classForStoryboard {
    return [_classForStoryboard copy];
}

+(void)setClassForStoryBoard:(NSString *)classString {
    if ([NSClassFromString(classString) isSubclassOfClass:[self class]]) {
        _classForStoryboard = [classString copy];
    } else {
        NSLog(@"Warning: %@ is not a subclass of %@, reverting to base class", classString, NSStringFromClass([self class]));
        _classForStoryboard = nil;
    }
}

+(instancetype)alloc {
    if (_classForStoryboard == nil) {
        return [super alloc];
    } else {
        if (NSClassFromString(_classForStoryboard) != [self class]) {
            TestViewController *subclassedVC = [NSClassFromString(_classForStoryboard) alloc];
            return subclassedVC;
        } else {
            return [super alloc];
        }
    }
}

Do mojego testu mam dwie podklasy TestViewController: RedTestViewControlleri GreenTestViewController. Każda z podklas ma dodatkowe właściwości i każda nadpisanie, viewDidLoadaby zmienić kolor tła widoku i zaktualizować tekst UILabel IBOutlet:

RedTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.view.backgroundColor = [UIColor redColor];
    self.testLabel.text = @"Set by RedTestVC";
}

GreenTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor greenColor];
    self.testLabel.text = @"Set by GreenTestVC";
}

Czasami mogę chcieć utworzyć instancję TestViewController, w innych przypadkach RedTestViewControllerlub GreenTestViewController. W powyższym kontrolerze widoku robię to losowo w następujący sposób:

NSInteger vcIndex = arc4random_uniform(4);
if (vcIndex == 0) {
    NSLog(@"Chose TestVC");
    [TestViewController setClassForStoryBoard:@"TestViewController"];
} else if (vcIndex == 1) {
    NSLog(@"Chose RedVC");
    [TestViewController setClassForStoryBoard:@"RedTestViewController"];
} else if (vcIndex == 2) {
    NSLog(@"Chose BlueVC");
    [TestViewController setClassForStoryBoard:@"BlueTestViewController"];
} else {
    NSLog(@"Chose GreenVC");
    [TestViewController setClassForStoryBoard:@"GreenTestViewController"];
}

Zwróć uwagę, że setClassForStoryBoardmetoda sprawdza, czy żądana nazwa klasy jest rzeczywiście podklasą TestViewController, aby uniknąć wszelkich pomyłek. Powyższe odniesienie BlueTestViewControllersłuży do testowania tej funkcji.

pbasdf
źródło
Zrobiliśmy coś podobnego w projekcie, ale przesłaniając metodę alokacji UIViewController w celu uzyskania podklasy z klasy zewnętrznej, zbierającej pełne informacje o wszystkich przesłonięciach. Działa świetnie.
Tim
Swoją drogą ta metoda może przestać działać tak szybko, jak Apple przestanie wywoływać alokację na kontrolerach widoku. Na przykład klasa NSManagedObject nigdy nie otrzymuje metody alokacji. Myślę, że Apple mógłby skopiować kod do innej metody: może + przydzielManagedObject
Tim
7

spróbuj tego, po wystąpieniu instantiateViewControllerWithIdentifier.

- (void)setClass:(Class)c {
    object_setClass(self, c);
}

lubić :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
莫 振华
źródło
Dodaj przydatne wyjaśnienie, co robi Twój kod.
codeforester
7
Co się z tym stanie, jeśli użyjesz zmiennych instancji z podklasy? Zgaduję, że się zawiesił, ponieważ nie ma wystarczającej ilości pamięci, aby to zmieścić. W moich testach dostaję EXC_BAD_ACCESS, więc nie polecam tego.
Bezległy
1
To nie zadziała, jeśli dodasz nowe zmienne w klasie potomnej. I dziecko initteż nie zostanie wezwane. Takie ograniczenia sprawiają, że każde podejście jest bezużyteczne.
Al Zonke
6

Bazując w szczególności na odpowiedziach nickgzzjr i Jiří Zahálka oraz komentarzu pod drugim z CocoaBob, przygotowałem krótką metodę generyczną, która robi dokładnie to, czego potrzebuje OP. Wystarczy sprawdzić nazwę serii ujęć i wyświetlić identyfikator serii ujęć kontrolerów

class func instantiate<T: BasicViewController>(as _: T.Type) -> T? {
        let storyboard = UIStoryboard(name: "StoryboardName", bundle: nil)
        guard let instance = storyboard.instantiateViewController(withIdentifier: "Identifier") as? BasicViewController else {
            return nil
        }
        object_setClass(instance, T.self)
        return instance as? T
    }

Dodawane są opcje, aby uniknąć wymuszonego rozwijania (ostrzeżenia swiftlint), ale metoda zwraca poprawne obiekty.

Adam Tucholski
źródło
5

Chociaż nie jest to ściśle podklasa, możesz:

  1. option-drag kontroler widoku klasy bazowej w konspekcie dokumentu, aby wykonać kopię
  2. Przenieś nową kopię kontrolera widoku w oddzielne miejsce w serii ujęć
  3. Zmień klasę na kontroler widoku podklasy w Inspektorze tożsamości

Oto przykład z samouczka Bloc, który napisałem, podklasy ViewControllerz WhiskeyViewController:

animacja powyższych trzech kroków

Pozwala to na tworzenie podklas podklas kontrolera widoku w scenorysie. Możesz następnie użyć instantiateViewControllerWithIdentifier:do utworzenia określonych podklas.

To podejście jest nieco nieelastyczne: późniejsze modyfikacje w scenorysie do kontrolera klasy bazowej nie są propagowane do podklasy. Jeśli masz wiele podklas, być może lepiej będzie, jeśli skorzystasz z jednego z pozostałych rozwiązań, ale to wystarczy.

Aaron Brager
źródło
11
To nie jest wiązanie podklasy, to po prostu powielanie ViewController.
Ace Green
1
To nie tak. Staje się podklasą po zmianie klasy na podklasę (krok 3). Następnie możesz wprowadzić dowolne zmiany i podłączyć się do gniazd / akcji w swojej podklasie.
Aaron Brager
6
Myślę, że nie rozumiesz koncepcji podklas.
Ace Green
5
Jeśli „późniejsze modyfikacje w scenorysie do kontrolera klasy bazowej nie są propagowane do podklasy”, nie jest to nazywane „podklasą”. To kopiuj i wklej.
superarts.org
Klasa bazowa, wybrana w Inspektorze tożsamości, nadal jest podklasą. Inicjowany obiekt i sterujący logiką biznesową nadal jest podklasą. Tylko zakodowane dane widoku, przechowywane jako XML w pliku scenorysu i zainicjowane przez initWithCoder:, nie mają odziedziczonej relacji. Ten typ relacji nie jest obsługiwany przez pliki scenorysu.
Aaron Brager
4

Metoda Objc_setclass nie tworzy instancji childvc. Ale podczas wyskakiwania z childvc, wywoływane jest deinit of childvc. Ponieważ nie ma pamięci przydzielonej oddzielnie dla childvc, aplikacja ulega awarii. Basecontroller ma instancję, podczas gdy podrzędny vc nie ma.

user8044830
źródło
2

Jeśli nie jesteś zbyt uzależniony od scenorysów, możesz utworzyć osobny plik .xib dla kontrolera.

Ustaw odpowiedniego właściciela pliku i wyjścia na MainViewControlleri nadpisz init(nibName:bundle:)w głównym VC, aby jego elementy podrzędne miały dostęp do tej samej stalówki i gniazd.

Twój kod powinien wyglądać następująco:

class MainViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: "MainViewController", bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .red
    }
}

A Twoje dziecko VC będzie mogło ponownie użyć stalówki swojego rodzica:

class ChildViewController: MainViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .blue
    }
}
azotawy
źródło
2

Biorąc odpowiedzi stąd i ówdzie, znalazłem to zgrabne rozwiązanie.

Utwórz nadrzędny kontroler widoku z tą funkcją.

class ParentViewController: UIViewController {


    func convert<T: ParentViewController>(to _: T.Type) {

        object_setClass(self, T.self)

    }

}

Dzięki temu kompilator może upewnić się, że podrzędny kontroler widoku dziedziczy po nadrzędnym kontrolerze widoku.

Następnie, gdy chcesz przejść do tego kontrolera za pomocą podklasy, możesz zrobić:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    if let parentViewController = segue.destination as? ParentViewController {
        ParentViewController.convert(to: ChildViewController.self)
    }

}

Fajne jest to, że możesz dodać do siebie odniesienie do scenorysu, a następnie nadal wywoływać „następny” podrzędny kontroler widoku.

nickgzzjr
źródło
1

Prawdopodobnie najbardziej elastycznym sposobem jest użycie widoków wielokrotnego użytku.

(Utwórz widok w oddzielnym pliku XIB lub Container viewi dodaj go do każdej sceny kontrolera widoku podklasy w serii ujęć)

DanSkeel
źródło
1
Prosimy o komentarz, gdy głosujesz przeciw. Wiem, że nie odpowiadam bezpośrednio na to pytanie, ale proponuję rozwiązanie problemu głównego.
DanSkeel
1

Jest proste, oczywiste, codzienne rozwiązanie.

Po prostu umieść istniejącą planszę / kontroler wewnątrz nowej planszy / kontrolera. IE jako widok kontenera.

Jest to dokładnie analogiczne pojęcie do „podklasy” dla kontrolerów widoku.

Wszystko działa dokładnie tak, jak w podklasie.

Tak jak zwykle umieszcza się widok podrzędny w innym widoku , naturalnie zwykle umieszcza się kontroler widoku w innym kontrolerze widoku .

Jak inaczej możesz to zrobić?

To podstawowa część iOS, tak prosta jak koncepcja „podwidoku”.

To takie proste ...

/*

Search screen is just a modification of our List screen.

*/

import UIKit

class Search: UIViewController {
    
    var list: List!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        list = (_sb("List") as! List
        addChild(list)
        view.addSubview(list.view)
        list.view.bindEdgesToSuperview()
        list.didMove(toParent: self)
    }
}

Teraz oczywiście musisz listrobić, co chcesz

list.mode = .blah
list.tableview.reloadData()
list.heading = 'Search!'
list.searchBar.isHidden = false

itd itd.

Widoki kontenerów są „tak jak” podklasy w taki sam sposób, jak „podklasy” są „tak samo jak” podklasy.

Oczywiście nie można „podklasować układu” - co to w ogóle oznacza?

(„Podklasy” odnoszą się do oprogramowania obiektowego i nie mają związku z „układami”).

Oczywiście, gdy chcesz ponownie użyć widoku, po prostu wyświetlasz go w innym widoku.

Jeśli chcesz ponownie użyć układu kontrolera, po prostu kontener przeglądaj go wewnątrz innego kontrolera.

To jest jak najbardziej podstawowy mechanizm iOS !!


Uwaga - od lat trywialne jest dynamiczne ładowanie innego kontrolera widoku jako widoku kontenera. Wyjaśniono w ostatniej sekcji: https://stackoverflow.com/a/23403979/294884

Uwaga - „_sb” to po prostu oczywiste makro, którego używamy, aby zaoszczędzić pisanie,

func _sb(_ s: String)->UIViewController {
    // by convention, for a screen "SomeScreen.storyboard" the
    // storyboardID must be SomeScreenID
    return UIStoryboard(name: s, bundle: nil)
       .instantiateViewController(withIdentifier: s + "ID")
}
Fattie
źródło
1

Dzięki za inspirującą odpowiedź @ Jiří Zahálka, odpowiedziałem na moje rozwiązanie 4 lata temu tutaj , ale @Sayka zasugerował, żebym opublikował to jako odpowiedź, więc oto jest.

W moich projektach zwykle, jeśli używam Storyboard dla podklasy UIViewController, zawsze przygotowuję statyczną metodę wywoływaną instantiate()w tej podklasie, aby łatwo utworzyć wystąpienie z Storyboard. Aby rozwiązać pytanie OP, jeśli chcemy udostępnić ten sam Storyboard dla różnych podklas, możemy po prostu setClass()przejść do tego wystąpienia przed jego zwróceniem.

class func instantiate() -> SubClass {
    let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)!
    object_setClass(instance, SubClass.self)
    return (instance as? SubClass)!
}
CocoaBob
źródło
0

Komentarz Cocoabob od odpowiedzi Jiříego Zahálki pomógł mi w znalezieniu tego rozwiązania i działa dobrze.

func openChildA() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil);
    let parentController = storyboard
        .instantiateViewController(withIdentifier: "ParentStoryboardID") 
        as! ParentClass;
    object_setClass(parentController, ChildA.self)
    self.present(parentController, animated: true, completion: nil);
}
Sayka
źródło