Init personalizzato per UIViewController in Swift con configurazione dell'interfaccia nello storyboard


89

Ho problemi a scrivere un inizializzazione personalizzato per la sottoclasse di UIViewController, fondamentalmente voglio passare la dipendenza attraverso il metodo init per viewController piuttosto che impostare la proprietà direttamente come viewControllerB.property = value

Quindi ho creato un init personalizzato per il mio viewController e ho chiamato init super designato

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

L'interfaccia del controller di visualizzazione risiede nello storyboard, ho anche creato l'interfaccia per la classe personalizzata come controller di visualizzazione. E Swift richiede di chiamare questo metodo init anche se non stai facendo nulla all'interno di questo metodo. Altrimenti il ​​compilatore si lamenterà ...

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

Il problema è che quando provo a chiamare il mio init personalizzato con MyViewController(meme: meme)esso non le proprietà di inizializzazione nel mio viewController ...

Stavo cercando di eseguire il debug, ho trovato nel mio viewController, init(coder aDecoder: NSCoder)viene chiamato prima, quindi il mio init personalizzato viene chiamato in seguito. Tuttavia, questi due metodi di inizializzazione restituiscono selfindirizzi di memoria diversi.

Sospetto che ci sia qualcosa di sbagliato nell'init per il mio viewController, e tornerà sempre selfcon init?(coder aDecoder: NSCoder), che, non ha alcuna implementazione.

Qualcuno sa come creare correttamente un init personalizzato per il tuo viewController? Nota: l'interfaccia del mio viewController è impostata nello storyboard

ecco il mio codice 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)
    }

}

hai trovato una soluzione per questo?
user481610

Risposte:


50

Come è stato specificato in una delle risposte precedenti non è possibile utilizzare sia il metodo di inizializzazione personalizzato che lo storyboard.

È comunque possibile utilizzare un metodo statico per creare un'istanza ViewControllerda uno storyboard ed eseguire ulteriori impostazioni su di esso.

Sarà simile a questo:

class MemeDetailVC : UIViewController {
    
    var meme : Meme!
    
    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "IdentifierOfYouViewController") as! MemeDetailVC
        
        newViewController.meme = meme
        
        return newViewController
    }
}

Non dimenticare di specificare IdentifierOfYouViewController come identificatore del controller di visualizzazione nello storyboard. Potrebbe anche essere necessario modificare il nome dello storyboard nel codice sopra.


Dopo che MemeDetailVC è stato inizializzato, la proprietà "meme" esiste. Quindi arriva l'iniezione di dipendenza per assegnare il valore a "meme". In questo momento, loadView () e viewDidLoad non sono stati chiamati. Questi due metodi verranno chiamati dopo che MemeDetailVC aggiunge / invia per visualizzare la gerarchia.
Jim Yu

La migliore risposta qui!
PascalS

È ancora richiesto il metodo init e il compilatore dirà che la proprietà memorizzata del meme non è stata inizializzata
Surendra Kumar

27

Non puoi utilizzare un inizializzatore personalizzato quando esegui l'inizializzazione da uno storyboard, poiché init?(coder aDecoder: NSCoder)Apple ha progettato lo storyboard per inizializzare un controller. Tuttavia, ci sono modi per inviare dati a un file UIViewController.

Il nome del tuo controller di visualizzazione contiene detail, quindi suppongo che tu ci arrivi da un controller diverso. In questo caso puoi utilizzare il prepareForSeguemetodo per inviare i dati ai dettagli (questo è Swift 3):

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

Ho appena usato una proprietà di tipo Stringinvece che Memea scopo di test. Inoltre, assicurati di passare l'identificatore segue corretto ( "identifier"era solo un segnaposto).


1
Ciao, ma come possiamo sbarazzarci della specificazione come opzionale e non vogliamo anche sbagliare non assegnarla. Vogliamo solo che la stretta dipendenza significativa per il controller esista al primo posto.
Amber K

1
@AmberK Potresti voler programmare la tua interfaccia invece di usare Interface Builder.
Caleb Kleveter

Ok, sto anche cercando come riferimento il progetto ios di kickstarter, non sono ancora in grado di dire come inviano i loro modelli di visualizzazione.
Amber K

23

Come ha sottolineato @Caleb Kleveter, non possiamo usare un inizializzatore personalizzato durante l'inizializzazione da uno Storyboard.

Tuttavia, possiamo risolvere il problema utilizzando il metodo factory / class che istanzia l'oggetto controller di visualizzazione da Storyboard e restituisce l'oggetto controller di visualizzazione. Penso che questo sia un modo piuttosto interessante.

Nota: questa non è una risposta esatta alla domanda, ma piuttosto una soluzione alternativa per risolvere il problema.

Crea il metodo della classe, nella classe MemeDetailVC, come segue:

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

Utilizzo:

let memeDetailVC = MemeDetailVC.init(meme: Meme())

5
Ma lo svantaggio c'è ancora, la proprietà deve essere facoltativa.
Amber K

quando si usa class func init (meme: Meme) -> MemeDetailVCXcode 10.2 è confuso e dà l'erroreIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël

21

Un modo in cui l'ho fatto è con un inizializzatore di convenienza.

class MemeDetailVC : UIViewController {

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

Quindi inizializzi il tuo MemeDetailVC con let memeDetailVC = MemeDetailVC(theMeme)

La documentazione di Apple sugli inizializzatori è abbastanza buona, ma la mia preferita è la serie di tutorial Ray Wenderlich: Initialization in Depth che dovrebbe darti molte spiegazioni / esempi sulle tue varie opzioni di init e il modo "corretto" di fare le cose.


EDIT : sebbene sia possibile utilizzare un inizializzatore di convenienza sui controller di visualizzazione personalizzati, tutti hanno ragione nell'affermare che non è possibile utilizzare inizializzatori personalizzati durante l'inizializzazione dallo storyboard o tramite uno storyboard segue.

Se la tua interfaccia è impostata nello storyboard e stai creando il controller in modo completamente programmatico, allora un inizializzatore di convenienza è probabilmente il modo più semplice per fare ciò che stai cercando di fare poiché non devi gestire l'init richiesto il NSCoder (che ancora non capisco veramente).

Se stai ottenendo il controller di visualizzazione tramite lo storyboard, dovrai seguire la risposta di @Caleb Kleveter e lanciare il controller di visualizzazione nella sottoclasse desiderata, quindi impostare la proprietà manualmente.


1
Non capisco, se la mia interfaccia è impostata nello storyboard e sì, la sto creando in modo programmatico, allora devo chiamare il metodo di istanziazione usando lo storyboard per caricare l'interfaccia, giusto? Come sarà di convenienceaiuto qui.
Amber K

Le classi in swift non possono essere inizializzate chiamando un'altra initfunzione della classe (le strutture possono però). Quindi, per chiamare self.init(), è necessario contrassegnare init(meme: Meme)come convenienceinizializzatore. Altrimenti, dovresti impostare manualmente tutte le proprietà richieste di UIViewControllera nell'inizializzatore e non sono sicuro di quali siano tutte quelle proprietà.
Ponyboy47

questo non è quindi una sottoclasse UIViewController, perché stai chiamando self.init () e non super.init (). puoi ancora inizializzare MemeDetailVC usando init predefinito come MemeDetail()e in tal caso il codice andrà in crash
Ali

È ancora considerato una sottoclasse perché self.init () dovrebbe chiamare super.init () nella sua implementazione o forse self.init () viene ereditato direttamente dal genitore, nel qual caso sono funzionalmente equivalenti.
Ponyboy47

6

In origine c'erano un paio di risposte, che sono state votate dalle vacche e cancellate anche se erano fondamentalmente corrette. La risposta è che non puoi.

Quando si lavora da una definizione di storyboard, le istanze del controller di visualizzazione vengono tutte archiviate. Quindi, per iniziarli è necessario che init?(coder...vengano utilizzati. È da coderdove provengono tutte le impostazioni / informazioni di visualizzazione.

Quindi, in questo caso, non è possibile chiamare anche qualche altra funzione di inizializzazione con un parametro personalizzato. Dovrebbe essere impostato come proprietà durante la preparazione del segue, oppure è possibile abbandonare i segues e caricare le istanze direttamente dallo storyboard e configurarle (fondamentalmente un modello di fabbrica che utilizza uno storyboard).

In tutti i casi si utilizza la funzione di inizializzazione richiesta dall'SDK e successivamente si passano parametri aggiuntivi.


4

Swift 5

Puoi scrivere un inizializzatore personalizzato come questo ->

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

3

UIViewControllerclasse conforme al NSCodingprotocollo che è definito come:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

Quindi UIViewControllerha due inizializzatori designati init?(coder aDecoder: NSCoder)e init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad chiama init?(coder aDecoder: NSCoder)direttamente init UIViewControllere UIView, non c'è spazio per passare i parametri.

Una soluzione alternativa ingombrante è utilizzare una cache temporanea:

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
}


1

Il flusso corretto è, chiama l'inizializzatore designato che in questo caso è l'init con 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

}

0

Disclaimer: non lo sostengo e non ho testato a fondo la sua resilienza, ma è una potenziale soluzione che ho scoperto giocando.

Tecnicamente, l'inizializzazione personalizzata può essere ottenuta preservando l'interfaccia configurata con lo storyboard inizializzando due volte il controller di visualizzazione: la prima volta tramite la personalizzazione inite la seconda all'interno da loadView()cui si prende la vista dallo 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)
  }
}

Ora altrove nella tua app puoi chiamare il tuo inizializzatore personalizzato CustomViewController(someParameter: foo)e continuare a ricevere la configurazione della vista dallo storyboard.

Non lo considero un'ottima soluzione per diversi motivi:

  • L'inizializzazione degli oggetti viene duplicata, comprese le proprietà pre-inizializzazione
  • I parametri passati alla personalizzazione initdevono essere archiviati come proprietà facoltative
  • Aggiunge boilerplate che deve essere mantenuto quando i punti vendita / proprietà vengono modificati

Forse puoi accettare questi compromessi, ma usali a tuo rischio .


0

Anche se ora possiamo creare un inizializzazione personalizzato per i controller predefiniti nello storyboard usando instantiateInitialViewController(creator:)e per segues inclusi relazione e spettacolo.

Questa funzionalità è stata aggiunta in Xcode 11 e quanto segue è un estratto dalle note di rilascio di Xcode 11 :

Un metodo del controller di visualizzazione annotato con il nuovo @IBSegueActionattributo può essere utilizzato per creare un controller di visualizzazione di destinazione di segue nel codice, usando un inizializzatore personalizzato con qualsiasi valore richiesto. Ciò rende possibile utilizzare i controller di visualizzazione con requisiti di inizializzazione non facoltativi negli storyboard. Creare una connessione da un segue a un @IBSegueActionmetodo nel relativo controller di visualizzazione di origine. Nelle nuove versioni del sistema operativo che supportano le azioni Segue, tale metodo verrà chiamato e il valore restituito sarà destinationViewControllerquello dell'oggetto segue passato prepareForSegue:sender:. @IBSegueActionÈ possibile definire più metodi su un singolo controller di visualizzazione di origine, che può alleviare la necessità di controllare le stringhe dell'identificatore segue in prepareForSegue:sender:. (47091566)

Un IBSegueActionmetodo richiede fino a tre parametri: un codificatore, il mittente e l'identificatore del segue. Il primo parametro è obbligatorio e gli altri parametri possono essere omessi dalla firma del metodo, se lo si desidera. Il NSCoderdeve essere passato all'inizializzatore del controller della vista di destinazione, per garantire che sia personalizzato con i valori configurati nello storyboard. Il metodo restituisce un controller di visualizzazione che corrisponde al tipo di controller di destinazione definito nello storyboard o nilper far sì che un controller di destinazione venga inizializzato con il init(coder:)metodo standard . Se sai di non dover restituire nil, il tipo di reso può essere non facoltativo.

In Swift, aggiungi l' @IBSegueActionattributo:

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

In Objective-C, aggiungi IBSegueActiondavanti al tipo di ritorno:

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

-1

// Il controller di visualizzazione si trova in Main.storyboard e ha un identificatore impostato

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