Как вы делитесь данными между контроллерами представлений и другими объектами в Swift?

Скажем, у меня есть несколько контроллеров представлений в моем приложении Swift, и я хочу иметь возможность передавать данные между ними. Если я несколько уровней в стеке диспетчера представлений, как передать данные другому контроллеру представления? Или между вкладками в контроллере просмотра панели вкладок?

(Обратите внимание, что этот вопрос является "звоном".) Его так много спрашивают, что я решил написать учебник по этому вопросу. См. Мой ответ ниже.

Ответ 1

Ваш вопрос очень широк. Предложить, что есть одно простое решение для каждого сценария, немного наивное. Итак, давайте рассмотрим некоторые из этих сценариев.


Самый распространенный сценарий, о котором я рассказывал о переполнении стека в своем опыте, - это простая передача информации с одного контроллера вида на следующий.

Если мы используем раскадровку, наш первый контроллер представления может переопределить prepareForSegue, что и есть то, для чего оно есть. Объект UIStoryboardSegue передается при вызове этого метода и содержит ссылку на наш контроллер представления назначения. Здесь мы можем установить значения, которые мы хотим передать.

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

В качестве альтернативы, если мы не используем раскадровки, мы загружаем наш контроллер представления из наконечника. Наш код немного проще.

func showNextController() {
    let destination = SecondController(nibName: "SecondController", bundle: NSBundle.mainBundle())
    destination.myInformation = self.myInformation
    self.showViewController(destination, sender: self)
}

В обоих случаях myInformation является свойством на каждом контроллере представления, в котором все данные должны передаваться от одного контроллера представления к другому. Очевидно, что они не должны иметь одно и то же имя на каждом контроллере.


Мы также можем обмениваться информацией между вкладками в UITabBarController.

В этом случае это на самом деле потенциально даже проще.

Сначала создайте подкласс UITabBarController и дайте ему свойства для любой информации, которую мы хотим разделить между различными вкладками:

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

Теперь, если мы создаем наше приложение из раскадровки, мы просто меняем класс контроллера панели вкладок со значения по умолчанию от UITabBarController до MyCustomTabController. Если мы не используем раскадровку, мы просто создаем экземпляр этого настраиваемого класса, а не класс по умолчанию UITabBarController и добавляем к нему наш контроллер представления.

Теперь все наши контроллеры представлений в контроллере панели вкладок могут получить доступ к этому свойству как таковое:

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

И путем подклассификации UINavigationController таким же образом мы можем использовать тот же подход для совместного использования данных во всем стеке навигации:

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

Существует несколько других сценариев. Ни в коем случае этот ответ не охватывает всех из них.

Ответ 2

Этот вопрос возникает постоянно.

Одно из предложений заключается в создании контейнера данных singleton: объект, который создается один раз и только один раз в жизни вашего приложения и сохраняется в течение срока действия вашего приложения.

Этот подход хорошо подходит для ситуации, когда у вас есть глобальные данные приложения, которые должны быть доступны/модифицируемы для разных классов в вашем приложении.

Другие подходы, такие как настройка односторонних или двухсторонних ссылок между контроллерами представлений, лучше подходят для ситуаций, когда вы передаете информацию/сообщения непосредственно между контроллерами представлений.

(см. ниже nhgrif ответ для других альтернатив.)

С контейнером данных singleton вы добавляете свойство в свой класс, в котором хранится ссылка на ваш singleton, и затем используйте это свойство в любое время, когда вам нужен доступ.

Вы можете настроить свой синглтон так, чтобы он сохранял его содержимое на диске, чтобы ваше состояние приложения сохранялось между запусками.

Я создал демонстрационный проект на GitHub, демонстрирующий, как вы можете это сделать. Вот ссылка:

Проект SwiftDataContainerSingleton на GitHub Вот README из этого проекта:

SwiftDataContainerSingleton

Демонстрация использования синтаксиса контейнера данных для сохранения состояния приложения и совместного использования его между объектами.

Класс DataContainerSingleton - это действительный синглтон.

Он использует статическую константу sharedDataContainer для сохранения ссылки на синглтон.

Чтобы получить доступ к синглтону, используйте синтаксис

DataContainerSingleton.sharedDataContainer

Пример проекта определяет 3 свойства в контейнере данных:

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

Чтобы загрузить свойство someInt из контейнера данных, вы должны использовать следующий код:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Чтобы сохранить значение someInt, вы должны использовать синтаксис:

DataContainerSingleton.sharedDataContainer.someInt = 3

Метод DataContainerSingleton init добавляет наблюдателя для UIApplicationDidEnterBackgroundNotification. Этот код выглядит следующим образом:

goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
  UIApplicationDidEnterBackgroundNotification,
  object: nil,
  queue: nil)
  {
    (note: NSNotification!) -> Void in
    let defaults = NSUserDefaults.standardUserDefaults()
    //-----------------------------------------------------------------------------
    //This code saves the singleton 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()
}

В коде наблюдателя он сохраняет свойства контейнера данных NSUserDefaults. Вы также можете использовать NSCoding, Core Data или различные другие способы сохранения данных состояния.

Метод DataContainerSingleton init также пытается загрузить сохраненные значения для его свойств.

Эта часть метода init выглядит так:

let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton 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?
//-----------------------------------------------------------------------------

Ключи для загрузки и сохранения значений в NSUserDefault хранятся в виде строковых констант, которые являются частью структуры DefaultsKeys, определенной следующим образом:

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

Вы ссылаетесь на одну из следующих констант:

DefaultsKeys.someInt

Использование контейнера данных singleton:

Это примерное приложение использует трехстороннее использование синтаксиса контейнера данных.

Существует два контроллера вида. Первый - это пользовательский подкласс UIViewController ViewController, а второй - пользовательский подкласс UIViewController SecondVC.

Оба диспетчера представлений имеют на них текстовое поле, и оба загружают значение из свойства singlelton someInt контейнера данных в текстовое поле в свой метод viewWillAppear и оба сохраняют текущее значение из текстового поля обратно в `someInt 'контейнера данных.

Код для загрузки значения в текстовое поле находится в методе viewWillAppear::

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

Код для сохранения измененного пользователем значения обратно в контейнер данных находится в методах диспетчера вида textFieldShouldEndEditing:

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

Вы должны загружать значения в свой пользовательский интерфейс в viewWillAppear, а не в viewDidLoad, чтобы ваш пользовательский интерфейс обновлялся каждый раз, когда отображается контроллер вида.

Ответ 3

Другой альтернативой является использование центра уведомлений (NSNotificationCenter) и отправки уведомлений. Это очень свободная связь. Отправитель уведомления не должен знать или заботиться о том, кто слушает. Он просто публикует уведомление и забывает об этом.

Уведомления хороши для передачи сообщений "один ко многим", поскольку может быть произвольное количество наблюдателей, слушающих данное сообщение.

Ответ 4

Swift 4

Существует так много подходов к передаче данных в swift. Здесь я добавляю некоторые из лучших подходов.

1) Использование StoryBoard Segue

Сегменты раскадровки очень полезны для передачи данных между контроллерами Source и Destination View Controllers и наоборот.

// 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) Использование методов делегата

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
        }

    }

Ответ 5

SWIFT 3:

Если у вас есть раскадровка с определенными сегментами, используйте:

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

Хотя если вы все программно осуществляете навигацию между различными UIViewControllers, используйте метод:

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

Примечание. Чтобы использовать второй способ, которым вам нужен ваш UINavigationController, вы нажимаете UIViewControllers на, делегат, и он должен соответствовать протоколу 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

    }
}

Ответ 6

Вместо создания контроллера данных контроллера я бы предложил создать экземпляр контроллера данных и передать его. Чтобы поддерживать инъекцию зависимостей, я бы сначала создал протокол DataController:

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

Тогда я бы создал SpecificDataController (или любое другое имя в настоящее время было бы подходящим) class:

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

В классе ViewController должно быть поле для хранения DataController. Обратите внимание, что тип DataController - это протокол DataController. Таким образом легко отключить реализацию контроллера данных:

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

В AppDelegate мы можем установить viewController DataController:

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

Когда мы переходим к другому viewController, мы можем передать DataController on в:

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

Теперь, когда мы хотим отключить контроллер данных для другой задачи, мы можем сделать это в AppDelegate и не нужно менять какой-либо другой код, который использует контроллер данных.

Это, конечно, излишне, если мы просто хотим передать одно значение. В этом случае лучше всего пойти с ответом nhgrif.

При таком подходе мы можем разделить вид логической части.

Ответ 7

Как отметил @nhgrif в своем превосходном ответе, существует множество способов, которыми VC (контроллеры просмотра) и другие объекты могут связываться друг с другом.

Синтаксис данных, который я изложил в своем первом ответе, действительно больше касается обмена и сохранения глобального состояния, чем непосредственного общения.

Ответ nhrif позволяет отправлять информацию непосредственно от источника до целевого VC. Как я уже упоминал в ответ, он также может отправлять сообщения обратно из пункта назначения в источник.

Фактически вы можете настроить активный односторонний или двухсторонний канал между различными контроллерами. Если контроллеры представлений связаны с помощью раскадровки, время, необходимое для настройки ссылок, находится в методе prepareFor Segue.

У меня есть пример проекта Github, который использует родительский контроллер представления для размещения двух разных табличных представлений в качестве дочерних. Контроллеры дочерних представлений связаны с использованием встроенных сегментов, а контроллер родительского представления подключает двухсторонние ссылки с каждым контроллером представления в методе prepareForSegue.

Вы можете найти этот проект на github (ссылка). Однако я написал его в Objective-C и не преобразовал его в Swift, поэтому, если вам не удобно в Objective-C, может быть немного сложно следовать