Powiedzmy, że mam wiele kontrolerów widoku w mojej aplikacji Swift i chcę mieć możliwość przekazywania danych między nimi. Jeśli jestem kilka poziomów niżej w stosie kontrolera widoku, w jaki sposób mogę przekazać dane do innego kontrolera widoku? Lub między kartami w kontrolerze widoku paska kart?
(Uwaga, to pytanie jest „dzwonkiem”). Zostało zadane tak często, że postanowiłem napisać tutorial na ten temat. Zobacz moją odpowiedź poniżej.
Odpowiedzi:
Twoje pytanie jest bardzo szerokie. Sugerowanie, że istnieje jedno proste, uniwersalne rozwiązanie dla każdego scenariusza, jest trochę naiwne. Przejdźmy więc przez niektóre z tych scenariuszy.
Z mojego doświadczenia wynika, że najczęstszym scenariuszem dotyczącym przepełnienia stosu jest proste przekazywanie informacji z jednego kontrolera widoku do drugiego.
Jeśli używamy scenorysu, nasz pierwszy kontroler widoku może przesłonić
prepareForSegue
, co jest dokładnie tym, do czego służy.UIStoryboardSegue
Obiekt zostanie przekazany w kiedy ta metoda jest wywoływana i zawiera odniesienie do naszego widzenia docelowego sterownika. Tutaj możemy ustawić wartości, które chcemy przekazać.override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "MySegueID" { if let destination = segue.destination as? SecondController { destination.myInformation = self.myInformation } } }
Alternatywnie, jeśli nie używamy scenorysów, ładujemy nasz kontroler widoku ze stalówki. Nasz kod jest wtedy nieco prostszy.
func showNextController() { let destination = SecondController(nibName: "SecondController", bundle: nil) destination.myInformation = self.myInformation show(destination, sender: self) }
W obu przypadkach
myInformation
jest to właściwość na każdym kontrolerze widoku przechowująca wszelkie dane, które mają być przekazane z jednego kontrolera widoku do drugiego. Oczywiście nie muszą mieć tej samej nazwy na każdym kontrolerze.Możemy również chcieć udostępniać informacje między kartami w pliku
UITabBarController
.W tym przypadku jest to potencjalnie jeszcze prostsze.
Najpierw utwórzmy podklasę
UITabBarController
i nadajmy jej właściwości dla wszelkich informacji, które chcemy udostępniać między różnymi zakładkami:class MyCustomTabController: UITabBarController { var myInformation: [String: AnyObject]? }
Teraz, jeśli tworzymy naszą aplikację z storyboardu, po prostu zmieniamy klasę naszego kontrolera paska kart z domyślnej
UITabBarController
naMyCustomTabController
. Jeśli nie używamy scenorysu, po prostu tworzymy wystąpienie tej niestandardowej klasy zamiastUITabBarController
klasy domyślnej i dodajemy do tego nasz kontroler widoku.Teraz wszystkie nasze kontrolery widoku w kontrolerze paska kart mają dostęp do tej właściwości jako takiej:
if let tbc = self.tabBarController as? MyCustomTabController { // do something with tbc.myInformation }
A poprzez tworzenie podklas
UINavigationController
w ten sam sposób możemy zastosować to samo podejście do udostępniania danych w całym stosie nawigacji:if let nc = self.navigationController as? MyCustomNavController { // do something with nc.myInformation }
Istnieje kilka innych scenariuszy. W żadnym wypadku ta odpowiedź nie obejmuje ich wszystkich.
źródło
prepareForSegue
. Szkoda, że ta bardzo prosta obserwacja ginie wśród innych odpowiedzi i dygresji.prepareForSegue
lub inne bezpośrednie przekazywanie informacji w prawie każdym scenariuszu, a następnie po prostu być w porządku z nowicjuszami, gdy pojawiają się ze scenariuszem, w którym te sytuacje nie działają, a następnie musimy uczyć ich o bardziej globalnym podejściu.To pytanie pojawia się cały czas.
Jedną z sugestii jest utworzenie pojedynczego kontenera danych: obiektu, który jest tworzony raz i tylko raz w życiu aplikacji i utrzymuje się przez cały okres użytkowania aplikacji.
To podejście jest dobrze dostosowane do sytuacji, gdy masz globalne dane aplikacji, które muszą być dostępne / modyfikowalne w różnych klasach aplikacji.
Inne podejścia, takie jak konfigurowanie jednokierunkowych lub dwukierunkowych połączeń między kontrolerami widoku, są lepiej dostosowane do sytuacji, w których przekazujesz informacje / komunikaty bezpośrednio między kontrolerami widoku.
(Zobacz odpowiedź nhgrif poniżej, aby poznać inne alternatywy.)
W przypadku pojedynczego kontenera danych dodajesz właściwość do swojej klasy, która przechowuje odwołanie do singletonu, a następnie używasz tej właściwości w dowolnym momencie, gdy potrzebujesz dostępu.
Możesz skonfigurować singleton tak, aby zapisywał jego zawartość na dysku, aby stan aplikacji utrzymywał się między uruchomieniami.
Stworzyłem projekt demonstracyjny na GitHub pokazujący, jak możesz to zrobić. Tutaj jest link:
Projekt SwiftDataContainerSingleton na GitHub Oto README z tego projektu:
SwiftDataContainerSingleton
Demonstracja użycia singletonu kontenera danych do zapisywania stanu aplikacji i udostępniania go między obiektami.
DataContainerSingleton
Klasa jest rzeczywista Singleton.Używa stałej statycznej,
sharedDataContainer
aby zapisać odwołanie do singletona.Aby uzyskać dostęp do singletona, użyj składni
DataContainerSingleton.sharedDataContainer
Przykładowy projekt definiuje 3 właściwości w kontenerze danych:
var someString: String? var someOtherString: String? var someInt: Int?
Aby załadować
someInt
właściwość z kontenera danych, należy użyć następującego kodu:let theInt = DataContainerSingleton.sharedDataContainer.someInt
Aby zapisać wartość w someInt, użyjesz składni:
DataContainerSingleton.sharedDataContainer.someInt = 3
Metoda DataContainerSingleton
init
dodaje obserwatora dlaUIApplicationDidEnterBackgroundNotification
. Ten kod wygląda następująco:goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName( UIApplicationDidEnterBackgroundNotification, object: nil, queue: nil) { (note: NSNotification!) -> Void in let defaults = NSUserDefaults.standardUserDefaults() //----------------------------------------------------------------------------- //This code saves the singleton's properties to NSUserDefaults. //edit this code to save your custom properties defaults.setObject( self.someString, forKey: DefaultsKeys.someString) defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString) defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt) //----------------------------------------------------------------------------- //Tell NSUserDefaults to save to disk now. defaults.synchronize() }
W kodzie obserwatora zapisuje właściwości kontenera danych
NSUserDefaults
. Możesz także użyćNSCoding
, Core Data lub różnych innych metod zapisywania danych stanu.Metoda DataContainerSingleton
init
próbuje również załadować zapisane wartości dla jej właściwości.Ta część metody init wygląda następująco:
let defaults = NSUserDefaults.standardUserDefaults() //----------------------------------------------------------------------------- //This code reads the singleton's properties from NSUserDefaults. //edit this code to load your custom properties someString = defaults.objectForKey(DefaultsKeys.someString) as! String? someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String? someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int? //-----------------------------------------------------------------------------
Klucze do ładowania i zapisywania wartości w NSUserDefaults są przechowywane jako stałe łańcuchowe, które są częścią struktury
DefaultsKeys
, zdefiniowane w następujący sposób:struct DefaultsKeys { static let someString = "someString" static let someOtherString = "someOtherString" static let someInt = "someInt" }
Odwołujesz się do jednej z tych stałych w ten sposób:
DefaultsKeys.someInt
Korzystanie z singletonu kontenera danych:
Ta przykładowa aplikacja umożliwia potrójne użycie pojedynczego kontenera danych.
Istnieją dwa kontrolery widoku. Pierwsza to niestandardowa podklasa UIViewController
ViewController
, a druga to niestandardowa podklasa UIViewControllerSecondVC
.Oba kontrolery widoku mają na sobie pole tekstowe i oba ładują wartość z właściwości singlelton kontenera danych
someInt
do pola tekstowego w swojejviewWillAppear
metodzie i oba zapisują bieżącą wartość z pola tekstowego z powrotem do „someInt” kontenera danych.Kod do załadowania wartości do pola tekstowego znajduje się w
viewWillAppear:
metodzie:override func viewWillAppear(animated: Bool) { //Load the value "someInt" from our shared ata container singleton let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0 //Install the value into the text field. textField.text = "\(value)" }
Kod umożliwiający zapisanie wartości edytowanej przez użytkownika z powrotem w kontenerze danych znajduje się w
textFieldShouldEndEditing
metodach kontrolerów widoku :func textFieldShouldEndEditing(textField: UITextField) -> Bool { //Save the changed value back to our data container singleton DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt() return true }
Wartości należy ładować do interfejsu użytkownika w viewWillAppear zamiast viewDidLoad, aby interfejs użytkownika był aktualizowany za każdym razem, gdy wyświetlany jest kontroler widoku.
źródło
Szybki 4
Istnieje wiele podejść do szybkiego przekazywania danych. Tutaj dodam niektóre z najlepszych podejść do tego.
1) Korzystanie ze StoryBoard Segue
Segmenty scenorysowe są bardzo przydatne do przekazywania danych między kontrolerami widoku źródłowego i docelowego i na odwrót.
// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB. @IBAction func unWindSeague (_ sender : UIStoryboardSegue) { if sender.source is ViewControllerB { if let _ = sender.source as? ViewControllerB { self.textLabel.text = "Came from B = B->A , B exited" } } } // If you want to send data from ViewControllerA to ViewControllerB override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.destination is ViewControllerB { if let vc = segue.destination as? ViewControllerB { vc.dataStr = "Comming from A View Controller" } } }
2) Korzystanie z metod delegowanych
ViewControllerD
//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data) protocol SendDataFromDelegate { func sendData(data : String) } import UIKit class ViewControllerD: UIViewController { @IBOutlet weak var textLabelD: UILabel! var delegate : SendDataFromDelegate? //Create Delegate Variable for Registering it to pass the data override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. textLabelD.text = "Child View Controller" } @IBAction func btnDismissTapped (_ sender : UIButton) { textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach" self.delegate?.sendData(data:textLabelD.text! ) _ = self.dismiss(animated: true, completion:nil) } }
ViewControllerC
import UIKit class ViewControllerC: UIViewController , SendDataFromDelegate { @IBOutlet weak var textLabelC: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) { if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as? ViewControllerD { vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method // vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing self.present(vcD, animated: true, completion: nil) } } //This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method) func sendData(data: String) { self.textLabelC.text = data } }
źródło
ViewControllerA
doViewControllerB
. Po prostu umieściłem fragment kodu na dole mojegoViewControllerA.swift
(gdzieViewControllerA.swift
właściwie jest to, jak nazywa się twój plik, oczywiście) tuż przed ostatnim nawiasem klamrowym. "prepare
" jest w rzeczywistości specjalną wbudowaną, istniejącą wcześniej funkcją w danej klasie [która nic nie robi], dlatego musisz ją "override
"Inną alternatywą jest skorzystanie z centrum powiadomień (NSNotificationCenter) i powiadomień pocztowych. To jest bardzo luźne powiązanie. Nadawca powiadomienia nie musi wiedzieć ani dbać o to, kto słucha. Po prostu publikuje powiadomienie i zapomina o tym.
Powiadomienia są dobre przy przekazywaniu wiadomości typu jeden do wielu, ponieważ może istnieć dowolna liczba obserwatorów nasłuchujących danej wiadomości.
źródło
Zamiast tworzyć singelton administratora danych, sugerowałbym utworzenie instancji kontrolera danych i przekazanie jej. Aby obsługiwać wstrzykiwanie zależności, najpierw utworzyłbym
DataController
protokół:protocol DataController { var someInt : Int {get set} var someString : String {get set} }
Następnie utworzyłbym
SpecificDataController
klasę (lub jakakolwiek inna nazwa byłaby obecnie odpowiednia):class SpecificDataController : DataController { var someInt : Int = 5 var someString : String = "Hello data" }
ViewController
Klasa powinna wtedy mieć pole do przechowywaniadataController
. Zauważ, że typemdataController
jest protokółDataController
. W ten sposób można łatwo zmienić implementacje kontrolerów danych:class ViewController : UIViewController { var dataController : DataController? ... }
W
AppDelegate
możemy ustawić viewController'sdataController
:func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { if let viewController = self.window?.rootViewController as? ViewController { viewController.dataController = SpecificDataController() } return true }
Gdy przejdziemy do innego viewController, możemy przekazać
dataController
dalej:override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { ... }
Teraz, gdy chcemy zmienić administratora danych do innego zadania, możemy to zrobić w ramach
AppDelegate
i nie musimy zmieniać żadnego innego kodu, który korzysta z administratora danych.Jest to oczywiście przesada, jeśli po prostu chcemy przekazać jedną wartość. W takim przypadku najlepiej jest skorzystać z odpowiedzi nhgrif.
Dzięki takiemu podejściu możemy oddzielić widok od części logicznej.
źródło
Jak @nhgrif wskazał w swojej doskonałej odpowiedzi, istnieje wiele różnych sposobów, na jakie VC (kontrolery widoku) i inne obiekty mogą się ze sobą komunikować.
Singleton danych, który nakreśliłem w mojej pierwszej odpowiedzi, dotyczy bardziej udostępniania i zapisywania stanu globalnego niż bezpośredniej komunikacji.
Odpowiedź nhrif umożliwia wysyłanie informacji bezpośrednio ze źródła do docelowego VC. Jak wspomniałem w odpowiedzi, możliwe jest również wysyłanie wiadomości z powrotem z miejsca docelowego do źródła.
W rzeczywistości można skonfigurować aktywny kanał jednokierunkowy lub dwukierunkowy między różnymi kontrolerami widoku. Jeśli kontrolery widoku są połączone za pomocą fragmentu scenorysu, czas na skonfigurowanie łączy jest w metodzie Przygotuj dla Segue.
Mam przykładowy projekt na Github, który używa nadrzędnego kontrolera widoku do hostowania 2 różnych widoków tabeli jako dzieci. Kontrolery widoku podrzędnego są połączone za pomocą segmentów osadzania, a kontroler widoku nadrzędnego łączy dwukierunkowe łącza z każdym kontrolerem widoku w metodzie PrzygotowanieForSegue.
Możesz znaleźć ten projekt na github (link). Napisałem go jednak w Objective-C i nie przekonwertowałem go na Swift, więc jeśli nie czujesz się komfortowo w Objective-C, może to być trochę trudne do zrozumienia
źródło
SWIFT 3:
Jeśli masz scenorys ze zidentyfikowanymi segmentami, użyj:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
Chociaż jeśli robisz wszystko programowo, w tym nawigację między różnymi UIViewControllers, użyj metody:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
Uwaga: aby użyć drugiego sposobu, w jaki musisz utworzyć UINavigationController, wciskasz UIViewControllers na delegata i musi on być zgodny z protokołem UINavigationControllerDelegate:
class MyNavigationController: UINavigationController, UINavigationControllerDelegate { override func viewDidLoad() { self.delegate = self } func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { // do what ever you need before going to the next UIViewController or back //this method will be always called when you are pushing or popping the ViewController } }
źródło
To zależy, kiedy chcesz uzyskać dane.
Jeśli chcesz uzyskać dane w dowolnym momencie, możesz użyć wzorca pojedynczego. Klasa wzorca jest aktywna w czasie wykonywania aplikacji. Oto przykład wzorca singleton.
class AppSession: NSObject { static let shared = SessionManager() var username = "Duncan" } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print(AppSession.shared.username) } }
Jeśli chcesz uzyskać dane po dowolnej akcji, możesz skorzystać z NotificationCenter.
extension Notification.Name { static let loggedOut = Notification.Name("loggedOut") } @IBAction func logoutAction(_ sender: Any) { NotificationCenter.default.post(name: .loggedOut, object: nil) } NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in print("User logged out") }
źródło