Jak udostępniasz dane między kontrolerami widoku i innymi obiektami w Swift?

88

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.

Duncan C
źródło
1
Spróbuj
googlować
4
Opublikowałem to, aby móc znaleźć rozwiązanie dla 10000 przypadków tego pytania, które pojawiają się każdego dnia w SO. Zobacz moją odpowiedź. :)
Duncan C
Przepraszam, że zareagowałem zbyt szybko :) Dobrze, że mogę to linkować :)
milo526
2
Bez obaw. Myślałeś, że jestem # 10 001, prawda? <grin>
Duncan C
4
@DuncanC Nie podoba mi się twoja odpowiedź. :( To jest w porządku - nie jest odpowiedzią na wszystkie scenariusze ... w zasadzie to będzie działać w każdym scenariuszu, ale nie jest to również właściwe podejście do prawie każdego scenariusza. Mimo to mamy to już w naszej głowie że oznaczanie dowolnego pytania na ten temat jako duplikatu tego jest dobrym pomysłem? Proszę, nie rób tego.
nhgrif

Odpowiedzi:

91

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. UIStoryboardSegueObiekt 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 myInformationjest 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ę UITabBarControlleri 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 UITabBarControllerna MyCustomTabController. Jeśli nie używamy scenorysu, po prostu tworzymy wystąpienie tej niestandardowej klasy zamiast UITabBarControllerklasy 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 UINavigationControllerw 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.

nhgrif
źródło
1
Dodałbym również, że czasami chcesz, aby kanał wysyłał informacje z powrotem z kontrolera widoku docelowego do kontrolera widoku źródłowego. Typowym sposobem radzenia sobie z tą sytuacją jest dodanie właściwości delegata do miejsca docelowego, a następnie ustawienie właściwości delegata kontrolera widoku docelowego na self w kontrolerze widoku źródła readyForSegue. (i zdefiniuj protokół, który definiuje komunikaty, których docelowy VC używa do wysyłania komunikatów do źródłowego VC)
Duncan C
1
nhgrif, zgadzam się. Rada dla nowych programistów powinna być taka, że ​​jeśli chcesz przekazywać dane między scenami w scenorysie, użyj prepareForSegue. Szkoda, że ​​ta bardzo prosta obserwacja ginie wśród innych odpowiedzi i dygresji.
Rob
2
@Rob Yup. Singletony i powiadomienia powinny być ostatnimi opcjami. Powinniśmy preferować prepareForSeguelub 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.
nhgrif
1
To zależy. Ale jestem bardzo, bardzo zaniepokojony użyciem delegata aplikacji jako miejsca zrzutu kodu, którego nie wiemy, gdzie indziej umieścić. Oto ścieżka do szaleństwa.
nhgrif
2
@nhgrif. dzięki za odpowiedź. co jednak, jeśli chcesz, aby dane były przesyłane między, powiedzmy, 4 lub 5 kontrolerami widoku. jeśli mam powiedzmy 4-5 kontrolerów widoku zarządzających loginem i hasłem klienta itp. i chcę przekazać e-mail użytkownika między tymi kontrolerami widoku, czy istnieje wygodniejszy sposób na zrobienie tego niż zadeklarowanie zmiennej w każdym kontrolerze widoku, a następnie przekazanie jej w ramach przygotowania do oceny. czy istnieje sposób, w jaki mogę raz zadeklarować, a każdy kontroler widoku może uzyskać do niego dostęp, ale w sposób, który jest również dobrą praktyką kodowania?
lozflan
45

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.

DataContainerSingletonKlasa jest rzeczywista Singleton.

Używa stałej statycznej, sharedDataContaineraby 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ć someIntwł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 initdodaje obserwatora dla UIApplicationDidEnterBackgroundNotification. 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 initpró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 UIViewController SecondVC.

Oba kontrolery widoku mają na sobie pole tekstowe i oba ładują wartość z właściwości singlelton kontenera danych someIntdo pola tekstowego w swojej viewWillAppearmetodzie 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 textFieldShouldEndEditingmetodach 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.

Duncan C
źródło
8
Nie chcę głosować w dół, ponieważ uważam, że to wspaniale, że poświęciłeś czas na stworzenie pytania i odpowiedzi jako zasobu. Dziękuję Ci. Mimo to myślę, że wyrządzamy wielką krzywdę nowym programistom, opowiadając się za singletonami dla obiektów modeli. Nie jestem w obozie „singletony są złe” (chociaż noobowie powinni wygooglować to wyrażenie, aby lepiej docenić problemy), ale myślę, że dane modelowe są wątpliwym / dyskusyjnym zastosowaniem singletonów.
Rob
chciałbym zobaczyć niesamowity
opis
@Duncan C Witaj Duncan Tworzę statyczny obiekt w każdym modelu, więc otrzymuję dane z dowolnego miejsca, w którym jest to właściwe podejście, lub muszę podążać twoją ścieżką, ponieważ wydaje się to bardzo właściwe.
Virendra Singh Rathore,
@VirendraSinghRathore, Globalne zmienne statyczne to najgorszy możliwy sposób udostępniania danych w całej aplikacji. Ściśle łączą ze sobą części aplikacji i wprowadzają poważne współzależności. Jest to dokładne przeciwieństwo słowa „bardzo dobrze”.
Duncan C
@DuncanC - czy ten wzorzec będzie działał dla obiektu CurrentUser - w zasadzie pojedynczego użytkownika, który jest zalogowany do Twojej aplikacji? thx
timpone
9

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
        }

    }
Zespół iOS
źródło
Dla pracowników Google, którzy są tak samo kompletnie zagubieni, jak umieścić fragmenty kodu Swift odpowiedzi StackOverflow tak jak ja, ponieważ wydaje się, że zawsze powinieneś wiedzieć, skąd wywnioskowali kod: użyłem opcji 1), aby wysłać z ViewControllerAdo ViewControllerB. Po prostu umieściłem fragment kodu na dole mojego ViewControllerA.swift(gdzie ViewControllerA.swiftwł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"
velkoon
8

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.

Duncan C
źródło
2
Zwróć uwagę, że korzystanie z centrum powiadomień wprowadza powiązanie, które może być zbyt luźne. Może to bardzo utrudnić śledzenie przebiegu programu, dlatego należy go używać ostrożnie.
Duncan C
2

Zamiast tworzyć singelton administratora danych, sugerowałbym utworzenie instancji kontrolera danych i przekazanie jej. Aby obsługiwać wstrzykiwanie zależności, najpierw utworzyłbym DataControllerprotokół:

protocol DataController {
    var someInt : Int {get set} 
    var someString : String {get set}
}

Następnie utworzyłbym SpecificDataControllerklasę (lub jakakolwiek inna nazwa byłaby obecnie odpowiednia):

class SpecificDataController : DataController {
   var someInt : Int = 5
   var someString : String = "Hello data" 
}

ViewControllerKlasa powinna wtedy mieć pole do przechowywania dataController. Zauważ, że typem dataControllerjest protokół DataController. W ten sposób można łatwo zmienić implementacje kontrolerów danych:

class ViewController : UIViewController {
   var dataController : DataController?
   ...
}

W AppDelegatemożemy ustawić viewController's dataController:

 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ć dataControllerdalej:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    ...   
}

Teraz, gdy chcemy zmienić administratora danych do innego zadania, możemy to zrobić w ramach AppDelegatei 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.

Kristiina
źródło
1
Witam, to podejście jest czyste, testowalne i czego używam najczęściej w małych aplikacjach, ale w większych, gdzie nie każdy VC (może nawet nie główny VC) może potrzebować zależności (np. DataController w tym przypadku) to wydaje się marnotrawstwem dla każdego VC wymaganie zależności tylko po to, by ją przekazać. Ponadto, jeśli używasz różnych typów VC (np. Zwykłe UIVC w porównaniu z NavigationVC), musisz podklasować te różne typy tylko po to, aby dodać tę zmienną zależności. Jak podchodzisz do tego?
RobertoCuba,
1

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

Duncan C
źródło
1

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

    }
}
Maksyma
źródło
never do self.delegate = self
malhal
1

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")
}
Yusuf
źródło