Как вы делитесь данными между контроллерами представлений и другими объектами в 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, может быть немного сложно следовать