Iniciação personalizada para UIViewController em Swift com configuração de interface em storyboard

89

Estou tendo problemas para escrever init personalizado para a subclasse de UIViewController, basicamente, quero passar a dependência por meio do método init para viewController em vez de definir a propriedade diretamente como viewControllerB.property = value

Então fiz um init personalizado para meu viewController e chamei o init super designado

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

A interface do controlador de visualização reside no storyboard, eu também fiz a interface da classe customizada para ser meu controlador de visualização. E o Swift requer a chamada deste método init mesmo se você não estiver fazendo nada dentro desse método. Caso contrário, o compilador irá reclamar ...

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

O problema é que quando tento chamar o meu init personalizado, MyViewController(meme: meme)ele não faz as propriedades do init no meu viewController ...

Eu estava tentando depurar, encontrei em meu viewController, fui init(coder aDecoder: NSCoder)chamado primeiro e, em seguida, meu init personalizado foi chamado mais tarde. No entanto, esses dois métodos init retornam selfendereços de memória diferentes .

Estou suspeitando de algo errado com o init do meu viewController e ele sempre retornará selfcom o init?(coder aDecoder: NSCoder), que não tem implementação.

Alguém sabe como fazer init personalizado para seu viewController corretamente? Nota: a interface do meu viewController é configurada no storyboard

aqui está meu 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)
    }

}
Mosca
fonte
você conseguiu uma solução para isso?
user481610

Respostas:

50

Como foi especificado em uma das respostas acima, você não pode usar o método init personalizado e o storyboard.

Mas você ainda pode usar um método estático para instanciar a ViewControllerpartir de um storyboard e realizar configurações adicionais nele.

Isso parecerá assim:

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

Não se esqueça de especificar IdentifierOfYouViewController como identificador do controlador de visualização em seu storyboard. Você também pode precisar alterar o nome do storyboard no código acima.

Alexander Grushevoy
fonte
Depois que MemeDetailVC é inicializado, a propriedade "meme" existe. Em seguida, a injeção de dependência chega para atribuir o valor a "meme". No momento, loadView () e viewDidLoad não foram chamados. Esses dois métodos serão chamados após adicionar / enviar MemeDetailVC para visualizar a hierarquia.
Jim Yu
Melhor resposta aqui!
PascalS
Ainda assim, o método init é necessário, e o compilador dirá que a propriedade meme armazenada não foi inicializada
Surendra Kumar
27

Você não pode usar um inicializador personalizado ao inicializar a partir de um Storyboard; init?(coder aDecoder: NSCoder)foi assim que a Apple projetou o storyboard para inicializar um controlador. No entanto, existem maneiras de enviar dados para a UIViewController.

O nome do seu controlador de visualização está detailnele, então suponho que você o tenha obtido de um controlador diferente. Neste caso, você pode usar o prepareForSeguemétodo para enviar dados para o detalhe (este é o Swift 3):

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

Acabei de usar uma propriedade do tipo em Stringvez de Memepara fins de teste. Além disso, certifique-se de passar o identificador de segue correto ( "identifier"era apenas um marcador de posição).

Caleb Kleveter
fonte
1
Olá, mas como podemos nos livrar da especificação como opcional e também não queremos errar em não atribuí-la. Queremos apenas a dependência estrita significativa para o controlador existir em primeiro lugar.
Amber K
1
@AmberK Você pode querer dar uma olhada na programação de sua interface em vez de usar o Interface Builder.
Caleb Kleveter
Ok, também estou procurando o projeto ios do kickstarter para referência também, não sou capaz de dizer ainda como eles enviam seus modelos de visualização.
Amber K de
23

Como @Caleb Kleveter apontou, não podemos usar um inicializador personalizado ao inicializar a partir de um Storyboard.

Mas, podemos resolver o problema usando o método de fábrica / classe que instancia o objeto do controlador de visão do Storyboard e retorna o objeto do controlador de visão. Eu acho que essa é uma maneira bem legal.

Nota: Esta não é uma resposta exata à pergunta, mas uma solução alternativa para resolver o problema.

Faça o método da classe, na classe MemeDetailVC, da seguinte maneira:

// 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
fonte
5
Mas ainda existe a desvantagem, a propriedade tem que ser opcional.
Amber K
ao usar o class func init (meme: Meme) -> MemeDetailVCXcode 10.2 é confuso e apresenta o erroIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël
21

Uma maneira de fazer isso é com um inicializador de conveniência.

class MemeDetailVC : UIViewController {

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

Então você inicializa seu MemeDetailVC com let memeDetailVC = MemeDetailVC(theMeme)

A documentação da Apple sobre inicializadores é muito boa, mas meu favorito pessoal é a série de tutoriais Ray Wenderlich: Initialization in Depth , que deve fornecer muitas explicações / exemplos sobre suas várias opções de init e a maneira "correta" de fazer as coisas.


EDITAR : Embora você possa usar um inicializador de conveniência em controladores de exibição personalizados, todos estão corretos ao afirmar que você não pode usar inicializadores personalizados ao inicializar a partir do storyboard ou por meio de um segue de storyboard.

Se sua interface estiver configurada no storyboard e você estiver criando o controlador completamente programaticamente, um inicializador de conveniência é provavelmente a maneira mais fácil de fazer o que você está tentando fazer, já que não precisa lidar com o init requerido o NSCoder (que ainda não entendi direito).

Porém, se você estiver obtendo seu controlador de visualização por meio do storyboard, precisará seguir a resposta de @Caleb Kleveter e lançar o controlador de visualização em sua subclasse desejada e definir a propriedade manualmente.

Ponyboy47
fonte
1
Não entendi, se minha interface estiver configurada no storyboard e sim, estou criando-a programaticamente, devo chamar o método instanciar usando o storyboard para carregar a interface, certo? Como o convenienceis ajuda aqui.
Amber K
Classes em swift não podem ser inicializadas chamando outra initfunção da classe (embora as estruturas possam). Portanto, para chamar self.init(), você deve marcar o init(meme: Meme)como um convenienceinicializador. Caso contrário, você teria que definir manualmente todas as propriedades necessárias de a UIViewControllerno inicializador, e não tenho certeza de quais são todas essas propriedades.
Ponyboy47
isso não é uma subclasse de UIViewController, porque você está chamando self.init () e não super.init (). você ainda pode inicializar o MemeDetailVC usando o init padrão como MemeDetail()e, nesse caso, o código irá travar
Ali
Ele ainda é considerado uma subclasse porque self.init () teria que chamar super.init () em sua implementação ou talvez self.init () seja herdado diretamente do pai, caso em que são funcionalmente equivalentes.
Ponyboy47
6

Havia originalmente algumas respostas, que foram votadas e excluídas, embora estivessem basicamente corretas. A resposta é: você não pode.

Ao trabalhar a partir de uma definição de storyboard, suas instâncias do controlador de visualização são todas arquivadas. Então, para iniciá-los é necessário que init?(coder...seja usado. O coderé o lugar onde todas as informações de definições / fotografia vem.

Portanto, neste caso, não é possível chamar também alguma outra função init com um parâmetro personalizado. Ele deve ser definido como uma propriedade ao preparar a segue, ou você pode abandonar segues e carregar as instâncias diretamente do storyboard e configurá-las (basicamente um padrão de fábrica usando um storyboard).

Em todos os casos, você usa a função init necessária do SDK e passa parâmetros adicionais posteriormente.

Wain
fonte
4

Swift 5

Você pode escrever um 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
fonte
3

UIViewControllerclasse em conformidade com o NSCodingprotocolo que é definido como:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

Então UIViewControllertem dois inicializadores designados init?(coder aDecoder: NSCoder)e init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

O Storyborad chama init?(coder aDecoder: NSCoder)diretamente para o init UIViewControllere UIView, não há espaço para você passar parâmetros.

Uma solução alternativa complicada é usar um cache temporário:

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
fonte
1

O fluxo correto é chamar o inicializador designado que, neste caso, é o init com 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
fonte
0

Isenção de responsabilidade: eu não defendo isso e não testei completamente sua resiliência, mas é uma solução potencial que descobri brincando.

Tecnicamente, a inicialização personalizada pode ser alcançada preservando a interface configurada do storyboard inicializando o controlador de visualização duas vezes: a primeira vez por meio de seu customizado inite a segunda vez dentro de loadView()onde você obtém a visualização do 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)
  }
}

Agora, em outro lugar em seu aplicativo, você pode chamar seu inicializador personalizado como CustomViewController(someParameter: foo)e ainda receber a configuração de exibição do storyboard.

Não considero esta uma ótima solução por vários motivos:

  • A inicialização do objeto é duplicada, incluindo quaisquer propriedades pré-inicialização
  • Os parâmetros passados ​​para o personalizado initdevem ser armazenados como propriedades opcionais
  • Adiciona clichês que deve ser mantido conforme as lojas / propriedades são alteradas

Talvez você possa aceitar essas compensações, mas use por sua própria conta e risco .

Nathan Hosselton
fonte
0

Embora agora possamos fazer init customizado para os controladores padrão no storyboard usando instantiateInitialViewController(creator:)e para segues incluindo relacionamento e show.

Esse recurso foi adicionado ao Xcode 11 e o seguinte é um trecho das Notas de versão do Xcode 11 :

Um método de controlador de visualização anotado com o novo @IBSegueActionatributo pode ser usado para criar um controlador de visualização de destino segue no código, usando um inicializador personalizado com quaisquer valores necessários. Isso torna possível usar controladores de visualização com requisitos de inicialização não opcionais em storyboards. Crie uma conexão de segue para um @IBSegueActionmétodo em seu controlador de exibição de origem. Em novas versões do sistema operacional que suportam Segue Actions, esse método será chamado e o valor que ele retornará será o destinationViewControllerdo objeto segue passado prepareForSegue:sender:. Vários @IBSegueActionmétodos podem ser definidos em um único controlador de exibição de origem, o que pode aliviar a necessidade de verificar as strings de identificador de segue prepareForSegue:sender:. (47091566)

Um IBSegueActionmétodo leva até três parâmetros: um codificador, o remetente e o identificador da segue. O primeiro parâmetro é obrigatório e os outros parâmetros podem ser omitidos da assinatura do seu método, se desejado. O NSCoderdeve ser passado para o inicializador do controlador de visualização de destino, para garantir que seja personalizado com os valores configurados no storyboard. O método retorna um controlador de visualização que corresponde ao tipo de controlador de destino definido no storyboard ou nilpara fazer com que um controlador de destino seja inicializado com o init(coder:)método padrão . Se você sabe que não precisa retornar nil, o tipo de retorno pode ser não opcional.

Em Swift, adicione o @IBSegueActionatributo:

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

Em Objective-C, adicione IBSegueActionna frente do tipo de retorno:

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

// O controlador de visualização está em Main.storyboard e possui um identificador definido

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
fonte