Inicio personalizado para UIViewController en Swift con configuración de interfaz en guión gráfico

89

Tengo problemas para escribir init personalizado para la subclase de UIViewController, básicamente quiero pasar la dependencia a través del método init para viewController en lugar de configurar la propiedad directamente como viewControllerB.property = value

Así que hice un init personalizado para mi viewController y llamé a init super designado

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

La interfaz del controlador de vista reside en el guión gráfico, también hice la interfaz para que la clase personalizada sea mi controlador de vista. Y Swift requiere llamar a este método init incluso si no está haciendo nada dentro de este método. De lo contrario, el compilador se quejará ...

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

El problema es que cuando intento llamar a mi init personalizado MyViewController(meme: meme), no inicia propiedades en mi viewController en absoluto ...

Estaba tratando de depurar, encontré en mi viewController, init(coder aDecoder: NSCoder)me llaman primero, luego se llama a mi init personalizado más tarde. Sin embargo, estos dos métodos init devuelven selfdirecciones de memoria diferentes .

Sospecho que algo anda mal con init para mi viewController, y siempre regresará selfcon init?(coder aDecoder: NSCoder), que no tiene implementación.

¿Alguien sabe cómo hacer un init personalizado para su viewController correctamente? Nota: la interfaz de mi viewController está configurada en el guión gráfico

aquí está mi código 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
fuente
¿Obtuviste una solución para esto?
user481610

Respuestas:

49

Como se especificó en una de las respuestas anteriores, no puede usar el método de inicio personalizado y el guión gráfico. Pero aún puede usar un método estático para crear una instancia de ViewController desde un guión gráfico y realizar una configuración adicional en él. Se verá así:

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

No olvide especificar IdentifierOfYouViewController como identificador del controlador de vista en su guión gráfico. También es posible que deba cambiar el nombre del guión gráfico en el código anterior.

Alexander Grushevoy
fuente
Después de inicializar MemeDetailVC, existe la propiedad "meme". Luego entra la inyección de dependencia para asignar el valor a "meme". En este momento, no se ha llamado a loadView () y viewDidLoad. Estos dos métodos se llamarán después de que MemeDetailVC agregue / presione para ver la jerarquía.
Jim Yu
¡La mejor respuesta aquí!
PascalS
Todavía se requiere método init, y el compilador dirá meme bienes almacenados no se ha inicializado
Surendra Kumar
27

No puede usar un inicializador personalizado cuando inicializa desde un Storyboard, así init?(coder aDecoder: NSCoder)es como Apple diseñó el storyboard para inicializar un controlador. Sin embargo, hay formas de enviar datos a un archivo UIViewController.

El nombre de su controlador de vista tiene detailen él, así que supongo que llega desde un controlador diferente. En este caso, puede usar el prepareForSeguemétodo para enviar datos al detalle (esto es Swift 3):

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

Solo usé una propiedad de tipo en Stringlugar de Memecon fines de prueba. Además, asegúrese de pasar el identificador de segue correcto ( "identifier"era solo un marcador de posición).

Caleb Kleveter
fuente
1
Hola, pero ¿cómo podemos deshacernos de la especificación como opcional y no queremos equivocarnos al no asignarla? Solo queremos que exista la dependencia estricta significativa para el controlador en primer lugar.
Amber K
1
@AmberK Es posible que desee estudiar la programación de su interfaz en lugar de utilizar Interface Builder.
Caleb Kleveter
De acuerdo, también estoy buscando el proyecto ios de kickstarter como referencia, no puedo decir todavía cómo envían sus modelos de vista.
Amber K
23

Como ha señalado @Caleb Kleveter, no podemos usar un inicializador personalizado al inicializar desde un Storyboard.

Pero, podemos resolver el problema utilizando el método de fábrica / clase que instancia el objeto del controlador de vista desde Storyboard y devuelve el objeto del controlador de vista. Creo que esta es una forma genial.

Nota: Esta no es una respuesta exacta a la pregunta, sino una solución para resolver el problema.

Haga el método de clase, en la clase MemeDetailVC, de la siguiente manera:

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

Uso:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Nitesh Borad
fuente
5
Pero el inconveniente sigue ahí, la propiedad tiene que ser opcional.
Amber K
cuando se usa class func init (meme: Meme) -> MemeDetailVCXcode 10.2 se confunde y da el errorIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël
21

Una forma de hacerlo es con un inicializador de conveniencia.

class MemeDetailVC : UIViewController {

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

Luego inicializa su MemeDetailVC con let memeDetailVC = MemeDetailVC(theMeme)

La documentación de Apple sobre los inicializadores es bastante buena, pero mi favorito personal es la serie de tutoriales Ray Wenderlich: Initialization in Depth , que debería darte muchas explicaciones / ejemplos sobre tus diversas opciones de inicio y la forma "correcta" de hacer las cosas.


EDITAR : Si bien puede usar un inicializador de conveniencia en los controladores de vista personalizados, todos tienen razón al afirmar que no puede usar inicializadores personalizados al inicializar desde el guión gráfico o mediante una secuencia de guión gráfico.

Si su interfaz está configurada en el guión gráfico y está creando el controlador de forma completamente programática, entonces un inicializador de conveniencia es probablemente la forma más fácil de hacer lo que está tratando de hacer, ya que no tiene que lidiar con el init requerido con el NSCoder (que todavía no entiendo realmente).

Sin embargo, si obtiene su controlador de vista a través del guión gráfico, deberá seguir la respuesta de @Caleb Kleveter y convertir el controlador de vista en la subclase deseada y luego configurar la propiedad manualmente.

Ponyboy47
fuente
1
No lo entiendo, si mi interfaz está configurada en el guión gráfico y sí, la estoy creando programáticamente, entonces debo llamar al método de creación de instancias usando el guión gráfico para cargar la interfaz, ¿verdad? ¿Cómo ayudará el convenienceis aquí?
Amber K
Las clases en swift no se pueden inicializar llamando a otra initfunción de la clase (aunque las estructuras sí). Entonces, para llamar self.init(), debe marcar init(meme: Meme)como convenienceinicializador. De lo contrario, tendría que configurar manualmente todas las propiedades requeridas de a UIViewControlleren el inicializador usted mismo, y no estoy seguro de cuáles son todas esas propiedades.
Ponyboy47
esto no es una subclase de UIViewController entonces, porque está llamando a self.init () y no a super.init (). todavía puede inicializar el MemeDetailVC usando init predeterminado como MemeDetail()y en ese caso el código fallará
Ali
Todavía se considera una subclasificación porque self.init () tendría que llamar a super.init () en su implementación o quizás self.init () se hereda directamente del padre, en cuyo caso son funcionalmente equivalentes.
Ponyboy47
6

Originalmente había un par de respuestas, que fueron votadas y eliminadas aunque eran básicamente correctas. La respuesta es que no puedes.

Cuando se trabaja desde una definición de guión gráfico, todas las instancias del controlador de vista se archivan. Entonces, para iniciarlos se requiere que init?(coder...se use. El coderes donde toda la información de los ajustes / vista viene.

Entonces, en este caso, no es posible llamar también a otra función de inicio con un parámetro personalizado. Debe establecerse como una propiedad al preparar la segue, o puede deshacerse de las segues y cargar las instancias directamente desde el guión gráfico y configurarlas (básicamente un patrón de fábrica usando un guión gráfico).

En todos los casos, utiliza la función de inicio requerida del SDK y luego pasa parámetros adicionales.

Wain
fuente
4

Rápido 5

Puede escribir un inicializador personalizado como este ->

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

UIViewControllerla clase se ajusta al NSCodingprotocolo que se define como:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

Así que UIViewControllertiene dos inicializador designado init?(coder aDecoder: NSCoder)y init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad llama init?(coder aDecoder: NSCoder)directamente a init UIViewControllery UIViewno hay espacio para que usted pase parámetros.

Una solución complicada es utilizar un caché temporal:

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

Descargo de responsabilidad: no abogo por esto y no he probado a fondo su resistencia, pero es una solución potencial que descubrí mientras jugaba.

Técnicamente, la inicialización personalizada se puede lograr mientras se conserva la interfaz configurada del guión gráfico inicializando el controlador de vista dos veces: la primera vez a través de su personalizado inity la segunda vez dentro de loadView()donde toma la vista del guión gráfico.

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

Ahora, en otra parte de su aplicación, puede llamar a su inicializador personalizado como CustomViewController(someParameter: foo)y aún recibir la configuración de vista del guión gráfico.

No considero que esta sea una gran solución por varias razones:

  • La inicialización del objeto está duplicada, incluidas las propiedades previas al inicio.
  • Los parámetros pasados ​​a la costumbre initdeben almacenarse como propiedades opcionales
  • Agrega un texto estándar que debe mantenerse a medida que se cambian los puntos de venta / propiedades

Quizás pueda aceptar estas compensaciones, pero úselas bajo su propio riesgo .

Nathan Hosselton
fuente
0

Aunque ahora podemos hacer una inicialización personalizada para los controladores predeterminados en el guión gráfico usando instantiateInitialViewController(creator:)y para segues, incluida la relación y el espectáculo.

Esta capacidad se agregó en Xcode 11 y el siguiente es un extracto de las Notas de la versión de Xcode 11 :

Se @IBSegueActionpuede usar un método de controlador de vista anotado con el nuevo atributo para crear un controlador de vista de destino de segue en el código, usando un inicializador personalizado con los valores requeridos. Esto hace posible utilizar controladores de vista con requisitos de inicialización no opcionales en los guiones gráficos. Cree una conexión desde un segue a un @IBSegueActionmétodo en su controlador de vista de origen. En las nuevas versiones del sistema operativo que admiten Segue Actions, se llamará a ese método y el valor que devuelve será el destinationViewControllerdel objeto segue al que se pasa prepareForSegue:sender:. Se @IBSegueActionpueden definir múltiples métodos en un solo controlador de vista de fuente, lo que puede aliviar la necesidad de verificar las cadenas de identificadores de segue en prepareForSegue:sender:. (47091566)

Un IBSegueActionmétodo toma hasta tres parámetros: un codificador, el remitente y el identificador del segue. El primer parámetro es obligatorio y los demás parámetros se pueden omitir de la firma de su método si lo desea. Se NSCoderdebe pasar al inicializador del controlador de la vista de destino para garantizar que esté personalizado con los valores configurados en el guión gráfico. El método devuelve un controlador de vista que coincide con el tipo de controlador de destino definido en el guión gráfico, o nilpara hacer que un controlador de destino se inicialice con el init(coder:)método estándar . Si sabe que no necesita regresar nil, el tipo de devolución puede ser no opcional.

En Swift, agregue el @IBSegueActionatributo:

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

En Objective-C, agregue IBSegueActiondelante del tipo de retorno:

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

El flujo correcto es llamar al inicializador designado que en este caso es el 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

}
Rahul Bansal
fuente
-1

// El controlador de vista está en Main.storyboard y tiene un identificador establecido

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

Clase A

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

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