Como você compartilha dados entre controladores de visualização e outros objetos no Swift?

88

Digamos que eu tenha vários controladores de visualização em meu aplicativo Swift e quero poder passar dados entre eles. Se estou vários níveis abaixo em uma pilha de controlador de exibição, como faço para passar dados para outro controlador de exibição? Ou entre as guias em um controlador de visualização da barra de guias?

(Observe, essa pergunta é um "toque".) Ela é tão questionada que resolvi escrever um tutorial sobre o assunto. Veja minha resposta abaixo.

Duncan C
fonte
1
Tente pesquisar no Google por delegados
milo526
4
Eu postei isso para que pudesse fornecer uma solução para as 10.000 instâncias dessa pergunta que aparecem todos os dias aqui no SO. Veja minha auto-resposta. :)
Duncan C
Desculpe, eu fui muito rápido em reagir :) bom para poder colocar um link para este :)
milo526
2
Não se preocupe. Você pensou que eu era o # 10.001, não é? <grin>
Duncan C,
4
@DuncanC Não gosto da sua resposta. :( É ok-isn como uma resposta geral para todos os cenários ... insomuchas, vai funcionar para todos os cenários, mas também não é a abordagem certa para quase todos os cenários. Apesar disso, agora temos isso em nossa cabeça que marcar qualquer pergunta sobre o tópico como uma duplicata deste é uma boa ideia? Por favor, não faça isso.
nhgrif

Respostas:

91

Sua pergunta é muito ampla. Sugerir que existe uma solução abrangente para cada cenário é um pouco ingênuo. Então, vamos examinar alguns desses cenários.


O cenário mais comum questionado sobre Stack Overflow na minha experiência é a simples passagem de informações de um controlador de visualização para o próximo.

Se estivermos usando storyboard, nosso primeiro controlador de visualização pode substituir prepareForSegue, que é exatamente o que existe. Um UIStoryboardSegueobjeto é passado quando este método é chamado e contém uma referência ao nosso controlador de visualização de destino. Aqui, podemos definir os valores que queremos passar.

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

Alternativamente, se não estivermos usando storyboards, estamos carregando nosso controlador de visualização de uma ponta. Nosso código é um pouco mais simples então.

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

Em ambos os casos, myInformationé uma propriedade em cada controlador de visualização contendo todos os dados que precisam ser passados ​​de um controlador de visualização para o próximo. Eles obviamente não precisam ter o mesmo nome em cada controlador.


Também podemos querer compartilhar informações entre as guias em a UITabBarController.

Nesse caso, é potencialmente ainda mais simples.

Primeiro, vamos criar uma subclasse de UITabBarControllere dar a ela propriedades para qualquer informação que desejamos compartilhar entre as várias guias:

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

Agora, se estamos construindo nosso aplicativo a partir do storyboard, simplesmente alteramos a classe do controlador da barra de guias do padrão UITabBarControllerpara MyCustomTabController. Se não estivermos usando um storyboard, simplesmente instanciamos uma instância dessa classe personalizada em vez da UITabBarControllerclasse padrão e adicionamos nosso controlador de visualização a ela.

Agora, todos os nossos controladores de visualização dentro do controlador da barra de guias podem acessar esta propriedade da seguinte forma:

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

E ao UINavigationControllercriar subclasses da mesma maneira, podemos usar a mesma abordagem para compartilhar dados em uma pilha de navegação inteira:

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

Existem vários outros cenários. De forma alguma esta resposta cobre todos eles.

nhgrif
fonte
1
Eu também acrescentaria que às vezes você deseja que um canal envie informações de volta do controlador de exibição de destino para o controlador de exibição de origem. Uma maneira comum de lidar com essa situação é adicionar uma propriedade delegada ao destino e, em seguida, no prepareForSegue do controlador de visualização de origem, definir a propriedade delegada do controlador de visualização de destino como self. (e definir um protocolo que defina as mensagens que o VC de destino usa para enviar mensagens para o VC de origem)
Duncan C
1
nhgrif, eu concordo. O conselho para novos desenvolvedores deve ser que, se você precisar passar dados entre as cenas no storyboard, use prepareForSegue. É uma pena que esta observação muito simples se perca entre as outras respostas e digressões aqui.
Rob
2
@Rob Yup. Singletons e notificações devem ser as últimas opções. Devemos preferir prepareForSegueoutras transferências diretas de informações em quase todos os cenários e então simplesmente concordar com os novatos quando eles aparecem com o cenário para o qual essas situações não funcionam e, então, temos que ensiná-los sobre essas abordagens mais globais.
nhgrif de
1
Depende. Mas estou muito, muito preocupado em usar o delegado do aplicativo como nossa lixeira para código que não sabemos mais onde colocar. Aqui está o caminho para a loucura.
nhgrif
2
@nhgrif. obrigado pela sua resposta. e se, no entanto, você quiser que os dados sejam transmitidos entre, digamos, 4 ou 5 controladores de visualização. se eu tiver digamos 4-5 viewcontrollers gerenciando login e senha do cliente, etc, e eu quiser passar o e-mail do usuário entre esses viewcontrollers, existe uma maneira mais conveniente de fazer isso do que declarar var em cada viewcontroller e depois passá-lo dentro do prepareforsegue. Existe uma maneira de eu declarar uma vez e cada viewcontroller pode acessá-lo, mas de uma forma que também seja uma boa prática de codificação?
lozflan
45

Essa pergunta surge o tempo todo.

Uma sugestão é criar um singleton de contêiner de dados: Um objeto que é criado uma vez e apenas uma vez na vida de seu aplicativo e persiste por toda a vida de seu aplicativo.

Essa abordagem é adequada para uma situação em que você tem dados globais de aplicativo que precisam estar disponíveis / modificáveis ​​em diferentes classes em seu aplicativo.

Outras abordagens, como a configuração de links unilaterais ou bidirecionais entre controladores de visualização, são mais adequadas para situações em que você está passando informações / mensagens diretamente entre controladores de visualização.

(Veja a resposta de nhgrif, abaixo, para outras alternativas.)

Com um singleton de contêiner de dados, você adiciona uma propriedade à sua classe que armazena uma referência ao seu singleton e, em seguida, usa essa propriedade sempre que precisar de acesso.

Você pode configurar seu singleton para que ele salve seu conteúdo em disco para que o estado do seu aplicativo persista entre as inicializações.

Eu criei um projeto de demonstração no GitHub demonstrando como você pode fazer isso. Aqui está o link:

Projeto SwiftDataContainerSingleton no GitHub Aqui está o README desse projeto:

SwiftDataContainerSingleton

Uma demonstração do uso de um singleton de contêiner de dados para salvar o estado do aplicativo e compartilhá-lo entre objetos.

A DataContainerSingletonclasse é o único singleton real.

Ele usa uma constante estática sharedDataContainerpara salvar uma referência ao singleton.

Para acessar o singleton, use a sintaxe

DataContainerSingleton.sharedDataContainer

O projeto de amostra define 3 propriedades no contêiner de dados:

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

Para carregar a someIntpropriedade do contêiner de dados, você usaria um código como este:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Para salvar um valor em someInt, você usaria a sintaxe:

DataContainerSingleton.sharedDataContainer.someInt = 3

O initmétodo DataContainerSingleton adiciona um observador para o UIApplicationDidEnterBackgroundNotification. Esse código é parecido com este:

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

No código do observador, ele salva as propriedades do contêiner de dados em NSUserDefaults. Você também pode usar NSCodingCore Data ou vários outros métodos para salvar dados de estado.

O initmétodo DataContainerSingleton também tenta carregar valores salvos para suas propriedades.

Essa parte do método init se parece com isto:

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

As chaves para carregar e salvar valores em NSUserDefaults são armazenadas como constantes de string que fazem parte de uma estrutura DefaultsKeys, definidas assim:

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

Você faz referência a uma dessas constantes assim:

DefaultsKeys.someInt

Usando o singleton do contêiner de dados:

Este aplicativo de amostra faz uso triplo do singleton do contêiner de dados.

Existem dois controladores de visualização. A primeira é uma subclasse personalizada de UIViewController ViewControllere a segunda é uma subclasse personalizada de UIViewController SecondVC.

Ambos os controladores de visão têm um campo de texto neles, e ambos carregam um valor da someIntpropriedade de singlelton do contêiner de dados no campo de texto em seu viewWillAppearmétodo, e ambos salvam o valor atual do campo de texto de volta em `someInt 'do contêiner de dados.

O código para carregar o valor no campo de texto está no 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)"
}

O código para salvar o valor editado pelo usuário de volta no recipiente de dados está nos textFieldShouldEndEditingmétodos dos controladores de visualização :

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

Você deve carregar valores em sua interface com o usuário em viewWillAppear em vez de viewDidLoad para que sua IU seja atualizada cada vez que o controlador de visualização for exibido.

Duncan C
fonte
8
Não quero votar contra isso porque acho excelente que você tenha investido tempo para criar a pergunta e a resposta como um recurso. Obrigado. Apesar disso, acho que prestamos um grande desserviço aos novos desenvolvedores ao defender singletons para objetos de modelo. Não estou no campo "singletons are evil" (embora os novatos devam pesquisar essa frase no Google para avaliar melhor os problemas), mas acho que dados de modelo são um uso questionável / discutível de singletons.
Rob
adoraria ver um artigo incrível como seu sobre links bidirecionais
Cmag
@Duncan C Olá Duncan, estou criando um objeto estático em cada modelo, então pego os dados de qualquer lugar onde seja a abordagem certa ou tenho que seguir seu caminho porque parece muito certo.
Virendra Singh Rathore
@VirendraSinghRathore, Variáveis ​​estáticas globais são a pior maneira possível de compartilhar dados no aplicativo. Eles unem fortemente as partes do seu aplicativo e introduzem interdependências sérias. É exatamente o oposto de "muito certo".
Duncan C
@DuncanC - esse padrão funcionaria para um objeto CurrentUser - basicamente um único usuário que está conectado ao seu aplicativo? thx
timpone
9

Swift 4

Existem muitas abordagens para a passagem rápida de dados. Aqui estou adicionando algumas das melhores abordagens disso.

1) Usando o StoryBoard Segue

As sequências de storyboard são muito úteis para passar dados entre os controladores de visualização de origem e destino e vice-versa.

// 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) Usando métodos de delegação

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
        }

    }
Equipe iOS
fonte
Para Googlers que estão totalmente perdidos quanto a onde colocar os snippets de código Swift das respostas StackOverflow como eu, parece que você sempre deve saber para onde eles inferem que o código vai: Usei a Opção 1) para enviar de ViewControllerApara ViewControllerB. Eu apenas coloquei o trecho de código na parte inferior do meu ViewControllerA.swift(onde ViewControllerA.swiftestá o nome do seu arquivo, é claro) logo antes da última chave. " prepare" é na verdade uma função pré-existente embutida especial em uma determinada classe [que não faz nada], e é por isso que você deve " override" isso
velkoon
8

Outra alternativa é usar a central de notificações (NSNotificationCenter) e postar notificações. Esse é um acoplamento muito fraco. O remetente de uma notificação não precisa saber ou se importar com quem está ouvindo. Ele apenas posta uma notificação e se esquece dela.

As notificações são boas para a passagem de mensagens um para muitos, uma vez que pode haver um número arbitrário de observadores escutando uma determinada mensagem.

Duncan C
fonte
2
Observe que o uso da central de notificação introduz um acoplamento que talvez seja muito frouxo. Isso pode dificultar muito o rastreamento do fluxo do programa, por isso deve ser usado com cuidado.
Duncan C
2

Em vez de criar um único controlador de dados, eu sugeriria criar uma instância do controlador de dados e distribuí-la. Para oferecer suporte à injeção de dependência, primeiro criaria um DataControllerprotocolo:

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

Então, eu criaria uma SpecificDataControllerclasse (ou qualquer nome que seja apropriado no momento):

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

A ViewControllerclasse deve ter um campo para conter o dataController. Observe que o tipo de dataControlleré o protocolo DataController. Dessa forma, é fácil alternar as implementações do controlador de dados:

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

Em AppDelegatepodemos definir o 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 mudamos para um viewController diferente, podemos passar o seguinte dataController:

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

Agora, quando desejamos trocar o controlador de dados para uma tarefa diferente, podemos fazer isso no AppDelegatee não precisamos alterar nenhum outro código que use o controlador de dados.

É claro que isso é um exagero se quisermos simplesmente passar um único valor. Nesse caso, é melhor seguir a resposta de nhgrif.

Com essa abordagem, podemos separar a visualização da parte lógica.

Kristiina
fonte
1
Olá, esta abordagem é limpa, testável e é o que eu uso na maioria das vezes em aplicativos pequenos, mas em maiores, onde nem todos os VC (talvez nem mesmo o VC raiz) podem precisar da dependência (por exemplo, DataController neste caso) parece um desperdício para cada VC exigir a dependência apenas para distribuí-la. Além disso, se você usar diferentes tipos de VC (por exemplo, UIVC regular versus NavigationVC), você precisará criar uma subclasse desses tipos diferentes apenas para adicionar essa variável de dependência. Como você aborda isso?
RobertoCuba
1

Como @nhgrif apontou em sua excelente resposta, há muitas maneiras diferentes pelas quais os VCs (controladores de visualização) e outros objetos podem se comunicar uns com os outros.

O singleton de dados que delineei em minha primeira resposta é realmente mais sobre como compartilhar e salvar o estado global do que comunicar-se diretamente.

A resposta do nhrif permite enviar informações diretamente da fonte para o VC de destino. Como mencionei na resposta, também é possível enviar mensagens de volta do destino para a origem.

Na verdade, você pode configurar um canal ativo unilateral ou bidirecional entre diferentes controladores de visualização. Se os controladores de visualização estiverem vinculados por meio de uma sequência de storyboard, a hora de configurar os links está no método prepareFor Segue.

Eu tenho um projeto de amostra no Github que usa um controlador de visualização pai para hospedar 2 visualizações de tabela diferentes como crianças. Os controladores de visualização filho são vinculados usando segues incorporados, e o controlador de visualização pai conecta links de 2 vias com cada controlador de visualização no método prepareForSegue.

Você pode encontrar esse projeto no github (link). Eu o escrevi em Objective-C, no entanto, e não o converti para Swift, então se você não estiver confortável em Objective-C, pode ser um pouco difícil de seguir

Duncan C
fonte
1

SWIFT 3:

Se você tiver um storyboard com segues identificados, use:

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

Embora se você fizer tudo programaticamente, incluindo a navegação entre diferentes UIViewControllers, use o método:

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

Nota: para usar a segunda maneira que você precisa para tornar seu UINavigationController, você está enviando UIViewControllers, um delegado e ele precisa estar em conformidade com o 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

    }
}
Máxima
fonte
nunca faça self.delegate = self
malhal
1

Depende de quando você deseja obter dados.

Se você quiser obter dados sempre que quiser, pode usar um padrão singleton. A classe padrão está ativa durante o tempo de execução do aplicativo. Aqui está um exemplo do padrão singleton.

class AppSession: NSObject {

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

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

        print(AppSession.shared.username)
    }
}

Se você deseja obter dados após qualquer ação, pode usar o 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
fonte