Comment partagez-vous des données entre les contrôleurs de vue et d'autres objets dans Swift?

88

Disons que j'ai plusieurs contrôleurs de vue dans mon application Swift et que je souhaite pouvoir transmettre des données entre eux. Si je suis plusieurs niveaux plus bas dans une pile de contrôleurs de vue, comment puis-je transmettre des données à un autre contrôleur de vue? Ou entre les onglets dans un contrôleur d'affichage de la barre d'onglets?

(Remarque, cette question est une "sonnerie".) On en pose tellement que j'ai décidé d'écrire un tutoriel sur le sujet. Voir ma réponse ci-dessous.

Duncan C
la source
1
Essayez googler pour les délégués
milo526
4
J'ai posté ceci pour pouvoir fournir une solution aux 10 000 instances de cette question qui apparaissent chaque jour ici sur SO. Voir ma réponse personnelle. :)
Duncan C
Désolé, j'ai été trop rapide avec la réaction :) bon de pouvoir
créer un
2
Pas de soucis. Vous pensiez que j'étais # 10 001, n'est-ce pas? <grin>
Duncan C
4
@DuncanC Je n'aime pas votre réponse. :( Ce n'est pas grave, c'est une réponse fourre-tout à chaque scénario ... insomuchas, cela fonctionnera pour chaque scénario, mais ce n'est pas non plus la bonne approche pour presque tous les scénarios. Malgré cela, nous l'avons maintenant dans notre tête que marquer une question sur le sujet comme un double de celle-ci est une bonne idée? S'il vous plaît, ne le faites pas.
nhgrif

Réponses:

91

Votre question est très large. Suggérer qu'il existe une solution fourre-tout simple à chaque scénario est un peu naïf. Alors, passons en revue certains de ces scénarios.


Le scénario le plus fréquemment posé sur Stack Overflow dans mon expérience est la simple transmission d'informations d'un contrôleur de vue à l'autre.

Si nous utilisons un storyboard, notre premier contrôleur de vue peut remplacer prepareForSegue, ce qui est exactement ce à quoi il sert. Un UIStoryboardSegueobjet est passé lorsque cette méthode est appelée, et il contient une référence à notre contrôleur de vue de destination. Ici, nous pouvons définir les valeurs que nous voulons transmettre.

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

Sinon, si nous n'utilisons pas de storyboards, nous chargeons notre contrôleur de vue à partir d'une pointe. Notre code est alors légèrement plus simple.

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

Dans les deux cas, myInformationest une propriété sur chaque contrôleur de vue contenant toutes les données qui doivent être transmises d'un contrôleur de vue au suivant. Ils ne doivent évidemment pas avoir le même nom sur chaque contrôleur.


Nous pourrions également souhaiter partager des informations entre les onglets dans un fichier UITabBarController.

Dans ce cas, c'est potentiellement encore plus simple.

Tout d'abord, créons une sous-classe de UITabBarControlleret attribuons-lui des propriétés pour les informations que nous voulons partager entre les différents onglets:

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

Maintenant, si nous construisons notre application à partir du storyboard, nous changeons simplement la classe de notre contrôleur de barre d'onglets de la valeur par défaut UITabBarControllerà MyCustomTabController. Si nous n'utilisons pas de storyboard, nous instancions simplement une instance de cette classe personnalisée plutôt que la UITabBarControllerclasse par défaut et y ajoutons notre contrôleur de vue.

Maintenant, tous nos contrôleurs de vue dans le contrôleur de la barre d'onglets peuvent accéder à cette propriété en tant que telle:

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

Et en sous-classant UINavigationControllerde la même manière, nous pouvons adopter la même approche pour partager des données sur une pile de navigation entière:

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

Il existe plusieurs autres scénarios. En aucun cas, cette réponse ne les couvre tous.

nhgrif
la source
1
J'ajouterais également que parfois vous voulez qu'un canal renvoie des informations du contrôleur de vue de destination au contrôleur de vue source. Un moyen courant de gérer cette situation consiste à ajouter une propriété de délégué à la destination, puis dans prepareForSegue du contrôleur de vue source, définissez la propriété de délégué du contrôleur de vue de destination sur self. (et définissez un protocole qui définit les messages que le VC de destination utilise pour envoyer des messages au VC source)
Duncan C
1
nhgrif, je suis d'accord. Le conseil aux nouveaux développeurs devrait être que si vous avez besoin de transmettre des données entre les scènes du storyboard, utilisez prepareForSegue. C'est dommage que cette observation très simple soit perdue parmi les autres réponses et digressions ici.
Rob
2
@Rob Yup. Les singletons et les notifications devraient être les derniers choix. Nous devrions préférer prepareForSegueou d'autres transferts directs d'informations dans presque tous les scénarios et ensuite simplement être d'accord avec les novices lorsqu'ils se présentent avec le scénario pour lequel ces situations ne fonctionnent pas et nous devons ensuite leur enseigner ces approches plus globales.
nhgrif
1
Ça dépend. Mais je suis très, très préoccupé par l'utilisation du délégué d'application comme dépotoir pour le code que nous ne savons pas où mettre d'autre. Ici se trouve le chemin de la folie.
nhgrif
2
@nhgrif. merci pour votre réponse. que se passe-t-il si cependant vous voulez que les données soient transmises entre 4 ou 5 contrôleurs de vue. si ive a, disons 4-5 viewcontrollers gérant la connexion et le mot de passe du client, etc. y a-t-il un moyen de déclarer une fois et chaque viewcontroller peut y accéder, mais d'une manière qui est également une bonne pratique de codage?
lozflan
45

Cette question revient tout le temps.

Une suggestion consiste à créer un singleton de conteneur de données: un objet qui est créé une et une seule fois dans la vie de votre application et qui persiste pendant toute la durée de vie de votre application.

Cette approche est bien adaptée à une situation où vous avez des données d'application globales qui doivent être disponibles / modifiables dans différentes classes de votre application.

D'autres approches, telles que la mise en place de liens unidirectionnels ou bidirectionnels entre les contrôleurs de vue, sont mieux adaptées aux situations où vous transmettez des informations / messages directement entre les contrôleurs de vue.

(Voir la réponse de nhgrif, ci-dessous, pour d'autres alternatives.)

Avec un singleton de conteneur de données, vous ajoutez une propriété à votre classe qui stocke une référence à votre singleton, puis utilisez cette propriété chaque fois que vous en avez besoin.

Vous pouvez configurer votre singleton pour qu'il enregistre son contenu sur le disque afin que l'état de votre application persiste entre les lancements.

J'ai créé un projet de démonstration sur GitHub montrant comment vous pouvez le faire. Voici le lien:

Projet SwiftDataContainerSingleton sur GitHub Voici le README de ce projet:

SwiftDataContainerSingleton

Une démonstration de l'utilisation d'un singleton de conteneur de données pour enregistrer l'état de l'application et le partager entre les objets.

La DataContainerSingletonclasse est le singleton réel.

Il utilise une constante statique sharedDataContainerpour enregistrer une référence au singleton.

Pour accéder au singleton, utilisez la syntaxe

DataContainerSingleton.sharedDataContainer

L'exemple de projet définit 3 propriétés dans le conteneur de données:

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

Pour charger la someIntpropriété à partir du conteneur de données, vous utiliserez un code comme celui-ci:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Pour enregistrer une valeur dans someInt, vous utiliserez la syntaxe:

DataContainerSingleton.sharedDataContainer.someInt = 3

La initméthode de DataContainerSingleton ajoute un observateur pour le UIApplicationDidEnterBackgroundNotification. Ce code ressemble à ceci:

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

Dans le code de l'observateur, il enregistre les propriétés du conteneur de données dans NSUserDefaults. Vous pouvez également utiliser NSCoding, Core Data ou diverses autres méthodes pour enregistrer les données d'état.

La initméthode de DataContainerSingleton tente également de charger les valeurs enregistrées pour ses propriétés.

Cette partie de la méthode init ressemble à ceci:

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

Les clés de chargement et d'enregistrement des valeurs dans NSUserDefaults sont stockées sous forme de constantes de chaîne faisant partie d'une structure DefaultsKeys, définie comme ceci:

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

Vous référencez l'une de ces constantes comme ceci:

DefaultsKeys.someInt

Utilisation du singleton du conteneur de données:

Cet exemple d'application fait un usage trival du singleton du conteneur de données.

Il existe deux contrôleurs de vue. Le premier est une sous-classe personnalisée de UIViewController ViewControlleret le second est une sous-classe personnalisée de UIViewController SecondVC.

Les deux contrôleurs de vue ont un champ de texte sur eux, et tous deux chargent une valeur de la someIntpropriété du conteneur de données singlelton dans le champ de texte de leur viewWillAppearméthode, et tous deux enregistrent la valeur actuelle du champ de texte dans le `someInt 'du conteneur de données.

Le code pour charger la valeur dans le champ de texte est dans la viewWillAppear:méthode:

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

Le code pour enregistrer la valeur modifiée par l'utilisateur dans le conteneur de données se trouve dans les textFieldShouldEndEditingméthodes des contrôleurs de vue :

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

Vous devez charger des valeurs dans votre interface utilisateur dans viewWillAppear plutôt que viewDidLoad afin que votre interface utilisateur se mette à jour chaque fois que le contrôleur de vue est affiché.

Duncan C
la source
8
Je ne veux pas voter contre cela parce que je pense qu'il est excellent que vous ayez investi du temps pour créer la question et la réponse en tant que ressource. Je vous remercie. Malgré cela, je pense que nous rendons un très mauvais service aux nouveaux développeurs en préconisant des singletons pour les objets modèles. Je ne suis pas dans le camp "les singletons sont mauvais" (bien que les noobs devraient rechercher cette phrase sur Google pour mieux apprécier les problèmes), mais je pense que les données de modèle sont une utilisation discutable / discutable des singletons.
Rob
aimerait voir un article génial comme le vôtre sur les liens bidirectionnels
Cmag
@Duncan C Bonjour Duncan Je crée des objets statiques dans chaque modèle, donc j'obtiens des données de n'importe où est la bonne approche ou je dois suivre votre chemin car cela semble très juste.
Virendra Singh Rathore
@VirendraSinghRathore, les variables statiques globales sont le pire moyen possible de partager des données sur l'application. Ils associent étroitement les parties de votre application et introduisent de sérieuses interdépendances. C'est exactement le contraire de «très juste».
Duncan C
@DuncanC - ce modèle fonctionnerait-il pour un objet CurrentUser - essentiellement un seul utilisateur connecté à votre application? thx
timpone
9

Swift 4

Il existe de nombreuses approches pour la transmission rapide des données. Ici, j'ajoute certaines des meilleures approches de celui-ci.

1) Utilisation de StoryBoard Segue

Les séquences de storyboard sont très utiles pour transmettre des données entre les contrôleurs de vue source et de destination et vice versa également.

// 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) Utilisation des méthodes de délégué

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
        }

    }
Équipe iOS
la source
Pour Googlers qui sont aussi totalement et complètement perdu à l' endroit où placer Swift les extraits de code de réponses StackOverflow que je suis, car il semble supposé que vous devez toujours savoir où ils en déduisent le code va: je Option 1) pour envoyer de ViewControllerAla ViewControllerB. Je viens de coller l'extrait de code au bas de mon ViewControllerA.swift(où ViewControllerA.swiftest en fait le nom de votre fichier, bien sûr) juste avant la dernière accolade. " prepare" est en fait une fonction préexistante intégrée spéciale dans une classe donnée [qui ne fait rien], c'est pourquoi vous devez " override" le faire
velkoon
8

Une autre alternative consiste à utiliser le centre de notification (NSNotificationCenter) et à publier des notifications. C'est un couplage très lâche. L'expéditeur d'une notification n'a pas besoin de savoir ou de se soucier de qui écoute. Il publie juste une notification et l'oublie.

Les notifications sont bonnes pour le passage de messages un à plusieurs, car il peut y avoir un nombre arbitraire d'observateurs à l'écoute d'un message donné.

Duncan C
la source
2
Notez que l'utilisation du centre de notification introduit un couplage peut-être trop lâche. Cela peut rendre très difficile le suivi du déroulement de votre programme, il doit donc être utilisé avec précaution.
Duncan C
2

Au lieu de créer un contrôleur de données singelton, je suggérerais de créer une instance de contrôleur de données et de la transmettre. Pour prendre en charge l'injection de dépendances, je créerais d'abord un DataControllerprotocole:

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

Ensuite, je créerais une SpecificDataControllerclasse (ou n'importe quel nom serait actuellement approprié):

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

La ViewControllerclasse devrait alors avoir un champ pour contenir le dataController. Notez que le type de dataControllerest le protocole DataController. De cette façon, il est facile de changer d'implémentation de contrôleur de données:

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

Dans AppDelegatenous pouvons définir le viewController dataController:

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

Lorsque nous passons à un autre viewController, nous pouvons transmettre le dataController:

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

Désormais, lorsque nous souhaitons désactiver le contrôleur de données pour une tâche différente, nous pouvons le faire dans le AppDelegateet nous n'avons pas à modifier un autre code utilisant le contrôleur de données.

C'est bien sûr exagéré si nous voulons simplement transmettre une seule valeur. Dans ce cas, il vaut mieux suivre la réponse de nhgrif.

Avec cette approche, nous pouvons séparer la vue de la partie logique.

Kristiina
la source
1
Bonjour, cette approche est propre, testable et ce que j'utilise la plupart du temps dans les petites applications, mais dans les plus grandes, où tous les VC (peut-être même pas le VC racine) n'ont peut-être pas besoin de la dépendance (par exemple, DataController dans ce cas). semble inutile pour chaque VC d'exiger la dépendance juste pour la transmettre. De plus, si vous utilisez différents types de VC (par exemple, UIVC standard ou NavigationVC), vous devez sous-classer ces différents types simplement pour ajouter cette variable de dépendance. Comment abordez-vous cela?
RobertoCuba
1

Comme @nhgrif l'a souligné dans son excellente réponse, les VC (contrôleurs de vue) et d'autres objets peuvent communiquer entre eux de nombreuses manières différentes.

Le singleton de données que j'ai décrit dans ma première réponse concerne vraiment plus le partage et la sauvegarde de l'état global que la communication directe.

La réponse de nhrif vous permet d'envoyer des informations directement de la source au VC de destination. Comme je l'ai mentionné en réponse, il est également possible de renvoyer des messages de la destination à la source.

En fait, vous pouvez configurer un canal actif unidirectionnel ou bidirectionnel entre différents contrôleurs de vue. Si les contrôleurs de vue sont liés via une séquence de storyboard, le temps de configurer les liens est dans la méthode prepareFor Segue.

J'ai un exemple de projet sur Github qui utilise un contrôleur de vue parent pour héberger 2 vues de table différentes en tant qu'enfants. Les contrôleurs de vue enfants sont liés à l'aide de segues incorporés, et le contrôleur de vue parent relie des liens bidirectionnels avec chaque contrôleur de vue dans la méthode prepareForSegue.

Vous pouvez trouver ce projet sur github (lien). Je l'ai cependant écrit en Objective-C et je ne l'ai pas converti en Swift, donc si vous n'êtes pas à l'aise avec Objective-C, cela peut être un peu difficile à suivre

Duncan C
la source
1

SWIFT 3:

Si vous avez un storyboard avec des segues identifiées, utilisez:

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

Bien que si vous faites tout par programme, y compris la navigation entre différents UIViewControllers, utilisez la méthode:

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

Remarque: pour utiliser la deuxième façon dont vous devez créer votre UINavigationController, vous poussez UIViewControllers sur, un délégué et il doit se conformer au protocole 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

    }
}
Maxime
la source
never do self.delegate = self
malhal
1

Cela dépend du moment où vous souhaitez obtenir des données.

Si vous souhaitez obtenir des données quand vous le souhaitez, vous pouvez utiliser un modèle singleton. La classe de modèle est active pendant l'exécution de l'application. Voici un exemple du modèle singleton.

class AppSession: NSObject {

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

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

        print(AppSession.shared.username)
    }
}

Si vous souhaitez obtenir des données après toute action, vous pouvez utiliser 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 Demirci
la source