web-dev-qa-db-fra.com

Comment utiliser uiviewcontroller de storyboard unique pour plusieurs sous-classes

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

À partir de là, j'aimerais utiliser le storyboard comme modèle pour d'autres vues sans avoir à créer de storyboards supplémentaires. Supposons 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 la même interface à l'exception de 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 le storyboard de BasicViewController mais avoir le contrôleur de vue racine dans la sous-classe SpecificViewController1 et SpecificViewController2?

Merci.

108
verdy

grande 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 à l'initialisation que tous les éléments de l'interface utilisateur dans le 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 les é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 jamais le contrôleur de vue les utilise pour qu'ils puissent "informer" le contrôleur de vue des événements.

Ce n'est pas très important de copier 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 lorsqu'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:

  • stocker les éléments de l'interface utilisateur dans une vue UIV - dans un fichier xib et l'instancier à partir de votre classe de base et l'ajouter 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 fondamentalement vides tenant leur place dans le storyboard mais avec la sous-classe de contrôleur de vue appropriée qui leur est affectée. Puisqu'ils hériteraient de la base, ils obtiendraient cette vue.

  • créer la disposition dans le code et l'installer à partir de votre contrôleur de vue de base. De toute évidence, cette approche va à l'encontre de l'objectif d'utiliser le 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 du storyboard, il est possible de dévier ici et là si nécessaire. Dans ce cas, comme ci-dessus, vous devez simplement utiliser des contrôleurs de vue de banque avec votre sous-classe affectée et laisser le contrôleur de vue de base installer l'interface utilisateur.

Ce serait bien si Apple a trouvé 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

57
CocoaEv

Le code de 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
    }
}
44
Jiří Zahálka

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 "Propriétaire du fichier" dans le MyViewController.xib vous ne spécifiez PAS dans quelle classe 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 plume.

13
kgaidis

Il est possible qu'un storyboard instancie différentes sous-classes d'un contrôleur de vue personnalisé, bien qu'il implique une technique légèrement peu orthodoxe: remplacer 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éfacer la réponse à la condition que, même si je l'ai testée dans divers scénarios et que je n'ai reçu aucune erreur, je ne peux pas m'assurer qu'elle s'adaptera à des configurations plus complexes (mais je ne vois aucune raison pour que cela ne fonctionne pas) . De plus, je n'ai soumis aucune application en utilisant 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 que je ne vois encore aucune raison pour laquelle cela devrait).

À des fins de démonstration, j'ai une sous-classe de UIViewController appelée TestViewController, qui a un UILabel IBOutlet 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'une séquence modale déclenchée par un UIButton sur le viewController précédent.

Storyboard image

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: RedTestViewController et GreenTestViewController. Les sous-classes ont chacune des propriétés supplémentaires et chacune remplace viewDidLoad pour changer la couleur d'arrière-plan de la vue et mettre à jour le texte de l'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";
}

À 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 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 méthode setClassForStoryBoard 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 à BlueTestViewController est là pour tester cette fonctionnalité.

9
pbasdf

essayez ceci, après instantiateViewControllerWithIdentifier.

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

comme :

SubViewController *vc = [sb instantiateViewControllerWithIdentifier:@"MainViewController"];
[vc setClass:[SubViewController class]];
6
莫振华

Bien qu'il ne s'agisse pas strictement d'une sous-classe, vous pouvez:

  1. option-faites glisser le contrôleur de vue de la classe de base dans le plan du document pour en faire une copie
  2. Déplacer 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, sous-classant ViewController avec WhiskeyViewController:

animation of the above three steps

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 rigide: les modifications ultérieures dans le storyboard au contrôleur de classe de base ne se propagent pas à la sous-classe. Si vous avez un lot de sous-classes, vous pouvez être mieux avec l'une des autres solutions, mais cela fera l'affaire.

5
Aaron Brager

Si vous ne dépendez pas trop des storyboards, vous pouvez créer un fichier .xib distinct pour le contrôleur.

Définissez le propriétaire du fichier et les prises appropriés sur MainViewController et remplacez init(nibName:bundle:) dans Main VC afin que ses enfants puissent accéder à la même Nib et à ses prises.

Votre code devrait 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 enfant VC pourra réutiliser la plume de son parent:

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

La méthode objc_setclass ne crée pas d'instance de childvc. Mais en sortant de childvc, deinit de childvc est appelé. Comme 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 child vc n'en a pas.

2
user8044830

En me basant particulièrement sur nickgzzjr et Jiří Zahálka réponses plus commentaire sous le deuxième de CocoaBob, j'ai préparé une méthode générique courte faisant exactement ce dont OP a besoin. Il vous suffit de vérifier le nom du storyboard et l'ID du storyboard des contrôleurs de vue

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 le débouclage forcé, mais la méthode renvoie des objets corrects.

0
Adam Tucholski

Prenant des réponses d'ici et là, j'ai trouvé cette solution soignée.

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 vous connecter à ce contrôleur à l'aide d'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 d'appeler le contrôleur de vue enfant "suivant".

0
nickgzzjr

Le moyen probablement le plus flexible consiste à utiliser des vues réutilisables.

(Créez une vue dans un fichier XIB distinct ou Container view et l'ajouter à chaque scène de contrôleur de vue de sous-classe dans le storyboard)

0
DanSkeel