Init personnalisé pour UIViewController dans Swift avec configuration de l'interface dans le storyboard

89

J'ai un problème pour écrire un init personnalisé pour la sous-classe de UIViewController, fondamentalement, je veux passer la dépendance via la méthode init pour viewController plutôt que de définir la propriété directement comme viewControllerB.property = value

J'ai donc créé un init personnalisé pour mon viewController et j'ai appelé init super désigné

init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

L'interface du contrôleur de vue réside dans le storyboard, j'ai également fait de l'interface pour la classe personnalisée mon contrôleur de vue. Et Swift nécessite d'appeler cette méthode init même si vous ne faites rien dans cette méthode. Sinon, le compilateur se plaindra ...

required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

Le problème est que lorsque j'essaye d'appeler mon init personnalisé sans MyViewController(meme: meme)qu'il n'initialise du tout les propriétés dans mon viewController ...

J'essayais de déboguer, j'ai trouvé dans mon viewController, init(coder aDecoder: NSCoder)être appelé en premier, puis mon init personnalisé est appelé plus tard. Cependant, ces deux méthodes init renvoient des selfadresses mémoire différentes .

Je soupçonne que quelque chose ne va pas avec l'initialisation de mon viewController, et il reviendra toujours selfavec le init?(coder aDecoder: NSCoder), qui n'a pas d'implémentation.

Est-ce que quelqu'un sait comment créer correctement un init personnalisé pour votre viewController? Remarque: l'interface de mon viewController est configurée dans le storyboard

voici mon code viewController:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    @IBOutlet weak var editedImage: UIImageView!

    // TODO: incorrect init
    init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        /// setup nav title
        title = "Detail Meme"

        super.viewDidLoad()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        editedImage = UIImageView(image: meme.editedImage)
    }

}
Pigfly
la source
avez-vous une solution pour cela?
user481610

Réponses:

49

Comme cela a été spécifié dans l'une des réponses ci-dessus, vous ne pouvez pas utiliser à la fois la méthode d'initialisation et le storyboard personnalisés. Mais vous pouvez toujours utiliser une méthode statique pour instancier ViewController à partir d'un storyboard et y effectuer une configuration supplémentaire. Il ressemblera à ceci:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("IdentifierOfYouViewController") as! MemeDetailVC

        newViewController.meme = meme

        return newViewController
    }
}

N'oubliez pas de spécifier IdentifierOfYouViewController comme identificateur de contrôleur de vue dans votre storyboard. Vous devrez peut-être également changer le nom du storyboard dans le code ci-dessus.

Alexandre Grushevoy
la source
Une fois MemeDetailVC initialisé, la propriété "meme" existe. Ensuite, l'injection de dépendances entre pour attribuer la valeur à "meme". À ce moment, loadView () et viewDidLoad n'ont pas été appelés. Ces deux méthodes seront appelées après MemeDetailVC add / push pour afficher la hiérarchie.
Jim Yu
Meilleure réponse ici!
PascalS
La méthode d'initialisation est toujours requise et le compilateur dira que la propriété stockée du mème n'a pas été initialisée
Surendra Kumar
27

Vous ne pouvez pas utiliser un initialiseur personnalisé lorsque vous initialisez à partir d'un storyboard, en utilisant init?(coder aDecoder: NSCoder)est la façon dont Apple a conçu le storyboard pour initialiser un contrôleur. Cependant, il existe des moyens d'envoyer des données à un fichier UIViewController.

Le nom de votre contrôleur de vue y est detail, donc je suppose que vous y arrivez à partir d'un contrôleur différent. Dans ce cas, vous pouvez utiliser la prepareForSegueméthode pour envoyer des données au détail (c'est Swift 3):

override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "identifier" {
        if let controller = segue.destinationViewController as? MemeDetailVC {
            controller.meme = "Meme"
        }
    }
}

Je viens d'utiliser une propriété de type Stringau lieu de Memeà des fins de test. Assurez-vous également de transmettre l'identifiant de segment correct (il "identifier"s'agissait simplement d'un espace réservé).

Caleb Kleveter
la source
1
Salut, mais comment pouvons-nous nous débarrasser de la spécification comme option et nous ne voulons pas non plus nous tromper de ne pas l'attribuer. Nous voulons simplement que la dépendance stricte significative pour le contrôleur existe en premier lieu.
Amber K
1
@AmberK Vous voudrez peut-être vous pencher sur la programmation de votre interface au lieu d'utiliser Interface Builder.
Caleb Kleveter
D'accord, je suis également à la recherche du projet Kickstarter ios pour référence, pas encore capable de dire comment ils envoient leurs modèles de vue.
Amber K
23

Comme @Caleb Kleveter l'a souligné, nous ne pouvons pas utiliser un initialiseur personnalisé lors de l'initialisation à partir d'un Storyboard.

Mais, nous pouvons résoudre le problème en utilisant la méthode factory / class qui instancie l'objet contrôleur de vue à partir de Storyboard et retourne l'objet contrôleur de vue. Je pense que c'est une manière assez cool.

Remarque: ce n'est pas une réponse exacte à la question mais plutôt une solution de contournement pour résoudre le problème.

Créez une méthode de classe, dans la classe MemeDetailVC, comme suit:

// Considering your view controller resides in Main.storyboard and it's identifier is set to "MemeDetailVC"
class func `init`(meme: Meme) -> MemeDetailVC? {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MemeDetailVC") as? MemeDetailVC
    vc?.meme = meme
    return vc
}

Usage:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Nitesh Borad
la source
5
Mais toujours l'inconvénient est là, la propriété doit être facultative.
Amber K
lors de l'utilisation d' class func init (meme: Meme) -> MemeDetailVCXcode 10.2 est confus et donne l'erreurIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël
21

Une façon de procéder est d'utiliser un initialiseur pratique.

class MemeDetailVC : UIViewController {

    convenience init(meme: Meme) {
        self.init()
        self.meme = meme
    }
}

Ensuite, vous initialisez votre MemeDetailVC avec let memeDetailVC = MemeDetailVC(theMeme)

La documentation d'Apple sur les initialiseurs est plutôt bonne, mais mon préféré est la série de didacticiels Ray Wenderlich: Initialisation en profondeur qui devrait vous donner de nombreuses explications / exemples sur vos différentes options d'initialisation et la «bonne» façon de faire les choses.


EDIT : Bien que vous puissiez utiliser un initialiseur pratique sur les contrôleurs de vue personnalisés, tout le monde a raison de dire que vous ne pouvez pas utiliser d'initialiseurs personnalisés lors de l'initialisation à partir du storyboard ou via une séquence de storyboard.

Si votre interface est configurée dans le storyboard et que vous créez le contrôleur complètement par programme, un initialiseur pratique est probablement le moyen le plus simple de faire ce que vous essayez de faire puisque vous n'avez pas à gérer l'initialisation requise le NSCoder (que je ne comprends toujours pas vraiment).

Si vous obtenez votre contrôleur de vue via le storyboard, vous devrez suivre la réponse de @Caleb Kleveter et convertir le contrôleur de vue dans la sous-classe souhaitée, puis définir la propriété manuellement.

Ponyboy47
la source
1
Je ne comprends pas, si mon interface est configurée dans le storyboard et que je la crée par programme, alors je dois appeler la méthode d'instanciation en utilisant le storyboard pour charger l'interface, n'est-ce pas? Comment cela convenienceaidera-t-il ici.
Amber K
Les classes dans swift ne peuvent pas être initialisées en appelant une autre initfonction de la classe (les structs peuvent cependant). Donc, pour appeler self.init(), vous devez marquer le init(meme: Meme)comme convenienceinitialiseur. Sinon, vous devrez définir manuellement toutes les propriétés requises de a UIViewControllerdans l'initialiseur vous-même, et je ne suis pas sûr de toutes ces propriétés.
Ponyboy47
il ne s'agit donc pas de sous-classer UIViewController, car vous appelez self.init () et non super.init (). vous pouvez toujours initialiser le MemeDetailVC en utilisant l'initialisation par défaut comme MemeDetail()et dans ce cas le code plantera
Ali
Il est toujours considéré comme un sous-classement car self.init () devrait appeler super.init () dans son implémentation ou peut-être que self.init () est directement hérité du parent auquel cas ils sont fonctionnellement équivalents.
Ponyboy47
6

Il y avait à l'origine quelques réponses, qui ont été votées par la vache et supprimées même si elles étaient fondamentalement correctes. La réponse est que vous ne pouvez pas.

Lorsque vous travaillez à partir d'une définition de storyboard, vos instances de contrôleur de vue sont toutes archivées. Donc, pour les initier, il est nécessaire de init?(coder...les utiliser. C'est de coderlà que proviennent tous les paramètres / informations d'affichage.

Donc, dans ce cas, il n'est pas possible d'appeler également une autre fonction init avec un paramètre personnalisé. Il doit être défini comme une propriété lors de la préparation de la segue, ou vous pouvez abandonner les segues et charger les instances directement à partir du storyboard et les configurer (essentiellement un modèle d'usine utilisant un storyboard).

Dans tous les cas, vous utilisez la fonction init requise par le SDK et passez des paramètres supplémentaires par la suite.

Wain
la source
4

Swift 5

Vous pouvez écrire un initialiseur personnalisé comme celui-ci ->

class MyFooClass: UIViewController {

    var foo: Foo?

    init(with foo: Foo) {
        self.foo = foo
        super.init(nibName: nil, bundle: nil)
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.foo = nil
    }
}
emrcftci
la source
3

UIViewControllerclasse conforme au NSCodingprotocole défini comme:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

   public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}    

A donc UIViewControllerdeux initialiseurs désignés init?(coder aDecoder: NSCoder)et init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad appelle init?(coder aDecoder: NSCoder)directement init UIViewControlleret UIView, il n'y a pas de place pour que vous passiez des paramètres.

Une solution de contournement fastidieuse consiste à utiliser un cache temporaire:

class TempCache{
   static let sharedInstance = TempCache()

   var meme: Meme?
}

TempCache.sharedInstance.meme = meme // call this before init your ViewController    

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder);
    self.meme = TempCache.sharedInstance.meme
}
wj2061
la source
0

Avertissement: je ne préconise pas cela et je n'ai pas testé en profondeur sa résilience, mais c'est une solution potentielle que j'ai découverte en jouant.

Techniquement, une initialisation personnalisée peut être obtenue tout en préservant l'interface configurée pour le storyboard en initialisant le contrôleur de vue deux fois: la première fois via votre personnalisé init, et la deuxième fois à l'intérieur loadView()où vous prenez la vue du storyboard.

final class CustomViewController: UIViewController {
  @IBOutlet private weak var label: UILabel!
  @IBOutlet private weak var textField: UITextField!

  private let foo: Foo!

  init(someParameter: Foo) {
    self.foo = someParameter
    super.init(nibName: nil, bundle: nil)
  }

  override func loadView() {
    //Only proceed if we are not the storyboard instance
    guard self.nibName == nil else { return super.loadView() }

    //Initialize from storyboard
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let storyboardInstance = storyboard.instantiateViewController(withIdentifier: "CustomVC") as! CustomViewController

    //Remove view from storyboard instance before assigning to us
    let storyboardView = storyboardInstance.view
    storyboardInstance.view.removeFromSuperview()
    storyboardInstance.view = nil
    self.view = storyboardView

    //Receive outlet references from storyboard instance
    self.label = storyboardInstance.label
    self.textField = storyboardInstance.textField
  }

  required init?(coder: NSCoder) {
    //Must set all properties intended for custom init to nil here (or make them `var`s)
    self.foo = nil
    //Storyboard initialization requires the super implementation
    super.init(coder: coder)
  }
}

Maintenant, ailleurs dans votre application, vous pouvez appeler votre initialiseur personnalisé comme CustomViewController(someParameter: foo)et toujours recevoir la configuration de la vue du storyboard.

Je ne considère pas cela comme une excellente solution pour plusieurs raisons:

  • L'initialisation de l'objet est dupliquée, y compris toutes les propriétés de pré-init
  • Les paramètres passés à la personnalisation initdoivent être stockés en tant que propriétés facultatives
  • Ajoute un passe-partout qui doit être entretenu lorsque les prises / propriétés sont modifiées

Vous pouvez peut-être accepter ces compromis, mais utilisez-les à vos propres risques .

Nathan Hosselton
la source
0

Bien que nous puissions maintenant faire une init personnalisée pour les contrôleurs par défaut dans le storyboard en utilisant instantiateInitialViewController(creator:) et pour les segues, y compris la relation et le spectacle.

Cette fonctionnalité a été ajoutée dans Xcode 11 et ce qui suit est un extrait des notes de mise à jour de Xcode 11 :

Une méthode de contrôleur de vue annotée avec le nouvel @IBSegueActionattribut peut être utilisée pour créer le contrôleur de vue de destination d'un segment dans le code, à l'aide d'un initialiseur personnalisé avec toutes les valeurs requises. Cela permet d'utiliser des contrôleurs de vue avec des exigences d'initialisation non facultatives dans les storyboards. Créez une connexion depuis un segue vers une @IBSegueActionméthode sur son contrôleur de vue source. Sur les nouvelles versions de système d'exploitation qui prennent en charge les actions Segue, cette méthode sera appelée et la valeur qu'elle renvoie sera celle destinationViewControllerde l'objet segue passé à prepareForSegue:sender:. Plusieurs @IBSegueActionméthodes peuvent être définies sur un seul contrôleur de vue source, ce qui peut réduire la nécessité de vérifier les chaînes d'identificateur de segue dans prepareForSegue:sender:. (47091566)

Une IBSegueActionméthode prend jusqu'à trois paramètres: un codeur, l'expéditeur et l'identifiant de la séquence. Le premier paramètre est obligatoire et les autres paramètres peuvent être omis de la signature de votre méthode si vous le souhaitez. Le NSCoderdoit être transmis à l'initialiseur du contrôleur de vue de destination, pour s'assurer qu'il est personnalisé avec des valeurs configurées dans le storyboard. La méthode retourne un contrôleur de vue qui correspond au type de contrôleur de destination défini dans le storyboard, ou nilpour provoquer l'initialisation d'un contrôleur de destination avec la init(coder:)méthode standard . Si vous savez que vous n'avez pas besoin de retourner nil, le type de retour peut être non facultatif.

Dans Swift, ajoutez l' @IBSegueActionattribut:

@IBSegueAction
func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
    PetController(
        coder: coder,
        petName:  self.selectedPetName, type: .dog
    )
}

En Objective-C, ajoutez IBSegueActiondevant le type de retour:

- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
               sender:(id)sender
      segueIdentifier:(NSString *)segueIdentifier
{
   return [PetController initWithCoder:coder
                               petName:self.selectedPetName
                                  type:@"dog"];
}
malhal
la source
0

Le bon flux est, appelez l'initialiseur désigné qui dans ce cas est l'init avec nibName,

init(tap: UITapGestureRecognizer)
{
    // Initialise the variables here


    // Call the designated init of ViewController
    super.init(nibName: nil, bundle: nil)

    // Call your Viewcontroller custom methods here

}
Rahul Bansal
la source
-1

// Le contrôleur de vue est dans Main.storyboard et son identificateur est défini

Classe B

class func customInit(carType:String) -> BViewController 

{

let storyboard = UIStoryboard(name: "Main", bundle: nil)

let objClassB = storyboard.instantiateViewController(withIdentifier: "BViewController") as? BViewController

    print(carType)
    return objClassB!
}

Classe A

let objB = customInit(carType:"Any String")

 navigationController?.pushViewController(objB,animated: true)
kashish makkar
la source