Niestandardowy init dla UIViewController w Swift z konfiguracją interfejsu w scenorysie

89

Mam problem z napisaniem niestandardowego init dla podklasy UIViewController, w zasadzie chcę przekazać zależność przez metodę init dla viewController, zamiast ustawiać właściwość bezpośrednio jak viewControllerB.property = value

Zrobiłem więc niestandardowy init dla mojego viewController i wywołałem super wyznaczony init

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

Interfejs kontrolera widoku znajduje się w scenorysie, stworzyłem również interfejs dla klasy niestandardowej, aby był moim kontrolerem widoku. Swift wymaga wywołania tej metody init, nawet jeśli nic nie robisz w ramach tej metody. W przeciwnym razie kompilator będzie narzekał ...

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

Problem polega na tym, że próbuję wywołać mój niestandardowy init, MyViewController(meme: meme)który w ogóle nie inicjuje właściwości w moim viewController ...

Próbowałem debugować, znalazłem w moim viewController, init(coder aDecoder: NSCoder)najpierw zostałem wywołany, a później mój niestandardowy init został wywołany. Jednak te dwie metody init zwracają różne selfadresy pamięci.

Podejrzewam, że coś jest nie tak z initem dla mojego kontrolera viewController i zawsze będzie zwracać selfz operatorem init?(coder aDecoder: NSCoder), który nie ma implementacji.

Czy ktoś wie, jak poprawnie wykonać niestandardowy init dla twojego viewController? Uwaga: mój interfejs viewControllera jest skonfigurowany w scenorysie

oto mój kod 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
źródło
czy masz na to rozwiązanie?
user481610

Odpowiedzi:

49

Jak określono w jednej z powyższych odpowiedzi, nie można używać zarówno niestandardowej metody init, jak i storyboardu. Ale nadal możesz użyć metody statycznej, aby utworzyć wystąpienie ViewController z serii ujęć i przeprowadzić dodatkową konfigurację. Będzie to wyglądać tak:

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

Nie zapomnij określić IdentifierOfYouViewController jako identyfikatora kontrolera widoku w swojej serii ujęć. Może być również konieczna zmiana nazwy scenorysu w powyższym kodzie.

Alexander Grushevoy
źródło
Po zainicjowaniu MemeDetailVC istnieje właściwość „meme”. Następnie pojawia się wstrzyknięcie zależności, aby przypisać wartość do „memu”. W tej chwili loadView () i viewDidLoad nie zostały wywołane. Te dwie metody zostaną wywołane po dodaniu / wypchnięciu przez MemeDetailVC w celu wyświetlenia hierarchii.
Jim Yu,
Najlepsza odpowiedź tutaj!
PascalS,
Nadal wymagane jest metoda init, a kompilator powie meme przechowywane nieruchomość nie zostało zainicjowane
Surendra Kumar
27

Nie możesz użyć niestandardowego inicjatora podczas inicjowania z Storyboard, używając init?(coder aDecoder: NSCoder)tego, jak Apple zaprojektował scenorys do zainicjowania kontrolera. Istnieją jednak sposoby wysyłania danych do pliku UIViewController.

Nazwa kontrolera widoku zawiera się detailw sobie, więc przypuszczam, że dostajesz się tam z innego kontrolera. W takim przypadku możesz użyć prepareForSeguemetody, aby przesłać dane do szczegółów (jest to Swift 3):

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

Po prostu użyłem właściwości typu Stringzamiast Memedo celów testowych. Upewnij się również, że przekazujesz poprawny identyfikator przejścia ( "identifier"był to tylko symbol zastępczy).

Caleb Kleveter
źródło
1
Cześć, ale jak możemy pozbyć się specyfikacji jako opcjonalnej i nie chcemy też pomylić się z nie przypisywaniem jej. Chcemy tylko, aby na pierwszym miejscu istniała ścisła zależność, która ma znaczenie dla kontrolera.
Amber K
1
@AmberK Możesz chcieć przyjrzeć się programowaniu swojego interfejsu zamiast używać Interface Builder.
Caleb Kleveter
Okej, szukam też projektu na Kickstarter na iOS jako odniesienia, nie jestem jeszcze w stanie powiedzieć, w jaki sposób wysyłają swoje modele widoku.
Amber K
23

Jak zauważył @Caleb Kleveter, nie możemy użyć niestandardowego inicjatora podczas inicjalizacji z Storyboard.

Ale możemy rozwiązać ten problem, używając metody fabryki / klasy, która tworzy instancję kontrolera widoku z Storyboard i zwraca obiekt kontrolera widoku. Myślę, że to całkiem fajny sposób.

Uwaga: to nie jest dokładna odpowiedź na pytanie, a raczej obejście w celu rozwiązania problemu.

Utwórz metodę klasy, w klasie MemeDetailVC, w następujący sposób:

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

Stosowanie:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Nitesh Borad
źródło
5
Ale nadal istnieje wada, właściwość musi być opcjonalna.
Amber K
podczas korzystania class func startowych (meme: Meme) -> MemeDetailVCXcode 10.2 jest zdezorientowany i daje błądIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuel
21

Jednym ze sposobów, w jaki to zrobiłem, jest wygodna inicjalizacja.

class MemeDetailVC : UIViewController {

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

Następnie inicjalizujesz swój MemeDetailVC za pomocą let memeDetailVC = MemeDetailVC(theMeme)

Dokumentacja Apple na temat inicjatorów jest całkiem dobra, ale moim ulubionym jest seria samouczków Ray Wenderlich: Inicjalizacja w głębi, która powinna dostarczyć wielu wyjaśnień / przykładów na temat różnych opcji inicjalizacji i „właściwego” sposobu robienia rzeczy.


EDYCJA : Chociaż możesz użyć wygodnego inicjatora na niestandardowych kontrolerach widoku, wszyscy mają rację, twierdząc, że nie można używać niestandardowych inicjatorów podczas inicjowania z serii ujęć lub za pośrednictwem fragmentu scenorysu.

Jeśli twój interfejs jest skonfigurowany w scenorysie i tworzysz kontroler całkowicie programowo, to wygodny inicjator jest prawdopodobnie najłatwiejszym sposobem zrobienia tego, co próbujesz zrobić, ponieważ nie musisz zajmować się wymaganym inicjatorem NSCoder (którego nadal nie rozumiem).

Jeśli jednak otrzymujesz kontroler widoku za pośrednictwem scenorysu, musisz postępować zgodnie z odpowiedzią @Caleb Kleveter i rzucić kontroler widoku na żądaną podklasę, a następnie ustawić właściwość ręcznie.

Ponyboy47
źródło
1
Nie rozumiem, jeśli mój interfejs jest skonfigurowany w scenorysie i tak, tworzę go programowo, to muszę wywołać metodę instancji przy użyciu storyboardu, aby załadować interfejs, prawda? Jak conveniencetu pomoże.
Amber K
Klasy w swift nie mogą zostać zainicjowane przez wywołanie innej initfunkcji klasy (chociaż struktury mogą). Aby więc zadzwonić self.init(), musisz oznaczyć init(meme: Meme)jako convenienceinicjator. W przeciwnym razie musiałbyś ręcznie ustawić wszystkie wymagane właściwości a UIViewControllerw inicjatorze i nie jestem pewien, jakie są wszystkie te właściwości.
Ponyboy47
nie jest to zatem podklasa UIViewController, ponieważ wywołujesz self.init (), a nie super.init (). nadal możesz zainicjować MemeDetailVC używając domyślnego init, MemeDetail()w takim przypadku kod ulegnie awarii
Ali
Nadal jest uważane za podklasę, ponieważ self.init () musiałoby wywołać super.init () w swojej implementacji lub być może self.init () jest bezpośrednio dziedziczone od rodzica, w którym to przypadku są funkcjonalnie równoważne.
Ponyboy47
6

Pierwotnie było kilka odpowiedzi, które zostały przegłosowane przez krowę i usunięte, mimo że były zasadniczo poprawne. Odpowiedź brzmi: nie możesz.

Podczas pracy z definicją scenorysu wszystkie instancje kontrolera widoku są archiwizowane. Tak więc, aby je zainicjować, wymagane init?(coder...jest ich użycie. coderJest, gdy wszystkie informacje Ustawienia / view pochodzi.

Tak więc w tym przypadku nie jest możliwe wywołanie również innej funkcji init z parametrem niestandardowym. Powinien być ustawiony jako właściwość podczas przygotowywania skoku lub możesz porzucić segmenty i załadować wystąpienia bezpośrednio z serii ujęć i skonfigurować je (w zasadzie wzór fabryczny przy użyciu scenorysu).

We wszystkich przypadkach używasz funkcji init wymaganej przez SDK, a następnie przekazujesz dodatkowe parametry.

Fura
źródło
4

Szybki 5

Możesz napisać niestandardowy inicjator w ten sposób ->

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
źródło
3

UIViewControllerklasa jest zgodna z NSCodingprotokołem zdefiniowanym jako:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

Więc UIViewControllerma dwa wyznaczone inicjatory init?(coder aDecoder: NSCoder)i init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad wywołuje init?(coder aDecoder: NSCoder)bezpośrednio init UIViewControlleri UIViewnie ma miejsca na przekazywanie parametrów.

Jednym uciążliwym obejściem jest użycie tymczasowej pamięci podręcznej:

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
źródło
0

Uwaga: nie opowiadam się za tym i nie przetestowałem dokładnie jego odporności, ale jest to potencjalne rozwiązanie, które odkryłem podczas zabawy.

Technicznie rzecz biorąc, niestandardową inicjalizację można osiągnąć, zachowując interfejs skonfigurowany w scenorysie, inicjując dwukrotnie kontroler widoku: pierwszy raz za pomocą niestandardowego initi drugi raz w środku, w loadView()którym bierzesz widok ze scenorysu.

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

Teraz w innym miejscu aplikacji możesz wywołać swój niestandardowy inicjator, jak CustomViewController(someParameter: foo)i nadal otrzymywać konfigurację widoku z serii ujęć.

Nie uważam tego za świetne rozwiązanie z kilku powodów:

  • Inicjalizacja obiektu jest zduplikowana, w tym wszelkie właściwości pre-init
  • Parametry przekazane do niestandardowego initmuszą być przechowywane jako właściwości opcjonalne
  • Dodaje szablon, który musi być utrzymywany w przypadku zmiany gniazd / właściwości

Być może możesz zaakceptować te kompromisy, ale używaj ich na własne ryzyko .

Nathan Hosselton
źródło
0

Chociaż możemy teraz wykonać niestandardowy init dla domyślnych kontrolerów w scenorysie, używając instantiateInitialViewController(creator:)i dla płynów, w tym relacji i pokazu.

Ta funkcja została dodana w Xcode 11, a poniżej znajduje się fragment informacji o wydaniu Xcode 11 :

Metoda kontrolera widoku z adnotacją z nowym @IBSegueActionatrybutem może służyć do tworzenia kontrolera widoku docelowego płynnego w kodzie przy użyciu niestandardowego inicjatora z dowolnymi wymaganymi wartościami. Dzięki temu w scenorysach można używać kontrolerów widoku z nie opcjonalnymi wymaganiami dotyczącymi inicjalizacji. Utwórz połączenie z segue do @IBSegueActionmetody na kontrolerze widoku źródła. W nowych wersjach systemu operacyjnego, które obsługują działania Segue, ta metoda zostanie wywołana, a wartość, którą zwróci, będzie wartością destinationViewControllerprzekazanego obiektu płynnego prepareForSegue:sender:. @IBSegueActionNa jednym kontrolerze widoku źródła można zdefiniować wiele metod, co może złagodzić potrzebę sprawdzania ciągów identyfikatorów segue w prepareForSegue:sender:. (47091566)

IBSegueActionMetoda trwa do trzech parametrów: koder, nadawcy i identyfikator Segue za. Pierwszy parametr jest wymagany, a pozostałe parametry można w razie potrzeby pominąć w sygnaturze metody. NSCoderMuszą być przekazywane przez inicjatora przeznaczenia do widoku kontrolera, dzięki którym będzie ona dostosowana do wartości skonfigurowanych w serii ujęć. Metoda zwraca kontroler widoku, który jest zgodny z typem kontrolera docelowego zdefiniowanym w scenorysie lub nilpowoduje zainicjowanie kontrolera docelowego init(coder:)metodą standardową . Jeśli wiesz, że nie musisz zwracać nil, typ zwrotu może być nieobowiązkowy.

W Swift dodaj @IBSegueActionatrybut:

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

W celu C dodaj IBSegueActionprzed zwracanym typem:

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

Prawidłowy przepływ to wywołanie wyznaczonego inicjatora, którym w tym przypadku jest init z 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
źródło
-1

// Kontroler widoku znajduje się w Main.storyboard i ma ustawiony identyfikator

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

Klasa A

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

 navigationController?.pushViewController(objB,animated: true)
kaszish makkar
źródło