¿Cómo se comparten datos entre controladores de vista y otros objetos en Swift?


88

Digamos que tengo varios controladores de vista en mi aplicación Swift y quiero poder pasar datos entre ellos. Si estoy varios niveles hacia abajo en una pila de controladores de vista, ¿cómo paso los datos a otro controlador de vista? ¿O entre pestañas en un controlador de vista de barra de pestañas?

(Tenga en cuenta que esta pregunta es un "timbre".) Se pregunta tanto que decidí escribir un tutorial sobre el tema. Vea mi respuesta a continuación.


1
Intente buscar delegados en Google
milo526

4
Publiqué esto para poder brindar una solución a las 10,000 instancias de esta pregunta que aparecen todos los días aquí en SO. Mira mi respuesta personal. :)
Duncan C

Lo siento, fui demasiado rápido al reaccionar :) Es bueno poder vincular a esto :)
milo526

2
Sin preocupaciones. Pensaste que yo era el número 10.001, ¿no? <sonrisa>
Duncan C

4
@DuncanC No me gusta tu respuesta. :( Está bien-no es una respuesta general para todos los escenarios ... insomuchas, funcionará para todos los escenarios, pero tampoco es el enfoque correcto para casi todos los escenarios. A pesar de esto, ahora lo tenemos en la cabeza que marcar cualquier pregunta sobre el tema como un duplicado de esta es una buena idea? Por favor, no lo haga.
nhgrif

Respuestas:


91

Tu pregunta es muy amplia. Sugerir que hay una solución simple para todos los escenarios es un poco ingenuo. Entonces, repasemos algunos de estos escenarios.


El escenario más común sobre el que se pregunta en Stack Overflow en mi experiencia es el simple paso de información de un controlador de vista al siguiente.

Si estamos usando un guión gráfico, nuestro primer controlador de vista puede anular prepareForSegue, que es exactamente para lo que está ahí. Se UIStoryboardSeguepasa un objeto cuando se llama a este método y contiene una referencia a nuestro controlador de vista de destino. Aquí, podemos establecer los valores que queremos pasar.

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

Alternativamente, si no estamos usando guiones gráficos, entonces cargamos nuestro controlador de vista desde una plumilla. Nuestro código es un poco más simple entonces.

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

En ambos casos, myInformationhay una propiedad en cada controlador de vista que contiene los datos que se deben pasar de un controlador de vista al siguiente. Obviamente, no es necesario que tengan el mismo nombre en cada controlador.


También es posible que deseemos compartir información entre pestañas en un UITabBarController.

En este caso, es potencialmente incluso más simple.

Primero, UITabBarControllercreemos una subclase de y le asignamos propiedades para cualquier información que queramos compartir entre las distintas pestañas:

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

Ahora, si estamos creando nuestra aplicación desde el guión gráfico, simplemente cambiamos la clase del controlador de la barra de pestañas de la predeterminada UITabBarControllera MyCustomTabController. Si no estamos usando un guión gráfico, simplemente creamos una instancia de esta clase personalizada en lugar de la UITabBarControllerclase predeterminada y agregamos nuestro controlador de vista a esto.

Ahora, todos nuestros controladores de vista dentro del controlador de la barra de pestañas pueden acceder a esta propiedad como tal:

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

Y al crear subclases UINavigationControllerde la misma manera, podemos adoptar el mismo enfoque para compartir datos en toda una pila de navegación:

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

Hay varios otros escenarios. De ninguna manera esta respuesta los cubre a todos.


1
También agregaría que a veces desea que un canal envíe información desde el controlador de vista de destino al controlador de vista de origen. Una forma común de manejar esa situación es agregar una propiedad delegada al destino, y luego en prepareForSegue del controlador de la vista de origen, establezca la propiedad delegada del controlador de la vista de destino en self. (y definir un protocolo que defina los mensajes que utiliza el VC de destino para enviar mensajes al VC de origen)
Duncan C

1
nhgrif, estoy de acuerdo. El consejo para los nuevos desarrolladores debería ser que, si necesita pasar datos entre escenas en el guión gráfico, utilice prepareForSegue. Es una lástima que esta simple observación se pierda entre las otras respuestas y digresiones aquí.
Rob

2
@Rob Sí. Los singletons y las notificaciones deben ser las últimas opciones. Deberíamos preferir prepareForSegueu otras transferencias directas de información en casi todos los escenarios y luego simplemente estar de acuerdo con los novatos cuando se presentan con el escenario para el que estas situaciones no funcionan y luego tenemos que enseñarles sobre estos enfoques más globales.
nhgrif

1
Depende. Pero estoy muy, muy preocupado por usar el delegado de la aplicación como nuestro vertedero para el código que no sabemos dónde más poner. Aquí está el camino de la locura.
nhgrif

2
@nhgrif. gracias por su respuesta. ¿Qué sucede si, sin embargo, desea que los datos se pasen entre, por ejemplo, 4 o 5 controladores de vista? Si tengo, por ejemplo, 4-5 controladores de vista que administran el inicio de sesión del cliente y la contraseña, etc. y quiero pasar el correo electrónico del usuario entre estos controladores de vista, ¿hay una forma más conveniente de hacerlo que declarando la var en cada controlador de vista y luego pasándola dentro de prepareforsegue? ¿Hay alguna manera de que pueda declarar una vez y cada controlador de vista pueda acceder a él, pero de una manera que también sea una buena práctica de codificación?
lozflan

45

Esta pregunta surge todo el tiempo.

Una sugerencia es crear un contenedor de datos singleton: un objeto que se crea una y solo una vez en la vida de su aplicación, y persiste durante la vida de su aplicación.

Este enfoque es adecuado para una situación en la que tiene datos de aplicaciones globales que deben estar disponibles / modificables en diferentes clases de su aplicación.

Otros enfoques, como la configuración de enlaces unidireccionales o bidireccionales entre controladores de vista, se adaptan mejor a situaciones en las que está pasando información / mensajes directamente entre controladores de vista.

(Consulte la respuesta de nhgrif, a continuación, para ver otras alternativas).

Con un contenedor de datos singleton, agrega una propiedad a su clase que almacena una referencia a su singleton y luego usa esa propiedad cada vez que necesite acceso.

Puede configurar su singleton para que guarde su contenido en el disco para que el estado de su aplicación persista entre lanzamientos.

Creé un proyecto de demostración en GitHub que demuestra cómo puede hacer esto. Aqui esta el link:

Proyecto SwiftDataContainerSingleton en GitHub Aquí está el archivo README de ese proyecto:

SwiftDataContainerSingleton

Una demostración del uso de un contenedor de datos singleton para guardar el estado de la aplicación y compartirlo entre objetos.

La DataContainerSingletonclase es el singleton real.

Utiliza una constante estática sharedDataContainerpara guardar una referencia al singleton.

Para acceder al singleton, use la sintaxis

DataContainerSingleton.sharedDataContainer

El proyecto de muestra define 3 propiedades en el contenedor de datos:

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

Para cargar la someIntpropiedad desde el contenedor de datos, usaría un código como este:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Para guardar un valor en someInt, usaría la sintaxis:

DataContainerSingleton.sharedDataContainer.someInt = 3

El initmétodo de DataContainerSingleton agrega un observador para UIApplicationDidEnterBackgroundNotification. Ese código se ve así:

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

En el código del observador, guarda las propiedades del contenedor de datos en NSUserDefaults. También puede utilizar NSCodingCore Data o varios otros métodos para guardar datos estatales.

El initmétodo de DataContainerSingleton también intenta cargar valores guardados para sus propiedades.

Esa parte del método init se ve así:

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

Las claves para cargar y guardar valores en NSUserDefaults se almacenan como constantes de cadena que forman parte de una estructura DefaultsKeys, definida así:

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

Hace referencia a una de estas constantes de esta manera:

DefaultsKeys.someInt

Usando el contenedor de datos singleton:

Esta aplicación de muestra hace un uso trival del contenedor de datos singleton.

Hay dos controladores de vista. La primera es una subclase personalizada de UIViewController ViewControllery la segunda es una subclase personalizada de UIViewController SecondVC.

Ambos controladores de vista tienen un campo de texto, y ambos cargan un valor de la someIntpropiedad singlelton del contenedor de datos en el campo de texto de su viewWillAppearmétodo, y ambos guardan el valor actual del campo de texto en el 'someInt' del contenedor de datos.

El código para cargar el valor en el campo de texto está en el viewWillAppear:método:

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

El código para guardar el valor editado por el usuario en el contenedor de datos está en los textFieldShouldEndEditingmétodos de los controladores de vista :

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

Debe cargar valores en su interfaz de usuario en viewWillAppear en lugar de viewDidLoad para que su interfaz de usuario se actualice cada vez que se muestre el controlador de vista.


8
No quiero rechazar esto porque creo que es excelente que haya invertido el tiempo para crear la pregunta y la respuesta como recurso. Gracias. A pesar de eso, creo que hacemos un flaco favor a los nuevos desarrolladores al defender los singletons para los objetos modelo. No estoy en el campo de los "singletons son malvados" (aunque los novatos deberían buscar en Google esa frase para apreciar mejor los problemas), pero sí creo que los datos del modelo son un uso cuestionable / debatible de singletons.
Rob

Me

@Duncan C Hola Duncan Estoy creando un objeto estático en cada modelo, por lo que obtengo datos de cualquier lugar, es el enfoque correcto o tengo que seguir su camino porque parece muy correcto.
Virendra Singh Rathore

@VirendraSinghRathore, las variables estáticas globales son la peor forma posible de compartir datos en la aplicación. Combinan estrechamente las partes de su aplicación e introducen serias interdependencias. Es exactamente lo contrario de "muy bien".
Duncan C

@DuncanC: ¿funcionaría este patrón para un objeto CurrentUser, básicamente un solo usuario que inició sesión en su aplicación? thx
timpone

9

Rápido 4

Hay muchos enfoques para la transmisión rápida de datos. Aquí estoy agregando algunos de los mejores enfoques.

1) Uso de StoryBoard Segue

Las secuencias de guiones gráficos son muy útiles para pasar datos entre los controladores de vista de origen y destino y viceversa también.

// 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) Usar métodos delegados

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
        }

    }

Para los empleados de Google que están tan total y absolutamente perdidos en cuanto a dónde poner los fragmentos de código Swift de StackOverflow responde como yo, ya que parece que siempre deben saber dónde infieren que va el código: utilicé la Opción 1) para enviar desde ViewControllerAa ViewControllerB. Acabo de pegar el fragmento de código en la parte inferior de mi ViewControllerA.swift(donde ViewControllerA.swiftestá realmente el nombre de su archivo, por supuesto) justo antes de la última llave. " prepare" es en realidad una función preexistente incorporada especial en una Clase determinada [que no hace nada], por lo que tienes que " override" hacerlo
velkoon

8

Otra alternativa es utilizar el centro de notificaciones (NSNotificationCenter) y publicar notificaciones. Ese es un acoplamiento muy flojo. El remitente de una notificación no necesita saber ni preocuparse por quién está escuchando. Simplemente publica una notificación y se olvida de ella.

Las notificaciones son buenas para el paso de mensajes de uno a varios, ya que puede haber un número arbitrario de observadores escuchando un mensaje determinado.


2
Tenga en cuenta que el uso del centro de notificaciones introduce un acoplamiento que quizás sea demasiado flojo. Puede hacer que rastrear el flujo de su programa sea muy difícil, por lo que debe usarse con cuidado.
Duncan C

2

En lugar de crear un singelton de controlador de datos, sugeriría crear una instancia de controlador de datos y pasarla. Para admitir la inyección de dependencia, primero crearía un DataControllerprotocolo:

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

Entonces crearía una SpecificDataControllerclase (o el nombre que sea apropiado actualmente):

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

La ViewControllerclase debería tener un campo para contener el dataController. Observe que el tipo de dataControlleres el protocolo DataController. De esta manera, es fácil cambiar las implementaciones del controlador de datos:

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

En AppDelegatepodemos configurar el 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
}

Cuando nos movemos a un viewController diferente podemos pasar el dataControlleren:

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

Ahora, cuando deseamos cambiar el controlador de datos para una tarea diferente, podemos hacerlo en el AppDelegate y no tenemos que cambiar ningún otro código que utilice el controlador de datos.

Por supuesto, esto es una exageración si simplemente queremos pasar un solo valor. En este caso, es mejor ir con la respuesta de nhgrif.

Con este enfoque podemos separar la vista de la parte lógica.


1
Hola, este enfoque es limpio, comprobable y lo uso la mayor parte del tiempo en aplicaciones pequeñas, pero en aplicaciones más grandes, donde no todos los VC (quizás ni siquiera el VC raíz) pueden necesitar la dependencia (por ejemplo, DataController en este caso) Parece un desperdicio que cada VC requiera la dependencia solo para pasarla. Además, si usa diferentes tipos de VC (por ejemplo, UIVC regular versus NavigationVC), entonces necesita subclasificar esos tipos diferentes solo para agregar esa variable de dependencia. ¿Cómo aborda esto?
RobertoCuba

1

Como señaló @nhgrif en su excelente respuesta, hay muchas formas diferentes en que los VC (controladores de vista) y otros objetos pueden comunicarse entre sí.

El singleton de datos que describí en mi primera respuesta es realmente más sobre compartir y guardar el estado global que sobre comunicarse directamente.

La respuesta de nhrif le permite enviar información directamente desde la fuente al VC de destino. Como mencioné en la respuesta, también es posible enviar mensajes desde el destino al origen.

De hecho, puede configurar un canal activo unidireccional o bidireccional entre diferentes controladores de vista. Si los controladores de vista están vinculados a través de una secuencia de guión gráfico, el momento de configurar los vínculos es en el método prepareFor Segue.

Tengo un proyecto de muestra en Github que usa un controlador de vista principal para alojar 2 vistas de tabla diferentes como elementos secundarios. Los controladores de vista secundaria están vinculados mediante incrustaciones de segues, y el controlador de vista principal conecta enlaces de 2 vías con cada controlador de vista en el método prepareForSegue.

Puede encontrar ese proyecto en github (enlace). Sin embargo, lo escribí en Objective-C y no lo he convertido a Swift, por lo que si no se siente cómodo con Objective-C, puede ser un poco difícil de seguir.


1

SWIFT 3:

Si tiene un guión gráfico con segues identificadas, use:

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

Aunque si hace todo mediante programación, incluida la navegación entre diferentes UIViewControllers, use el método:

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

Nota: para usar la segunda forma que necesita para hacer su UINavigationController, está presionando UIViewControllers, un delegado y debe cumplir con el protocolo 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

    }
}

nunca hacer self.delegate = self
malhal

1

Depende de cuándo quieras obtener datos.

Si desea obtener datos cuando lo desee, puede utilizar un patrón singleton. La clase de patrón está activa durante el tiempo de ejecución de la aplicación. A continuación se muestra un ejemplo del patrón singleton.

class AppSession: NSObject {

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

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

        print(AppSession.shared.username)
    }
}

Si desea obtener datos después de cualquier acción, puede utilizar 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")
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.