Benutzerdefinierte Init für UIViewController in Swift mit Schnittstelleneinrichtung im Storyboard

89

Ich habe Probleme beim Schreiben eines benutzerdefinierten Init für die Unterklasse von UIViewController. Grundsätzlich möchte ich die Abhängigkeit über die Init-Methode für ViewController übergeben, anstatt die Eigenschaft direkt wie festzulegen viewControllerB.property = value

Also habe ich eine benutzerdefinierte Init für meinen viewController erstellt und einen super designierten Init aufgerufen

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

Die View Controller-Oberfläche befindet sich im Storyboard. Ich habe auch die Schnittstelle für die benutzerdefinierte Klasse zu meinem View Controller gemacht. Und Swift muss diese init-Methode aufrufen, auch wenn Sie innerhalb dieser Methode nichts tun. Andernfalls beschwert sich der Compiler ...

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

Das Problem ist, wenn ich versuche, meinen benutzerdefinierten Init aufzurufen, MyViewController(meme: meme)ohne dass die Eigenschaften in meinem viewController überhaupt nicht initialisiert werden ...

Ich habe versucht zu debuggen, ich habe in meinem viewController gefunden, werde zuerst init(coder aDecoder: NSCoder)aufgerufen, dann wird mein benutzerdefinierter Init später aufgerufen. Diese beiden Init-Methoden geben jedoch unterschiedliche selfSpeicheradressen zurück.

Ich vermute, dass mit dem init für meinen viewController etwas nicht stimmt, und er wird immer selfmit dem zurückgegeben init?(coder aDecoder: NSCoder), der keine Implementierung hat.

Weiß jemand, wie man benutzerdefinierte Init für Ihren viewController richtig macht? Hinweis: Die Benutzeroberfläche meines viewControllers ist im Storyboard eingerichtet

Hier ist mein viewController-Code:

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

}
Ferkel
quelle
Hast du eine Lösung dafür bekommen?
user481610

Antworten:

49

Wie in einer der obigen Antworten angegeben, können Sie nicht sowohl die benutzerdefinierte Init-Methode als auch das Storyboard verwenden. Sie können jedoch weiterhin eine statische Methode verwenden, um ViewController über ein Storyboard zu instanziieren und zusätzliche Einstellungen vorzunehmen. Es wird so aussehen:

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

Vergessen Sie nicht, IdentifierOfYouViewController als View Controller-ID in Ihrem Storyboard anzugeben. Möglicherweise müssen Sie auch den Namen des Storyboards im obigen Code ändern.

Alexander Grushevoy
quelle
Nach der Initialisierung von MemeDetailVC ist die Eigenschaft "meme" vorhanden. Dann kommt die Abhängigkeitsinjektion herein, um den Wert "meme" zuzuweisen. Derzeit wurden loadView () und viewDidLoad nicht aufgerufen. Diese beiden Methoden werden nach dem Hinzufügen / Drücken von MemeDetailVC aufgerufen, um die Hierarchie anzuzeigen.
Jim Yu
Beste Antwort hier!
PascalS
Die Init-Methode ist weiterhin erforderlich, und der Compiler gibt an, dass die gespeicherte Eigenschaft meme nicht initialisiert wurde
Surendra Kumar,
27

Sie können keinen benutzerdefinierten Initialisierer verwenden, wenn Sie von einem Storyboard aus initialisieren. Auf diese init?(coder aDecoder: NSCoder)Weise hat Apple das Storyboard zum Initialisieren eines Controllers entworfen. Es gibt jedoch Möglichkeiten, Daten an a zu senden UIViewController.

Der Name Ihres View Controllers ist detaildarin enthalten, also nehme ich an, dass Sie von einem anderen Controller dorthin gelangen. In diesem Fall können Sie die prepareForSegueMethode verwenden, um Daten an das Detail zu senden (Dies ist Swift 3):

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

Ich habe nur eine Eigenschaft vom Typ Stringanstelle von Memezu Testzwecken verwendet. Stellen Sie außerdem sicher, dass Sie die richtige Segue-ID eingeben ( "identifier"war nur ein Platzhalter).

Caleb Kleveter
quelle
1
Hallo, aber wie können wir die Angabe als optional loswerden und wir wollen auch nicht verwechseln, wenn wir sie nicht zuweisen. Wir wollen nur, dass die strikte Abhängigkeit, die für den Controller von Bedeutung ist, an erster Stelle steht.
Amber K
1
@AmberK Möglicherweise möchten Sie die Programmierung Ihrer Schnittstelle untersuchen, anstatt Interface Builder zu verwenden.
Caleb Kleveter
Okay, ich suche auch ein Kickstarter-iOS-Projekt als Referenz und kann noch nicht sagen, wie sie ihre Ansichtsmodelle senden.
Amber K
23

Wie @Caleb Kleveter hervorgehoben hat, können wir beim Initialisieren von einem Storyboard keinen benutzerdefinierten Initialisierer verwenden.

Wir können das Problem jedoch lösen, indem wir die Factory / Class-Methode verwenden, mit der das View Controller-Objekt aus dem Storyboard instanziiert und das View Controller-Objekt zurückgegeben wird. Ich denke, das ist ein ziemlich cooler Weg.

Hinweis: Dies ist keine genaue Antwort auf die Frage, sondern eine Problemumgehung zur Lösung des Problems.

Erstellen Sie die Klassenmethode in der MemeDetailVC-Klasse wie folgt:

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

Verwendung:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Nitesh Borad
quelle
5
Aber der Nachteil ist immer noch da, die Eigenschaft muss optional sein.
Amber K
bei der Verwendung von class func init (meme: Meme) -> MemeDetailVCXcode 10.2 ist verwirrt und gibt den FehlerIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël
21

Eine Möglichkeit, dies zu tun, ist mit einem praktischen Initialisierer.

class MemeDetailVC : UIViewController {

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

Dann initialisieren Sie Ihre MemeDetailVC mit let memeDetailVC = MemeDetailVC(theMeme)

Apples Dokumentation zu Initialisierern ist ziemlich gut, aber mein persönlicher Favorit ist die Tutorialserie Ray Wenderlich: Initialization in Depth , die Ihnen viele Erklärungen / Beispiele zu Ihren verschiedenen Init-Optionen und der "richtigen" Vorgehensweise geben sollte.


BEARBEITEN : Während Sie auf benutzerdefinierten Ansichts-Controllern einen praktischen Initialisierer verwenden können, gibt jeder zu Recht an, dass Sie beim Initialisieren vom Storyboard oder über einen Storyboard-Übergang keine benutzerdefinierten Initialisierer verwenden können.

Wenn Ihre Benutzeroberfläche im Storyboard eingerichtet ist und Sie den Controller vollständig programmgesteuert erstellen, ist ein praktischer Initialisierer wahrscheinlich der einfachste Weg, um das zu tun, was Sie versuchen, da Sie sich nicht mit dem erforderlichen Init befassen müssen der NSCoder (den ich immer noch nicht wirklich verstehe).

Wenn Sie Ihren View Controller jedoch über das Storyboard erhalten, müssen Sie der Antwort von @Caleb Kleveter folgen, den View Controller in die gewünschte Unterklasse umwandeln und die Eigenschaft manuell festlegen.

Ponyboy47
quelle
1
Ich verstehe es nicht. Wenn meine Benutzeroberfläche im Storyboard eingerichtet ist und ich sie programmgesteuert erstelle, muss ich die Instanziierungsmethode mit Storyboard aufrufen, um die Benutzeroberfläche zu laden, oder? Wie wird das conveniencehier helfen?
Amber K
Klassen in Swift können nicht durch Aufrufen einer anderen initFunktion der Klasse initialisiert werden (Strukturen können dies jedoch). Um aufzurufen self.init(), müssen Sie das init(meme: Meme)als convenienceInitialisierer markieren . Andernfalls müssten Sie alle erforderlichen Eigenschaften von a UIViewControllerim Initialisierer selbst manuell festlegen , und ich bin mir nicht sicher, was all diese Eigenschaften sind.
Ponyboy47
Dies ist dann keine Unterklasse von UIViewController, da Sie self.init () und nicht super.init () aufrufen. Sie können die MemeDetailVC weiterhin mit der Standardeinstellung init initialisieren. MemeDetail()In diesem Fall stürzt der Code ab
Ali
Es wird immer noch als Unterklasse betrachtet, da self.init () in seiner Implementierung super.init () aufrufen müsste oder self.init () direkt vom übergeordneten Element geerbt wird. In diesem Fall sind sie funktional äquivalent.
Ponyboy47
6

Es gab ursprünglich ein paar Antworten, die von der Kuh gewählt und gelöscht wurden, obwohl sie im Grunde richtig waren. Die Antwort ist, Sie können nicht.

Wenn Sie mit einer Storyboard-Definition arbeiten, werden alle Ihre View Controller-Instanzen archiviert. Um sie zu initiieren, init?(coder...muss sie verwendet werden. Das coderist , wo alle Einstellungen / view Informationen stammen aus.

In diesem Fall ist es also nicht möglich, eine andere Init-Funktion mit einem benutzerdefinierten Parameter aufzurufen. Es sollte entweder als Eigenschaft bei der Vorbereitung des Segues festgelegt werden, oder Sie können Segues loswerden und die Instanzen direkt aus dem Storyboard laden und konfigurieren (im Grunde ein Factory-Muster mit einem Storyboard).

In allen Fällen verwenden Sie die für das SDK erforderliche Init-Funktion und übergeben anschließend zusätzliche Parameter.

Wain
quelle
4

Swift 5

Sie können einen benutzerdefinierten Initialisierer wie folgt schreiben ->

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
quelle
3

UIViewControllerKlasse entspricht dem NSCodingProtokoll, das definiert ist als:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

So UIViewControllerhat zwei bezeichnete Initialisierer init?(coder aDecoder: NSCoder)und init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad ruft init?(coder aDecoder: NSCoder)direkt bei init auf UIViewControllerund UIView, Es gibt keinen Platz für Sie, um Parameter zu übergeben.

Eine umständliche Problemumgehung besteht darin, einen temporären Cache zu verwenden:

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
quelle
0

Haftungsausschluss: Ich befürworte dies nicht und habe seine Widerstandsfähigkeit nicht gründlich getestet, aber es ist eine mögliche Lösung, die ich beim Herumspielen entdeckt habe.

Technisch gesehen kann eine benutzerdefinierte Initialisierung unter Beibehaltung der vom Storyboard konfigurierten Oberfläche erreicht werden, indem der Ansichts-Controller zweimal initialisiert wird: das erste Mal über Ihre benutzerdefinierte initund das zweite Mal, loadView()wenn Sie die Ansicht vom Storyboard aus übernehmen.

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

Jetzt können Sie an anderer Stelle in Ihrer App Ihren benutzerdefinierten Initialisierer wie aufrufen CustomViewController(someParameter: foo) und trotzdem die Ansichtskonfiguration vom Storyboard erhalten.

Ich halte dies aus mehreren Gründen nicht für eine großartige Lösung:

  • Die Objektinitialisierung wird dupliziert, einschließlich aller Pre-Init-Eigenschaften
  • An den Benutzer übergebene Parameter initmüssen als optionale Eigenschaften gespeichert werden
  • Fügt eine Kesselplatte hinzu, die beibehalten werden muss, wenn die Auslässe / Eigenschaften geändert werden

Vielleicht können Sie diese Kompromisse akzeptieren, aber auf eigenes Risiko verwenden .

Nathan Hosselton
quelle
0

Obwohl wir jetzt benutzerdefinierte Init für die Standard-Controller im Storyboard verwenden können instantiateInitialViewController(creator:) und für Segmente einschließlich Beziehung und Show durchführen können.

Diese Funktion wurde in Xcode 11 hinzugefügt. Das Folgende ist ein Auszug aus den Versionshinweisen zu Xcode 11 :

Eine mit dem neuen @IBSegueActionAttribut versehene View-Controller-Methode kann verwendet werden, um den Ziel-View-Controller eines Segues im Code mithilfe eines benutzerdefinierten Initialisierers mit den erforderlichen Werten zu erstellen. Dies ermöglicht die Verwendung von View-Controllern mit nicht optionalen Initialisierungsanforderungen in Storyboards. Erstellen Sie eine Verbindung von einem Übergang zu einer @IBSegueActionMethode auf dem Quellansichts-Controller. In neuen Betriebssystemversionen, die Segue-Aktionen unterstützen, wird diese Methode aufgerufen und der zurückgegebene Wert ist der destinationViewControllerdes übergebenen Segue-Objekts prepareForSegue:sender:. @IBSegueActionAuf einem einzelnen Quellansichts-Controller können mehrere Methoden definiert werden, wodurch die Notwendigkeit verringert werden kann, Zeichenfolgen für die Segue-ID zu überprüfen prepareForSegue:sender:. (47091566)

Eine IBSegueActionMethode akzeptiert bis zu drei Parameter: einen Codierer, den Absender und die Kennung des Segues. Der erste Parameter ist erforderlich, und die anderen Parameter können bei Bedarf in der Signatur Ihrer Methode weggelassen werden. Das NSCodermuss an den Initialisierer des Zielansichts-Controllers weitergeleitet werden, um sicherzustellen, dass es mit den im Storyboard konfigurierten Werten angepasst wird. Die Methode gibt einen Ansichtscontroller zurück, der dem im Storyboard definierten Zielcontrollertyp entspricht, oder veranlasst nil, dass ein Zielcontroller mit der Standardmethode initialisiert init(coder:)wird. Wenn Sie wissen, dass Sie nicht zurückkehren müssen, nilkann der Rückgabetyp nicht optional sein.

Fügen Sie in Swift das @IBSegueActionAttribut hinzu:

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

Fügen Sie in Objective-C IBSegueActionvor dem Rückgabetyp Folgendes hinzu:

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

Der richtige Ablauf ist, rufen Sie den angegebenen Initialisierer auf, in diesem Fall den Init mit 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
quelle
-1

// View Controller befindet sich in Main.storyboard und hat einen festgelegten Bezeichner

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

Klasse a

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

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