Comment utiliser un seul storyboard uiviewcontroller pour plusieurs sous-classes


118

Disons que j'ai un storyboard qui contient UINavigationControllercomme contrôleur de vue initial. Son contrôleur de vue racine est une sous-classe de UITableViewController, qui est BasicViewController. Il a IBActionqui est connecté au bouton de navigation droit de la barre de navigation

À partir de là, je voudrais utiliser le storyboard comme modèle pour d'autres vues sans avoir à créer des storyboards supplémentaires. Disons que ces vues auront exactement la même interface mais avec le contrôleur de vue racine de la classe SpecificViewController1et SpecificViewController2qui sont des sous-classes de BasicViewController.
Ces 2 contrôleurs de vue auraient les mêmes fonctionnalités et interface à l'exception de la IBActionméthode.
Ce serait comme ceci:

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

Puis-je faire quelque chose comme ça?
Puis-je simplement instancier le storyboard de BasicViewControllermais avoir un contrôleur de vue racine dans la sous SpecificViewController1- classe et SpecificViewController2?

Merci.


3
Cela vaut peut-être la peine de souligner que vous pouvez le faire avec la plume. Mais si vous êtes comme moi qui voulez des fonctionnalités intéressantes que seul le storyboard a (cellule statique / prototype, par exemple), alors je suppose que nous n'avons pas de chance.
Joseph Lin

Réponses:


57

excellente question - mais malheureusement seulement une réponse boiteuse. Je ne pense pas qu'il soit actuellement possible de faire ce que vous proposez car il n'y a pas d'initialiseurs dans UIStoryboard qui permettent de remplacer le contrôleur de vue associé au storyboard tel que défini dans les détails de l'objet dans le storyboard lors de l'initialisation. C'est lors de l'initialisation que tous les éléments de l'interface utilisateur du stoaryboard sont liés à leurs propriétés dans le contrôleur de vue.

Il s'initialise par défaut avec le contrôleur de vue spécifié dans la définition du storyboard.

Si vous essayez de réutiliser des éléments d'interface utilisateur que vous avez créés dans le storyboard, ils doivent toujours être liés ou associés à des propriétés dans lesquelles le contrôleur de vue les utilise pour qu'ils puissent "informer" le contrôleur de vue des événements.

Ce n'est pas un gros problème de copier sur une mise en page de storyboard, surtout si vous n'avez besoin que d'un design similaire pour 3 vues, mais si vous le faites, vous devez vous assurer que toutes les associations précédentes sont effacées, sinon il se bloquera quand il essaiera pour communiquer avec le contrôleur de vue précédent. Vous pourrez les reconnaître comme des messages d'erreur KVO dans la sortie du journal.

Quelques approches que vous pourriez adopter:

  • stockez les éléments de l'interface utilisateur dans un UIView - dans un fichier xib et instanciez-le à partir de votre classe de base et ajoutez-le en tant que sous-vue dans la vue principale, généralement self.view. Ensuite, vous utiliseriez simplement la disposition du storyboard avec des contrôleurs de vue essentiellement vides tenant leur place dans le storyboard mais avec la sous-classe de contrôleur de vue correcte qui leur est attribuée. Puisqu'ils hériteraient de la base, ils auraient ce point de vue.

  • créez la mise en page dans le code et installez-la à partir de votre contrôleur de vue de base. De toute évidence, cette approche va à l'encontre de l'objectif de l'utilisation du storyboard, mais peut être la voie à suivre dans votre cas. Si vous avez d'autres parties de l'application qui bénéficieraient de l'approche storyboard, vous pouvez dévier ici et là si nécessaire. Dans ce cas, comme ci-dessus, vous utiliseriez simplement des contrôleurs de vue bancaire avec votre sous-classe attribuée et laissez le contrôleur de vue de base installer l'interface utilisateur.

Ce serait bien si Apple proposait un moyen de faire ce que vous proposez, mais le problème d'avoir les éléments graphiques pré-liés à la sous-classe du contrôleur serait toujours un problème.

Excellente nouvelle année!! être bien


C'était rapide. Comme je le pensais, ce ne serait pas possible. Actuellement, je propose une solution en ayant juste cette classe BasicViewController et j'ai une propriété supplémentaire pour indiquer quelle "classe" / "mode" elle agira. Merci quand même.
verdy

2
dommage: (Je suppose que je dois copier et coller le même contrôleur de vue et changer sa classe comme solution de contournement.
Hlung

1
Et c'est pourquoi je n'aime pas les storyboards ... d'une manière ou d'une autre, ils ne fonctionnent pas vraiment une fois que vous faites un peu plus que les vues standard ...
TheEye

Tellement triste en vous entendant dire ça. Je cherche une solution
Tony

2
Il existe une autre approche: spécifiez la logique personnalisée dans différents délégués et dans prepareForSegue, affectez le délégué approprié. De cette façon, vous créez 1 UIViewController + 1 UIViewController dans le Storyboard mais vous avez plusieurs versions d'implémentation.
plam4u

45

Le code de la ligne que nous recherchons est:

object_setClass(AnyObject!, AnyClass!)

Dans Storyboard -> ajoutez UIViewController, donnez-lui un nom de 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
Merci, ça marche, par exemple: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
Je ne suis pas sûr de comprendre comment cela est censé fonctionner. Le parent définit sa classe comme celle d'un enfant? Comment pouvez-vous alors avoir plusieurs enfants?!
user1366265

2
vous monsieur, avez fait ma journée
jere

2
OK les gars, alors laissez-moi vous expliquer un peu plus en détail: que voulons-nous accomplir? Nous voulons sous-classer notre ParentViewController afin que nous puissions utiliser son Storyboard pour plus de classes. Ainsi, la ligne magique qui fait tout est mise en évidence dans ma solution et doit être utilisée dans awakeFromNib dans ParentVC. Ce qui se passe alors, c'est qu'il utilise toutes les méthodes de ChildVC1 nouvellement défini qui devient une sous-classe. Si vous souhaitez l'utiliser pour plus de ChildVC? Faites simplement votre logique dans awakeFromNib .. if (type = a) {object_setClass (self, ChildVC1.self)} else {object_setClass (self.ChildVC2.self)} Bonne chance.
Jiří Zahálka

10
Soyez très prudent lorsque vous l'utilisez! Normalement, cela ne devrait pas être utilisé du tout ... Cela change simplement le pointeur isa du pointeur donné et ne réalloue pas la mémoire pour tenir compte, par exemple, de différentes propriétés. Un indicateur pour cela est que le pointeur vers selfne change pas. Ainsi, l'inspection de l'objet (par exemple la lecture des valeurs de _ivar / propriété) après object_setClasspeut provoquer des plantages.
Patrik

15

Comme l'indique la réponse acceptée, il ne semble pas possible de faire avec des storyboards.

Ma solution est d'utiliser Nib's - tout comme les développeurs les utilisaient avant les storyboards. Si vous voulez avoir un contrôleur de vue réutilisable et sous-classable (ou même une vue), ma recommandation est d'utiliser Nibs.

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

Lorsque vous connectez tous vos points de vente au "File Owner" dans le, MyViewController.xibvous ne spécifiez PAS la classe sous laquelle le Nib doit être chargé, vous spécifiez simplement des paires clé-valeur: " cette vue doit être connectée à ce nom de variable d'instance ." Lors de l'appel, [SubclassMyViewController alloc] initWithNibName:le processus d'initialisation spécifie quel contrôleur de vue sera utilisé pour " contrôler " la vue que vous avez créée dans la pointe.


Étonnamment, cela semble possible avec les storyboards, grâce à la bibliothèque d'exécution ObjC. Vérifiez ma réponse ici: stackoverflow.com/a/57622836/7183675
Adam Tucholski

9

Il est possible qu'un storyboard instancie différentes sous-classes d'un contrôleur de vue personnalisé, bien que cela implique une technique légèrement peu orthodoxe: remplacer la allocméthode pour le contrôleur de vue. Lorsque le contrôleur de vue personnalisé est créé, la méthode d'allocation remplacée renvoie en fait le résultat de l'exécution allocsur la sous-classe.

Je devrais commencer la réponse à condition que, bien que je l'ai testé dans divers scénarios et que je n'ai reçu aucune erreur, je ne peux pas garantir qu'il fera face à des configurations plus complexes (mais je ne vois aucune raison pour laquelle cela ne devrait pas fonctionner) . De plus, je n'ai soumis aucune application à l'aide de cette méthode, il y a donc une chance extérieure qu'elle soit rejetée par le processus de révision d'Apple (bien qu'une fois de plus je ne vois aucune raison pour laquelle cela devrait).

À des fins de démonstration, j'ai une sous-classe de UIViewControllercalled TestViewController, qui a un IBOutlet UILabel et un IBAction. Dans mon storyboard, j'ai ajouté un contrôleur de vue et modifié sa classe en TestViewController, et connecté l'IBOutlet à un UILabel et l'IBAction à un UIButton. Je présente le TestViewController au moyen d'un segment modal déclenché par un UIButton sur le viewController précédent.

Image du storyboard

Pour contrôler quelle classe est instanciée, j'ai ajouté une variable statique et des méthodes de classe associées afin d'obtenir / définir la sous-classe à utiliser (je suppose que l'on pourrait adopter d'autres façons de déterminer quelle sous-classe doit être instanciée):

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

Pour mon test, j'ai deux sous-classes de TestViewController: RedTestViewControlleret GreenTestViewController. Les sous-classes ont chacune des propriétés supplémentaires et chaque remplacement viewDidLoadpour changer la couleur d'arrière-plan de la vue et mettre à jour le texte de l'IBOutlet UILabel:

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

À certaines occasions, je pourrais vouloir s'instancier TestViewController, à d'autres occasions RedTestViewControllerou GreenTestViewController. Dans le contrôleur de vue précédent, je le fais au hasard comme suit:

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

Notez que la setClassForStoryBoardméthode vérifie que le nom de classe demandé est bien une sous-classe de TestViewController, pour éviter toute confusion. La référence ci-dessus BlueTestViewControllerest là pour tester cette fonctionnalité.


Nous avons fait quelque chose de similaire dans le projet, mais en surchargeant la méthode alloc de UIViewController pour obtenir une sous-classe d'une classe externe rassemblant les informations complètes sur toutes les substitutions. Fonctionne parfaitement.
Tim

À propos, cette méthode peut cesser de fonctionner aussi vite qu'Apple arrête d'appeler alloc sur les contrôleurs de vue. Pour l'exemple, la classe NSManagedObject ne reçoit jamais la méthode alloc. Je pense qu'Apple pourrait copier le code dans une autre méthode: peut-être + allocManagedObject
Tim

7

essayez ceci, après instantiateViewControllerWithIdentifier.

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

comme :

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

Veuillez ajouter des explications utiles sur ce que fait votre code.
codeforester

7
Que se passe-t-il avec cela, si vous utilisez des variables d'instance de la sous-classe? J'imagine un crash, car il n'y a pas assez de mémoire allouée pour cela. Dans mes tests, j'ai euEXC_BAD_ACCESS , donc ne le recommande pas.
Legoless

1
Cela ne fonctionnera pas si vous ajoutez de nouvelles variables dans la classe enfant. Et l'enfant initne sera pas appelé non plus. De telles contraintes rendent toute approche inutilisable.
Al Zonke

6

En me basant en particulier sur les réponses de nickgzzjr et Jiří Zahálka, ainsi que sur le commentaire sous le second de CocoaBob, j'ai préparé une courte méthode générique faisant exactement ce dont OP a besoin. Il vous suffit de vérifier le nom du storyboard et l'ID du storyboard de View Controller

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
    }

Des options sont ajoutées pour éviter de forcer le dépliage (avertissements swiftlint), mais la méthode renvoie les objets corrects.


5

Bien que ce ne soit pas strictement une sous-classe, vous pouvez:

  1. option-drag le contrôleur de vue de classe de base dans la structure du document pour faire une copie
  2. Déplacez la nouvelle copie du contrôleur de vue vers un emplacement distinct sur le storyboard
  3. Remplacez la classe par le contrôleur de vue de sous-classe dans l'inspecteur d'identité

Voici un exemple d'un tutoriel Bloc que j'ai écrit, en sous-classant ViewControlleravec WhiskeyViewController:

animation des trois étapes ci-dessus

Cela vous permet de créer des sous-classes de sous-classes de contrôleur de vue dans le storyboard. Vous pouvez ensuite utiliser instantiateViewControllerWithIdentifier:pour créer des sous-classes spécifiques.

Cette approche est un peu inflexible: les modifications ultérieures dans le storyboard vers le contrôleur de classe de base ne se propagent pas à la sous-classe. Si vous avez beaucoup de sous-classes, vous serez peut-être mieux avec l'une des autres solutions, mais cela suffira à la rigueur.


11
Ce n'est pas un compagnon de sous-classe, il s'agit simplement de dupliquer un ViewController.
Ace Green

1
Ce n'est pas juste. Il devient une sous-classe lorsque vous remplacez la classe par la sous-classe (étape 3). Ensuite, vous pouvez apporter les modifications souhaitées et vous connecter aux prises / actions de votre sous-classe.
Aaron Brager

6
Je ne pense pas que vous compreniez le concept de sous-classification.
Ace Green

5
Si "les modifications ultérieures dans le storyboard au contrôleur de classe de base ne se propagent pas à la sous-classe", cela ne s'appelle pas "sous-classement". C'est du copier-coller.
superarts.org

La classe sous-jacente, sélectionnée dans l'inspecteur d'identité, est toujours une sous-classe. L'objet en cours d'initialisation et contrôlant la logique métier est toujours une sous-classe. Seules les données de vue codées, stockées au format XML dans le fichier de storyboard et initialisées via initWithCoder:, n'ont pas de relation héritée. Ce type de relation n'est pas pris en charge par les fichiers de storyboard.
Aaron Brager

4

La méthode Objc_setclass ne crée pas d'instance de childvc. Mais tout en sortant de childvc, la deinit de childvc est appelée. Puisqu'il n'y a pas de mémoire allouée séparément pour childvc, l'application se bloque. Basecontroller a une instance, alors que l'enfant vc n'en a pas.


2

Si vous n'êtes pas trop dépendant des storyboards, vous pouvez créer un fichier .xib distinct pour le contrôleur.

Définissez le propriétaire et les prises du fichier appropriés MainViewControllersur etinit(nibName:bundle:) dans le VC principal afin que ses enfants puissent accéder au même Nib et à ses prises.

Votre code doit ressembler à ceci:

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

Et votre VC enfant pourra réutiliser la pointe de son parent:

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

2

En prenant des réponses d'ici et là, j'ai trouvé cette solution intéressante.

Créez un contrôleur de vue parent avec cette fonction.

class ParentViewController: UIViewController {


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

        object_setClass(self, T.self)

    }

}

Cela permet au compilateur de s'assurer que le contrôleur de vue enfant hérite du contrôleur de vue parent.

Ensuite, chaque fois que vous souhaitez passer à ce contrôleur en utilisant une sous-classe, vous pouvez faire:

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 partie intéressante est que vous pouvez ajouter une référence de storyboard à lui-même, puis continuer à appeler le contrôleur de vue enfant "suivant".


1

Le moyen le plus flexible est probablement d'utiliser des vues réutilisables.

(Créez une vue dans un fichier XIB séparé ou Container viewajoutez-la à chaque scène de contrôleur de vue de sous-classe dans le storyboard)


1
Veuillez commenter lorsque vous votez contre. Je sais que je ne réponds pas directement à cette question, mais je propose une solution au problème racine.
DanSkeel

1

Il existe une solution simple, évidente et quotidienne.

Mettez simplement le storyboard / contrôleur existant dans le nouveau storyobard / contrôleur. IE en tant que vue de conteneur.

C'est le concept exactement analogue au "sous-classement", pour les contrôleurs de vue.

Tout fonctionne exactement comme dans une sous-classe.

Tout comme vous mettez généralement une sous-vue de vue dans une autre vue , vous placez naturellement généralement un contrôleur de vue dans un autre contrôleur de vue .

Comment pouvez-vous le faire autrement?

C'est une partie de base d'iOS, aussi simple que le concept de «sous-vue».

C'est aussi simple que ça ...

/*

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

Tu dois évidemment faire listce que tu veux avec

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

etc.

Les vues de conteneur sont "juste comme" les sous-classes de la même manière que les "sous-vues" sont "juste comme" les sous-classes.

Bien sûr, vous ne pouvez pas "sous-classer une mise en page" - qu'est-ce que cela signifierait?

("Sous-classement" concerne le logiciel OO et n'a aucun lien avec les "mises en page".)

Évidemment, lorsque vous souhaitez réutiliser une vue, vous la sous-visualisez simplement dans une autre vue.

Lorsque vous souhaitez réutiliser une disposition de contrôleur, il vous suffit de l'afficher dans un autre contrôleur.

C'est comme le mécanisme le plus basique d'iOS !!


Remarque - depuis des années, il est devenu trivial de charger dynamiquement un autre contrôleur de vue en tant que vue de conteneur. Expliqué dans la dernière section: https://stackoverflow.com/a/23403979/294884

Remarque - "_sb" est juste une macro évidente que nous utilisons pour enregistrer la saisie,

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

Merci pour la réponse inspirante de @ Jiří Zahálka, j'ai répondu à ma solution il y a 4 ans ici , mais @Sayka m'a suggéré de l'afficher comme réponse, alors la voici.

Dans mes projets, normalement, si j'utilise Storyboard pour une sous-classe UIViewController, je prépare toujours une méthode statique appelée instantiate()dans cette sous-classe, pour créer facilement une instance à partir de Storyboard. Donc, pour résoudre la question de OP, si nous voulons partager le même Storyboard pour différentes sous-classes, nous pouvons simplement accéder setClass()à cette instance avant de la renvoyer.

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

Le commentaire de Cocoabob de la réponse de Jiří Zahálka m'a aidé à obtenir cette solution et cela a bien fonctionné.

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);
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.