128 votes

Comment utiliser un seul storyboard uiviewcontroller pour plusieurs sous-classes

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

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

@interface BasicViewController : UITableViewController

@interface SpecificViewController1 : BasicViewController

@interface SpecificViewController2 : BasicViewController

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

Merci.

3 votes

Il pourrait être utile de mentionner que vous pouvez le faire avec Nib. Mais si vous êtes comme moi et que vous voulez certaines fonctionnalités intéressantes que seul le storyboard a (cellule statique/prototype, par exemple), alors je suppose que nous n'avons pas de chance.

0 votes

0 votes

Est-ce que cela répond à votre question? Jeter à une sous-classe depuis Storyboard

43voto

Jiří Zahálka Points 32

Le code de la ligne que nous recherchons est :

object_setClass(AnyObject!, AnyClass!)

Dans Storyboard -> ajoutez UIViewController et 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)
        // La console affiche 0
    }
}

class ChildVC2: ParentVC {

    override func viewDidLoad() {
        super.viewDidLoad()

        println(type)
        // La console affiche 1
    }
}

6 votes

Merci, ça marche juste, 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)! }

3 votes

Je ne suis pas sûr de comprendre comment c'est censé fonctionner. Le parent définit sa classe comme celle d'un enfant ? Comment pouvez-vous avoir plusieurs enfants alors ?!

0 votes

Pouvez-vous développer sur la façon dont c'est une solution? Je suis d'accord avec @user136265 -- comment pouvez-vous avoir plusieurs ChildVCs?

16voto

anon Points 108

Comme le dit la réponse acceptée, il ne semble pas possible de le faire avec les storyboards.

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

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

Lorsque vous connectez toutes vos sorties à "File Owner" dans le MyViewController.xib vous NE spécifiez pas la classe que le Nib doit charger, vous spécifiez simplement des paires clé-valeur: "cette vue doit être connectée à ce nom de variable d'instance." Lors de l'appel de [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 le nib.

0 votes

Il semble étonnamment que cela soit possible avec les storyboards, grâce à la bibliothèque ObjC runtime. Consultez ma réponse ici : stackoverflow.com/a/57622836/7183675

9voto

pbasdf Points 16551

Il est possible d'avoir un storyboard instancier différentes sous-classes d'un contrôleur de vue personnalisé, bien que cela implique une technique légèrement non orthodoxe: en remplaçant la méthode alloc pour le contrôleur de vue. Lorsque le contrôleur de vue personnalisé est créé, la méthode alloc remplacée renvoie en fait le résultat de l'exécution de alloc sur la sous-classe.

Je devrais prévenir la réponse en spécifiant que, bien que je l'ai testée dans divers scénarios et n'ai reçu aucune erreur, je ne peux pas garantir qu'elle fonctionnera avec 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 utilisant cette méthode, donc il y a une chance qu'elle puisse être rejetée par le processus de révision d'Apple (encore une fois, je ne vois aucune raison pour laquelle cela devrait arriver).

À des fins de démonstration, j'ai une sous-classe de UIViewController appelée TestViewController, qui a un IBOutlet UILabel, et une IBAction. Dans mon storyboard, j'ai ajouté un contrôleur de vue et modifié sa classe en TestViewController, et j'ai connecté l'IBOutlet à un UILabel et l'IBAction à un UIButton. Je présente le TestViewController via un segue modal déclenché par un UIButton sur le contrôleur de vue 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 pour obtenir/définir la sous-classe à utiliser (je suppose qu'on pourrait adopter d'autres moyens 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(@"Attention : %@ n'est pas une sous-classe de %@, on revient à la classe de base", 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 : RedTestViewController et GreenTestViewController. Les sous-classes ont chacune des propriétés supplémentaires et chaque override viewDidLoad pour 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 = "Défini par RedTestVC";
}

GreenTestViewController.m:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor greenColor];
    self.testLabel.text = "Défini par GreenTestVC";
}

À certaines occasions, je pourrais vouloir instancier TestViewController lui-même, à d'autres occasions RedTestViewController ou GreenTestViewController. Dans le contrôleur de vue précédent, je le fais au hasard de la manière suivante :

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

Remarquez que la méthode setClassForStoryBoard vérifie pour s'assurer que le nom de classe demandé est en effet une sous-classe de TestViewController, pour éviter toute confusion. La référence ci-dessus à BlueTestViewController est là pour tester cette fonctionnalité.

0 votes

Nous avons fait quelque chose de similaire dans le projet, mais en remplaçant la méthode alloc de UIViewController pour obtenir une sous-classe à partir d'une classe externe rassemblant toutes les informations sur les remplacements. Fonctionne parfaitement.

0 votes

Au fait, cette méthode pourrait cesser de fonctionner dès qu'Apple arrêtera d'appeler alloc sur les contrôleurs de vue. Par 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

7voto

Adam Tucholski Points 123

En se basant particulièrement sur nickgzzjr et Jirí Zahálka réponses plus le commentaire sous la deuxième de CocoaBob, j'ai préparé une méthode générique courte faisant exactement ce dont l'OP a besoin. Vous devez seulement vérifier le nom du storyboard et l'ID du contrôleur de vue.

class func instantiate(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 optionnels sont ajoutés pour éviter le déballage forcé (avertissements swiftlint), mais la méthode renvoie les objets corrects.

Aussi : vous devez initialiser les propriétés existant seulement dans la sous-classe avant de les lire des objets convertis (si la sous-classe a ces propriétés et que BasicViewController ne les a pas). Ces propriétés ne seront pas initialisées automatiquement et toute tentative de les lire avant l'initialisation entraînera un crash. Parce qu'elles sont là en raison de la conversion, il est très probable que même les variables faibles ne seront pas définies comme nulles (elles contiendront des déchets).

6voto

莫振华 Points 77

Essayez ceci, après avoir instancié instantiateViewControllerWithIdentifier.

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

comme :

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

0 votes

Veuillez ajouter quelques explications utiles sur ce que votre code fait.

7 votes

Que se passe-t-il avec cela, si vous utilisez des variables d'instance de sous-classe? Je suppose que cela provoquera un crash, car il n'y a pas assez de mémoire allouée pour accueillir cela. Dans mes tests, j'ai reçu EXC_BAD_ACCESS, donc je ne recommande pas cela.

1 votes

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X