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 self
adresy pamięci.
Podejrzewam, że coś jest nie tak z initem dla mojego kontrolera viewController i zawsze będzie zwracać self
z 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)
}
}
źródło
Odpowiedzi:
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.
źródło
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 plikuUIViewController
.Nazwa kontrolera widoku zawiera się
detail
w sobie, więc przypuszczam, że dostajesz się tam z innego kontrolera. W takim przypadku możesz użyćprepareForSegue
metody, 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
String
zamiastMeme
do celów testowych. Upewnij się również, że przekazujesz poprawny identyfikator przejścia ("identifier"
był to tylko symbol zastępczy).źródło
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())
źródło
class func
startowych(meme: Meme) -> MemeDetailVC
Xcode 10.2 jest zdezorientowany i daje błądIncorrect argument label in call (have 'meme:', expected 'coder:')
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.
źródło
convenience
tu pomoże.init
funkcji klasy (chociaż struktury mogą). Aby więc zadzwonićself.init()
, musisz oznaczyćinit(meme: Meme)
jakoconvenience
inicjator. W przeciwnym razie musiałbyś ręcznie ustawić wszystkie wymagane właściwości aUIViewController
w inicjatorze i nie jestem pewien, jakie są wszystkie te właściwości.MemeDetail()
w takim przypadku kod ulegnie awariiPierwotnie 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.coder
Jest, 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.
źródło
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 } }
źródło
UIViewController
klasa jest zgodna zNSCoding
protokołem zdefiniowanym jako:public protocol NSCoding { public func encode(with aCoder: NSCoder) public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER }
Więc
UIViewController
ma dwa wyznaczone inicjatoryinit?(coder aDecoder: NSCoder)
iinit(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
.Storyborad wywołuje
init?(coder aDecoder: NSCoder)
bezpośrednio initUIViewController
iUIView
nie 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 }
źródło
Począwszy od iOS 13, możesz zainicjować kontroler widoku, który znajduje się w scenorysie przy użyciu
instantiateViewController(identifier:creator:)
metody : wUIStoryboard
instancji.samouczek: https://sarunw.com/posts/better-dependency-injection-for-storyboards-in-ios13/
źródło
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
init
i drugi raz w środku, wloadView()
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:
init
muszą być przechowywane jako właściwości opcjonalneByć może możesz zaakceptować te kompromisy, ale używaj ich na własne ryzyko .
źródło
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
@IBSegueAction
atrybutem 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@IBSegueAction
metody 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ądestinationViewController
przekazanego obiektu płynnegoprepareForSegue:sender:
.@IBSegueAction
Na jednym kontrolerze widoku źródła można zdefiniować wiele metod, co może złagodzić potrzebę sprawdzania ciągów identyfikatorów segue wprepareForSegue:sender:
. (47091566)IBSegueAction
Metoda 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.NSCoder
Muszą 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 lubnil
powoduje zainicjowanie kontrolera docelowegoinit(coder:)
metodą standardową . Jeśli wiesz, że nie musisz zwracaćnil
, typ zwrotu może być nieobowiązkowy.W Swift dodaj
@IBSegueAction
atrybut:@IBSegueAction func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? { PetController( coder: coder, petName: self.selectedPetName, type: .dog ) }
W celu C dodaj
IBSegueAction
przed zwracanym typem:- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder sender:(id)sender segueIdentifier:(NSString *)segueIdentifier { return [PetController initWithCoder:coder petName:self.selectedPetName type:@"dog"]; }
źródło
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 }
źródło
// 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)
źródło