Come condividi i dati tra i controller di visualizzazione e altri oggetti in Swift?


88

Supponiamo che io disponga di più controller di visualizzazione nella mia app Swift e voglio essere in grado di passare i dati tra di loro. Se sono diversi livelli più in basso in uno stack di controller di visualizzazione, come faccio a passare i dati a un altro controller di visualizzazione? O tra le schede in un controller di visualizzazione della barra delle schede?

(Nota, questa domanda è una "suoneria".) Mi viene chiesto così tanto che ho deciso di scrivere un tutorial sull'argomento. Vedi la mia risposta di seguito.


1
Prova a cercare su
Google

4
L'ho pubblicato in modo da poter fornire una soluzione alle 10.000 istanze di questa domanda che compaiono ogni giorno qui su SO. Vedi la mia auto-risposta. :)
Duncan C

Scusa se ho reagito troppo velocemente :) bello essere in grado di collegarmi a questo :)
milo526

2
Nessun problema. Pensavi fossi il numero 10.001, vero? <grin>
Duncan C

4
@ DuncanC Non mi piace la tua risposta. :( Va bene, non è una risposta a tutti gli scenari ... insomuchas, funzionerà per ogni scenario, ma non è nemmeno l' approccio giusto per quasi tutti gli scenari. Nonostante questo, ora ce l'abbiamo in testa che contrassegnare una domanda sull'argomento come un duplicato di questa è una buona idea? Per favore, non farlo.
nhgrif

Risposte:


91

La tua domanda è molto ampia. Suggerire che esista una semplice soluzione universale per ogni scenario è un po 'ingenuo. Quindi, esaminiamo alcuni di questi scenari.


Lo scenario più comune richiesto su Stack Overflow nella mia esperienza è il semplice passaggio di informazioni da un controller di visualizzazione a quello successivo.

Se stiamo usando lo storyboard, il nostro primo controller di visualizzazione può eseguire l'override prepareForSegue, che è esattamente ciò per cui è lì. Un UIStoryboardSegueoggetto viene passato quando viene chiamato questo metodo e contiene un riferimento al nostro controller di visualizzazione di destinazione. Qui possiamo impostare i valori che vogliamo trasmettere.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "MySegueID" {
        if let destination = segue.destination as? SecondController {
            destination.myInformation = self.myInformation
        }
    }
}

In alternativa, se non stiamo usando gli storyboard, stiamo caricando il nostro controller di visualizzazione da un pennino. Il nostro codice è quindi leggermente più semplice.

func showNextController() {
    let destination = SecondController(nibName: "SecondController", bundle: nil)
    destination.myInformation = self.myInformation
    show(destination, sender: self)
}

In entrambi i casi, myInformation è una proprietà su ciascun controller di visualizzazione che contiene tutti i dati che devono essere passati da un controller di visualizzazione a quello successivo. Ovviamente non devono avere lo stesso nome su ogni controller.


Potremmo anche voler condividere le informazioni tra le schede in un file UITabBarController .

In questo caso, in realtà è potenzialmente ancora più semplice.

Per prima cosa, creiamo una sottoclasse di UITabBarControllere assegniamo le proprietà per qualsiasi informazione che vogliamo condividere tra le varie schede:

class MyCustomTabController: UITabBarController {
    var myInformation: [String: AnyObject]?
}

Ora, se stiamo creando la nostra app dallo storyboard, cambiamo semplicemente la classe del controller della barra delle schede da quella predefinita UITabBarControllera MyCustomTabController. Se non stiamo utilizzando uno storyboard, creiamo semplicemente un'istanza di questa classe personalizzata anziché quella predefinitaUITabBarController classe e aggiungiamo il nostro controller di visualizzazione a questa.

Ora, tutti i nostri controller di visualizzazione all'interno del controller della barra delle schede possono accedere a questa proprietà come tale:

if let tbc = self.tabBarController as? MyCustomTabController {
    // do something with tbc.myInformation
}

E creando sottoclassi UINavigationControllerallo stesso modo, possiamo adottare lo stesso approccio per condividere i dati su un intero stack di navigazione:

if let nc = self.navigationController as? MyCustomNavController {
    // do something with nc.myInformation
}

Ci sono molti altri scenari. In nessun modo questa risposta li copre tutti.


1
Aggiungerei anche che a volte si desidera che un canale invii le informazioni dal controller di visualizzazione di destinazione al controller di visualizzazione di origine. Un modo comune per gestire questa situazione consiste nell'aggiungere una proprietà delegato alla destinazione, quindi nel prepareForSegue del controller della visualizzazione di origine impostare la proprietà delegate del controller della visualizzazione di destinazione su self. (e definire un protocollo che definisce i messaggi che il VC di destinazione utilizza per inviare messaggi al VC di origine)
Duncan C

1
nhgrif, sono d'accordo. Il consiglio ai nuovi sviluppatori dovrebbe essere che se hai bisogno di passare dati tra le scene sullo storyboard, usa prepareForSegue. Peccato che questa semplice osservazione si perda tra le altre risposte e digressioni qui.
Rob il

2
@ Rob Yup. I singleton e le notifiche dovrebbero essere le ultime scelte. Dovremmo preferire prepareForSegueo altri trasferimenti diretti di informazioni in quasi tutti gli scenari e poi semplicemente essere d'accordo con i novizi quando si presentano con lo scenario per il quale queste situazioni non funzionano e quindi dobbiamo insegnare loro questi approcci più globali.
nhgrif

1
Dipende. Ma sono molto, molto preoccupato per l'utilizzo del delegato dell'app come discarica per il codice che non sappiamo dove altro mettere. Qui sta il percorso verso la follia.
nhgrif

2
@nhgrif. grazie per la tua risposta. cosa succede se tuttavia si desidera che i dati vengano passati tra diciamo 4 o 5 viewcontroller. Se ho detto 4-5 viewcontroller che gestiscono il login e la password del client ecc. e voglio passare l'e-mail dell'utente tra questi viewcontroller, esiste un modo più conveniente per farlo che dichiarare la var in ogni viewcontroller e poi passarla all'interno di prepareforsegue. c'è un modo in cui posso dichiarare una volta e ogni viewcontroller può accedervi, ma in un modo che è anche una buona pratica di codifica?
lozflan

45

Questa domanda viene fuori tutto il tempo.

Un suggerimento è creare un contenitore di dati singleton: un oggetto che viene creato una sola volta nella vita della tua applicazione e persiste per la vita della tua app.

Questo approccio è adatto per una situazione in cui si dispone di dati di app globali che devono essere disponibili / modificabili in diverse classi nella propria app.

Altri approcci come l'impostazione di collegamenti unidirezionali o bidirezionali tra i controller di visualizzazione sono più adatti alle situazioni in cui si passano informazioni / messaggi direttamente tra i controller di visualizzazione.

(Vedi la risposta di nhgrif, di seguito, per altre alternative.)

Con un contenitore di dati singleton, aggiungi una proprietà alla tua classe che memorizza un riferimento al tuo singleton e quindi usa quella proprietà ogni volta che hai bisogno di accedere.

Puoi configurare il tuo singleton in modo che salvi i suoi contenuti su disco in modo che lo stato della tua app persista tra i lanci.

Ho creato un progetto demo su GitHub dimostrando come puoi farlo. Ecco il link:

Progetto SwiftDataContainerSingleton su GitHub Ecco il README di quel progetto:

SwiftDataContainerSingleton

Una dimostrazione dell'utilizzo di un contenitore di dati singleton per salvare lo stato dell'applicazione e condividerlo tra gli oggetti.

Il DataContainerSingleton classe è il singleton effettivo.

Utilizza una costante statica sharedDataContainer per salvare un riferimento al singleton.

Per accedere al singleton, usa la sintassi

DataContainerSingleton.sharedDataContainer

Il progetto di esempio definisce 3 proprietà nel contenitore di dati:

  var someString: String?
  var someOtherString: String?
  var someInt: Int?

Per caricare la someIntproprietà dal contenitore di dati, dovresti utilizzare un codice come questo:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Per salvare un valore in someInt, dovresti usare la sintassi:

DataContainerSingleton.sharedDataContainer.someInt = 3

Il initmetodo di DataContainerSingleton aggiunge un osservatore per UIApplicationDidEnterBackgroundNotification. Quel codice ha questo aspetto:

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

Nel codice dell'osservatore salva le proprietà del contenitore di dati in NSUserDefaults. Puoi anche usareNSCoding Core Data o vari altri metodi per salvare i dati di stato.

Il initmetodo di DataContainerSingleton tenta anche di caricare i valori salvati per le sue proprietà.

Quella parte del metodo init ha questo aspetto:

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

Le chiavi per il caricamento e il salvataggio dei valori in NSUserDefaults sono archiviate come costanti stringa che fanno parte di una struttura DefaultsKeys, definita in questo modo:

struct DefaultsKeys
{
  static let someString  = "someString"
  static let someOtherString  = "someOtherString"
  static let someInt  = "someInt"
}

Fai riferimento a una di queste costanti in questo modo:

DefaultsKeys.someInt

Utilizzando il contenitore di dati singleton:

Questa applicazione di esempio utilizza trival il singleton del contenitore di dati.

Sono disponibili due controller di visualizzazione. La prima è una sottoclasse personalizzata di UIViewController ViewControllere la seconda è una sottoclasse personalizzata di UIViewController SecondVC.

Entrambi i controller di visualizzazione hanno un campo di testo su di essi ed entrambi caricano un valore dalla proprietà del contenitore di dati singlelton someIntnel campo di testo nel loroviewWillAppear metodo, ed entrambi salvano il valore corrente dal campo di testo nel "someInt" del contenitore di dati.

Il codice per caricare il valore nel campo di testo è nel viewWillAppear:metodo:

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

Il codice per salvare il valore modificato dall'utente nel contenitore dati si trova nei textFieldShouldEndEditingmetodi dei controller di visualizzazione :

 func textFieldShouldEndEditing(textField: UITextField) -> Bool
 {
   //Save the changed value back to our data container singleton
   DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
   return true
 }

È necessario caricare i valori nell'interfaccia utente in viewWillAppear anziché in viewDidLoad in modo che l'interfaccia utente si aggiorni ogni volta che viene visualizzato il controller di visualizzazione.


8
Non voglio votare per difetto perché penso che sia eccellente che tu abbia investito il tempo per creare la domanda e la risposta come risorsa. Grazie. Nonostante ciò, penso che rendiamo un grande disservizio ai nuovi sviluppatori nel sostenere i singleton per gli oggetti modello. Non sono nel campo "i singleton sono malvagi" (anche se i noob dovrebbero cercare su Google quella frase per apprezzare meglio i problemi), ma penso che i dati del modello siano un uso discutibile / discutibile dei singleton.
Rob il

mi piacerebbe vedere un fantastico articolo come tuo sui link a due vie
Cmag

@ Duncan C Ciao Duncan, sto creando oggetti statici in ogni modello, quindi ottengo i dati da qualsiasi punto sia giusto o devo seguire il tuo percorso perché sembra molto giusto.
Virendra Singh Rathore

@VirendraSinghRathore, le variabili statiche globali sono il peggior modo possibile per condividere i dati attraverso l'app. Associano strettamente le parti della tua app e introducono serie interdipendenze. È l'esatto opposto di "molto giusto".
Duncan C

@DuncanC - questo pattern funzionerebbe per un oggetto CurrentUser, fondamentalmente un singolo utente che ha effettuato l'accesso alla tua app? thx
timpone

9

Swift 4

Ci sono così tanti approcci per la trasmissione rapida dei dati. Qui sto aggiungendo alcuni dei migliori approcci di esso.

1) Utilizzo di StoryBoard Segue

Le sequenze dello storyboard sono molto utili per il passaggio dei dati tra i controller di visualizzazione di origine e di destinazione e viceversa.

// 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) Utilizzo di metodi delegati

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
        }

    }

Per i googler che sono totalmente e completamente persi su dove mettere StackOverflow rispondono "Snippet di codice Swift come me, poiché sembra che dovresti sempre sapere dove deducono il codice: ho usato l'opzione 1) per inviare da ViewControllerAa ViewControllerB. Ho appena bloccato lo snippet di codice nella parte inferiore del mio ViewControllerA.swift(dove si ViewControllerA.swifttrova effettivamente il nome del tuo file, ovviamente) subito prima dell'ultima parentesi graffa. " prepare" è in realtà una speciale funzione preesistente incorporata in una data Classe [che non fa nulla], motivo per cui devi " override" farlo
velkoon

8

Un'altra alternativa è utilizzare il centro notifiche (NSNotificationCenter) e pubblicare notifiche. Questo è un accoppiamento molto lento. Il mittente di una notifica non ha bisogno di sapere o preoccuparsi di chi sta ascoltando. Pubblica solo una notifica e se ne dimentica.

Le notifiche sono utili per il passaggio di messaggi uno-a-molti, poiché può esserci un numero arbitrario di osservatori in ascolto per un determinato messaggio.


2
Si noti che l'utilizzo del centro notifiche introduce un accoppiamento forse troppo lento. Può rendere molto difficile tracciare il flusso del programma, quindi dovrebbe essere usato con cautela.
Duncan C

2

Invece di creare un singolo controller di dati, suggerirei di creare un'istanza di controller di dati e passarla in giro. Per supportare l'inserimento delle dipendenze creerei prima un DataControllerprotocollo:

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

Quindi creerei una SpecificDataControllerclasse (o qualunque nome sarebbe attualmente appropriato):

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

La ViewControllerclasse dovrebbe quindi avere un campo per contenere il file dataController. Si noti che il tipo di dataControllerè il protocollo DataController. In questo modo è facile cambiare le implementazioni del titolare del trattamento:

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

In AppDelegatepossiamo impostare il viewController dataController:

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    if let viewController = self.window?.rootViewController as? ViewController {
        viewController.dataController =  SpecificDataController()
    }   
    return true
}

Quando ci spostiamo su un viewController diverso possiamo trasmettere il messaggio dataControllerin:

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

Ora, quando desideriamo cambiare il responsabile del trattamento dei dati per un'attività diversa, possiamo farlo nel AppDelegate e non dobbiamo modificare nessun altro codice che utilizza il titolare del trattamento.

Questo è ovviamente eccessivo se vogliamo semplicemente passare un singolo valore. In questo caso è meglio andare con la risposta di nhgrif.

Con questo approccio possiamo separare la vista dalla parte logica.


1
Ciao, questo approccio è pulito, testabile e quello che uso la maggior parte del tempo in piccole applicazioni, ma in quelle più grandi, dove non tutti i VC (forse nemmeno il root VC) potrebbero aver bisogno della dipendenza (ad esempio DataController in questo caso) sembra uno spreco per ogni VC richiedere la dipendenza solo per trasmetterla. Inoltre, se si utilizzano diversi tipi di VC (ad esempio, UIVC normale rispetto a NavigationVC), è necessario creare una sottoclasse di tali tipi diversi solo per aggiungere quella variabile di dipendenza. Come ti avvicini a questo?
RobertoCuba

1

Come ha sottolineato @nhgrif nella sua eccellente risposta, ci sono molti modi diversi in cui i VC (controller di visualizzazione) e altri oggetti possono comunicare tra loro.

I dati singleton che ho delineato nella mia prima risposta riguardano in realtà più la condivisione e il salvataggio dello stato globale che la comunicazione diretta.

La risposta di nhrif ti consente di inviare informazioni direttamente dalla sorgente al VC di destinazione. Come ho detto in risposta, è anche possibile inviare messaggi di ritorno dalla destinazione alla fonte.

In effetti, è possibile impostare un canale attivo unidirezionale o bidirezionale tra diversi controller di visualizzazione. Se i controller di visualizzazione sono collegati tramite uno storyboard segue, il tempo per impostare i collegamenti è nel metodo prepareFor Segue.

Ho un progetto di esempio su Github che utilizza un controller di visualizzazione genitore per ospitare 2 diverse visualizzazioni di tabelle come bambini. I controller di visualizzazione figlio sono collegati tramite embed segues e il controller di visualizzazione padre collega collegamenti a 2 vie con ogni controller di visualizzazione nel metodo prepareForSegue.

Puoi trovare quel progetto su GitHub (link). L'ho scritto in Objective-C, tuttavia, e non l'ho convertito in Swift, quindi se non ti senti a tuo agio in Objective-C potrebbe essere un po 'difficile da seguire


1

SWIFT 3:

Se hai uno storyboard con segues identificati usa:

func prepare(for segue: UIStoryboardSegue, sender: Any?)

Sebbene se esegui tutto a livello di programmazione, inclusa la navigazione tra diversi UIViewControllers, utilizza il metodo:

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)

Nota: per utilizzare il secondo modo è necessario creare il tuo UINavigationController, stai spingendo UIViewControllers su, un delegato e deve essere conforme al protocollo 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

    }
}

mai fare self.delegate = self
malhal

1

Dipende da quando vuoi ottenere i dati.

Se vuoi ottenere dati ogni volta che vuoi, puoi usare un pattern singleton. La classe pattern è attiva durante il runtime dell'app. Ecco un esempio del pattern singleton.

class AppSession: NSObject {

    static let shared = SessionManager()
    var username = "Duncan"
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(AppSession.shared.username)
    }
}

Se desideri ottenere dati dopo qualsiasi azione, puoi utilizzare 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")
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.