Come utilizzare uiviewcontroller a storyboard singolo per più sottoclassi


118

Diciamo che ho uno storyboard che contiene UINavigationControllercome controller di visualizzazione iniziale. Il suo controller di visualizzazione principale è una sottoclasse di UITableViewController, ovvero BasicViewController. Ha il IBActionquale è collegato al pulsante di navigazione destro della barra di navigazione

Da lì vorrei utilizzare lo storyboard come modello per altre visualizzazioni senza dover creare storyboard aggiuntivi. Diciamo che queste viste avranno esattamente la stessa interfaccia ma con il controller della vista principale della classe SpecificViewController1e SpecificViewController2che sono sottoclassi di BasicViewController.
Quei 2 controller di visualizzazione avrebbero la stessa funzionalità e interfaccia ad eccezione del IBActionmetodo.
Sarebbe come il seguente:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Posso fare qualcosa del genere?
Posso solo BasicViewControllercreare un'istanza dello storyboard di ma avere il controller della vista principale in una sottoclasse SpecificViewController1e SpecificViewController2?

Grazie.


3
Potrebbe valere la pena sottolineare che puoi farlo con il pennino. Ma se sei come me che vuoi alcune caratteristiche carine che solo lo storyboard ha (cella statica / prototipo, per esempio), allora credo che siamo sfortunati.
Joseph Lin

Risposte:


57

grande domanda - ma sfortunatamente solo una risposta scadente. Non credo che sia attualmente possibile fare ciò che proponi perché non ci sono inizializzatori in UIStoryboard che consentono di sovrascrivere il controller di visualizzazione associato allo storyboard come definito nei dettagli dell'oggetto nello storyboard durante l'inizializzazione. È al momento dell'inizializzazione che tutti gli elementi dell'interfaccia utente nello stoaryboard sono collegati alle loro proprietà nel controller della vista.

Per impostazione predefinita, verrà inizializzato con il controller di visualizzazione specificato nella definizione dello storyboard.

Se stai cercando di riutilizzare gli elementi dell'interfaccia utente che hai creato nello storyboard, devono comunque essere collegati o associati a proprietà in cui il controller di visualizzazione li utilizza per poter "dire" al controller di visualizzazione gli eventi.

Non è un grosso problema copiare su un layout di storyboard, specialmente se hai solo bisogno di un design simile per 3 viste, tuttavia se lo fai, devi assicurarti che tutte le associazioni precedenti siano cancellate, altrimenti si verificheranno arresti anomali quando proverà per comunicare con il controller di visualizzazione precedente. Sarai in grado di riconoscerli come messaggi di errore KVO nell'output del registro.

Un paio di approcci che potresti adottare:

  • memorizzare gli elementi dell'interfaccia utente in un UIView - in un file xib e istanziarlo dalla classe base e aggiungerlo come vista secondaria nella vista principale, tipicamente self.view. Quindi useresti semplicemente il layout dello storyboard con controller di visualizzazione sostanzialmente vuoti che mantengono il loro posto nello storyboard ma con la sottoclasse del controller di visualizzazione corretta assegnata a loro. Dal momento che erediteranno dalla base, otterrebbero quella vista.

  • creare il layout nel codice e installarlo dal controller della visualizzazione di base. Ovviamente questo approccio sconfigge lo scopo dell'utilizzo dello storyboard, ma potrebbe essere la strada da percorrere nel tuo caso. Se hai altre parti dell'app che trarrebbero vantaggio dall'approccio dello storyboard, puoi deviare qua e là se appropriato. In questo caso, come sopra, useresti semplicemente i controller di visualizzazione banca con la tua sottoclasse assegnata e lascerai che il controller di visualizzazione di base installi l'interfaccia utente.

Sarebbe bello se Apple trovasse un modo per fare ciò che proponi, ma il problema di avere gli elementi grafici pre-collegati con la sottoclasse del controller sarebbe ancora un problema.

buon anno nuovo !! Stammi bene


È stato veloce. Come pensavo, non sarebbe stato possibile. Attualmente trovo una soluzione avendo solo quella classe BasicViewController e ho proprietà aggiuntive per indicare quale "classe" / "modalità" agirà come. Grazie comunque.
verdy

2
peccato :( Immagino di dover copiare e incollare lo stesso controller di visualizzazione e cambiare la sua classe come soluzione alternativa.
Hlung

1
Ed è per questo che non mi piacciono gli storyboard ... in qualche modo non funzionano davvero una volta che fai un po 'di più delle visualizzazioni standard ...
TheEye

Così triste quando hai sentito che hai detto questo. Sto cercando una soluzione
Tony

2
C'è un altro approccio: specificare la logica personalizzata in delegati diversi e in prepareForSegue, assegnare il delegato corretto. In questo modo, crei 1 UIViewController + 1 UIViewController nello Storyboard ma hai più versioni di implementazione.
plam4u

45

Il codice di linea che stiamo cercando è:

object_setClass(AnyObject!, AnyClass!)

In Storyboard -> aggiungi UIViewController dagli un nome di classe 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
    }
}

5
Grazie, funziona, ad esempio: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
Non sono sicuro di capire come dovrebbe funzionare. Il genitore sta impostando la sua classe come quella di un bambino? Come puoi avere più figli allora ?!
user1366265

2
voi signore, avete reso la mia giornata
jere

2
Ok ragazzi, quindi lasciate che lo spieghi un po 'più in dettaglio: cosa vogliamo ottenere? Vogliamo creare una sottoclasse del nostro ParentViewController in modo da poter utilizzare il suo Storyboard per più classi. Quindi la linea magica che fa tutto è evidenziata nella mia soluzione e deve essere utilizzata in awakeFromNib in ParentVC. Quello che succede allora è che utilizza tutti i metodi da ChildVC1 appena impostato che diventa una sottoclasse. Se vuoi usarlo per più ChildVC? Esegui semplicemente la tua logica in awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Buona fortuna.
Jiří Zahálka

10
Stai molto attento quando lo usi! Normalmente questo non dovrebbe essere usato affatto ... Questo cambia semplicemente il puntatore isa del puntatore dato e non rialloca la memoria per adattarsi, ad esempio, a proprietà diverse. Un indicatore di ciò è che il puntatore a selfnon cambia. Quindi l'ispezione dell'oggetto (ad esempio la lettura di _ivar / valori di proprietà) dopo object_setClasspuò causare arresti anomali.
Patrik

15

Come afferma la risposta accettata, non sembra possibile fare con gli storyboard.

La mia soluzione è usare Nib's, proprio come gli sviluppatori li usavano prima degli storyboard. Se si desidera avere un controller di visualizzazione riutilizzabile e sottoclassabile (o anche una visualizzazione), il mio consiglio è di utilizzare i pennini.

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

Quando colleghi tutti i tuoi punti vendita al "File Owner" in MyViewController.xibNON stai specificando la classe in cui deve essere caricato il pennino, stai solo specificando le coppie chiave-valore: " questa vista dovrebbe essere connessa a questo nome di variabile di istanza ". Quando si chiama [SubclassMyViewController alloc] initWithNibName:il processo di inizializzazione, si specifica quale controller di visualizzazione verrà utilizzato per " controllare " la visualizzazione creata nel pennino.


Sorprendentemente sembra che questo sia possibile con gli storyboard, grazie alla libreria di runtime ObjC. Controlla la mia risposta qui: stackoverflow.com/a/57622836/7183675
Adam Tucholski

9

È possibile fare in modo che uno storyboard crei un'istanza di sottoclassi diverse di un controller di visualizzazione personalizzato, sebbene implichi una tecnica leggermente non ortodossa: sovrascrivere il allocmetodo per il controller di visualizzazione. Quando viene creato il controller di visualizzazione personalizzato, il metodo di allocazione sovrascritto restituisce infatti il ​​risultato dell'esecuzione allocnella sottoclasse.

Dovrei anteporre la risposta con la condizione che, anche se l'ho testato in vari scenari e non ho ricevuto errori, non posso garantire che possa far fronte a configurazioni più complesse (ma non vedo motivo per cui non dovrebbe funzionare) . Inoltre, non ho inviato alcuna app utilizzando questo metodo, quindi c'è la possibilità che possa essere rifiutata dal processo di revisione di Apple (anche se ancora una volta non vedo motivo per cui dovrebbe).

A scopo dimostrativo, ho una sottoclasse di UIViewControllerchiamata TestViewController, che ha un UILabel IBOutlet, ed un IBAction. Nel mio storyboard, ho aggiunto un controller di visualizzazione e modificato la sua classe in TestViewControllere ho collegato l'IBOutlet a una UILabel e l'IBAction a un UIButton. Presento il TestViewController tramite un segue modale attivato da un UIButton sul viewController precedente.

Immagine dello storyboard

Per controllare quale classe viene istanziata, ho aggiunto una variabile statica e metodi di classe associati in modo da ottenere / impostare la sottoclasse da utilizzare (immagino che si potrebbero adottare altri modi per determinare quale sottoclasse deve essere istanziata):

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

Per il mio test ho due sottoclassi di TestViewController: RedTestViewControllere GreenTestViewController. Le sottoclassi hanno ciascuna proprietà aggiuntive e ciascuna sostituisce viewDidLoadper modificare il colore di sfondo della vista e aggiornare il testo di 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";
}

In alcune occasioni potrei voler creare un'istanza TestViewController, in altre occasioni RedTestViewControllero GreenTestViewController. Nel controller di visualizzazione precedente, lo faccio a caso come segue:

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"];
}

Si noti che il setClassForStoryBoardmetodo verifica che il nome della classe richiesta sia effettivamente una sottoclasse di TestViewController, per evitare confusioni. Il riferimento sopra a BlueTestViewControllerè lì per testare questa funzionalità.


Abbiamo fatto qualcosa di simile nel progetto, ma sovrascrivendo il metodo alloc di UIViewController per ottenere una sottoclasse da una classe esterna che raccoglie le informazioni complete su tutte le sostituzioni. Funziona perfettamente.
Tim

A proposito, questo metodo potrebbe smettere di funzionare più velocemente che Apple smette di chiamare alloc sui controller di visualizzazione. Per l'esempio la classe NSManagedObject non riceve mai il metodo di allocazione. Penso che Apple potrebbe copiare il codice in un altro metodo: forse + allocManagedObject
Tim

7

prova questo, dopo instantiateViewControllerWithIdentifier.

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

piace :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];

Aggiungi qualche spiegazione utile su ciò che fa il tuo codice.
codeforester

7
Cosa succede con questo, se usi variabili di istanza dalla sottoclasse? Immagino che si blocchi, perché non c'è abbastanza memoria allocata per adattarlo. Nei miei test, ho ricevuto EXC_BAD_ACCESS, quindi non lo consiglio.
Legoless

1
Questo non funzionerà se aggiungerai nuove variabili nella classe figlia. E initanche il bambino non sarà chiamato. Tali vincoli rendono inutilizzabili tutti gli approcci.
Al Zonke

6

Basandomi in particolare sulle risposte di nickgzzjr e Jiří Zahálka più un commento sotto il secondo di CocoaBob, ho preparato un breve metodo generico che fa esattamente ciò di cui OP ha bisogno. È sufficiente controllare il nome dello storyboard e l'ID dello storyboard dei controller di visualizzazione

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
    }

Gli optional vengono aggiunti per evitare di forzare lo scartamento (avvisi di swiftlint), ma il metodo restituisce gli oggetti corretti.


5

Sebbene non sia strettamente una sottoclasse, puoi:

  1. optiontrascina il controller della vista della classe base nella struttura del documento per crearne una copia
  2. Spostare la nuova copia del controller di visualizzazione in una posizione separata nello storyboard
  3. Modificare la classe nel controller di visualizzazione della sottoclasse nell'Identity Inspector

Ecco un esempio da un tutorial di Bloc che ho scritto, sottoclasse ViewControllercon WhiskeyViewController:

animazione dei tre passaggi precedenti

Ciò consente di creare sottoclassi di sottoclassi del controller di visualizzazione nello storyboard. È quindi possibile utilizzare instantiateViewControllerWithIdentifier:per creare sottoclassi specifiche.

Questo approccio è un po 'inflessibile: le modifiche successive all'interno dello storyboard al controller della classe base non si propagano alla sottoclasse. Se hai molte sottoclassi potresti stare meglio con una delle altre soluzioni, ma questo andrà bene in un pizzico.


11
Questo non è un accoppiamento di sottoclasse, si tratta solo di duplicare un ViewController.
Ace Green

1
Non è giusto. Diventa una sottoclasse quando si cambia Classe nella sottoclasse (passaggio 3). Quindi puoi apportare tutte le modifiche che desideri e collegarti agli sbocchi / azioni nella tua sottoclasse.
Aaron Brager

6
Non credo che tu abbia il concetto di sottoclasse.
Ace Green

5
Se "le modifiche successive all'interno dello storyboard al controller della classe base non si propagano alla sottoclasse", non si chiama "sottoclasse". È copia e incolla.
superarts.org

La classe sottostante, selezionata in Identity Inspector, è ancora una sottoclasse. L'oggetto che viene inizializzato e che controlla la logica aziendale è ancora una sottoclasse. Solo i dati della vista codificati, archiviati come XML nel file storyboard e inizializzati tramite initWithCoder:, non hanno una relazione ereditata. Questo tipo di relazione non è supportato dai file dello storyboard.
Aaron Brager

4

Il metodo Objc_setclass non crea un'istanza di childvc. Ma mentre salta fuori da childvc, viene chiamato deinit di childvc. Poiché non c'è memoria allocata separatamente per childvc, l'app si arresta in modo anomalo. Basecontroller ha un'istanza, mentre child vc non ha.


2

Se non ti affidi troppo agli storyboard, puoi creare un file .xib separato per il controller.

Impostare il proprietario e le prese del file appropriato su MainViewControllere sovrascrivere init(nibName:bundle:)nel VC principale in modo che i suoi figli possano accedere allo stesso pennino e ai suoi punti vendita.

Il tuo codice dovrebbe assomigliare a questo:

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

E il tuo bambino VC potrà riutilizzare il pennino del genitore:

class ChildViewController: MainViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        button.tintColor = .blue
    }
}

2

Prendendo risposte di qua e di là, ho trovato questa bella soluzione.

Crea un controller di visualizzazione genitore con questa funzione.

class ParentViewController: UIViewController {


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

        object_setClass(self, T.self)

    }

}

Ciò consente al compilatore di garantire che il controller di visualizzazione figlio erediti dal controller di visualizzazione padre.

Quindi ogni volta che vuoi passare a questo controller usando una sottoclasse puoi fare:

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

}

La parte interessante è che puoi aggiungere un riferimento allo storyboard a se stesso e quindi continuare a chiamare il controller di visualizzazione figlio "successivo".


1

Probabilmente il modo più flessibile è usare le visualizzazioni riutilizzabili.

(Creare una vista in un file XIB separato o Container viewaggiungerla a ciascuna scena del controller di visualizzazione della sottoclasse nello storyboard)


1
Si prega di commentare quando si downvote. So di non rispondere direttamente a questa domanda, ma propongo una soluzione per il problema alla radice.
DanSkeel

1

C'è una soluzione semplice, ovvia, quotidiana.

Metti semplicemente lo storyboard / controller esistente all'interno del nuovo storyobard / controller. IE come visualizzazione contenitore.

Questo è il concetto esattamente analogo alla "sottoclasse", per i controller di visualizzazione.

Tutto funziona esattamente come in una sottoclasse.

Proprio come normalmente metti una vista secondaria in un'altra vista , naturalmente metti comunemente un controller della vista all'interno di un altro controller della vista .

In quale altro modo potresti farlo?

È una parte fondamentale di iOS, semplice come il concetto di "sottoview".

È così facile ...

/*

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

Ora ovviamente listdevi fare quello che vuoi

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

ecc ecc.

Le visualizzazioni contenitore sono "proprio come" le sottoclassi nello stesso modo in cui le "visualizzazioni secondarie" sono "proprio come" le sottoclassi.

Ovviamente non puoi "sublimare un layout", cosa significherebbe?

("Sottoclassi" si riferisce al software OO e non ha alcun collegamento con i "layout".)

Ovviamente quando vuoi riutilizzare una vista, la sottovisualizzi semplicemente all'interno di un'altra vista.

Quando si desidera riutilizzare un layout di controller, è sufficiente visualizzarlo in un contenitore all'interno di un altro controller.

Questo è come il meccanismo più basilare di iOS !!


Nota: da anni è banale caricare dinamicamente un altro controller di visualizzazione come visualizzazione contenitore. Spiegato nell'ultima sezione: https://stackoverflow.com/a/23403979/294884

Nota: "_sb" è solo una macro ovvia che utilizziamo per salvare la digitazione,

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

1

Grazie per la risposta ispiratrice di @ Jiří Zahálka, ho risposto alla mia soluzione 4 anni fa qui , ma @Sayka mi ha suggerito di pubblicarla come risposta, quindi eccola qui.

Nei miei progetti, normalmente, se utilizzo Storyboard per una sottoclasse UIViewController, preparo sempre un metodo statico chiamato instantiate()in quella sottoclasse, per creare facilmente un'istanza da Storyboard. Quindi, per risolvere la domanda di OP, se vogliamo condividere lo stesso Storyboard per diverse sottoclassi, possiamo semplicemente setClass()a quell'istanza prima di restituirlo.

class func instantiate() -> SubClass {
    let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)!
    object_setClass(instance, SubClass.self)
    return (instance as? SubClass)!
}

0

Il commento di Cocoabob dalla risposta di Jiří Zahálka mi ha aiutato a ottenere questa soluzione e ha funzionato bene.

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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.