Скажем, у меня есть несколько контроллеров представления в моем приложении Swift, и я хочу иметь возможность передавать данные между ними. Если я на несколько уровней ниже в стеке контроллера представления, как передать данные другому контроллеру представления? Или между вкладками в контроллере представления панели вкладок?
(Обратите внимание, этот вопрос - "звоночек".) Его задают так много, что я решил написать учебник по этому вопросу. Смотрите мой ответ ниже.
Ответы:
Ваш вопрос очень широкий. Было бы наивно предлагать одно простое универсальное решение для каждого сценария. Итак, давайте рассмотрим некоторые из этих сценариев.
По моему опыту, наиболее распространенный сценарий, о котором спрашивают в Stack Overflow, - это простая передача информации от одного контроллера представления к другому.
Если мы используем раскадровку, наш первый контроллер представления может переопределить
prepareForSegue
, и это именно то, для чего он существует. АUIStoryboardSegue
вызове этого метода передается объект, и он содержит ссылку на наш целевой контроллер представления. Здесь мы можем установить значения, которые хотим передать.override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "MySegueID" { if let destination = segue.destination as? SecondController { destination.myInformation = self.myInformation } } }
В качестве альтернативы, если мы не используем раскадровки, мы загружаем наш контроллер представления из пера. Тогда наш код немного проще.
func showNextController() { let destination = SecondController(nibName: "SecondController", bundle: nil) destination.myInformation = self.myInformation show(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 }
Есть несколько других сценариев. Этот ответ никоим образом не охватывает их всех.
источник
prepareForSegue
. Жаль, что это очень простое наблюдение теряется среди других ответов и отступлений здесь.prepareForSegue
или другую прямую передачу информации почти в каждом сценарии, а затем просто быть в порядке с новичками, когда они появляются со сценарием, для которого эти ситуации не работают, и затем мы должны научить их этим более глобальным подходам.Этот вопрос возникает постоянно.
Одно из предложений - создать одноэлементный контейнер данных: объект, который создается один раз и только один раз в жизни вашего приложения и сохраняется на протяжении всего срока службы вашего приложения.
Этот подход хорошо подходит для ситуации, когда у вас есть глобальные данные приложения, которые должны быть доступны / изменены в разных классах вашего приложения.
Другие подходы, такие как установка односторонних или двусторонних связей между контроллерами представления, лучше подходят для ситуаций, когда вы передаете информацию / сообщения напрямую между контроллерами представления.
(См. Ответ nhgrif ниже для других альтернатив.)
Используя синглтон контейнера данных, вы добавляете свойство в свой класс, в котором хранится ссылка на ваш синглтон, а затем используете это свойство в любое время, когда вам нужен доступ.
Вы можете настроить свой синглтон так, чтобы он сохранял его содержимое на диск, чтобы состояние вашего приложения сохранялось между запусками.
Я создал демонстрационный проект на 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'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() }
В коде наблюдателя он сохраняет свойства контейнера данных в
NSUserDefaults
. Вы также можете использоватьNSCoding
Core Data или различные другие методы для сохранения данных о состоянии.Метод DataContainerSingleton
init
также пытается загрузить сохраненные значения для своих свойств.Эта часть метода инициализации выглядит так:
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? //-----------------------------------------------------------------------------
Ключи для загрузки и сохранения значений в NSUserDefaults хранятся как строковые константы, которые являются частью структуры
DefaultsKeys
, определенной следующим образом:struct DefaultsKeys { static let someString = "someString" static let someOtherString = "someOtherString" static let someInt = "someInt" }
Вы ссылаетесь на одну из этих констант следующим образом:
DefaultsKeys.someInt
Использование синглтона контейнера данных:
В этом примере приложения трижды используется одноэлементный контейнер данных.
Есть два контроллера представления. Первый - это настраиваемый подкласс UIViewController
ViewController
, а второй - настраиваемый подкласс UIViewControllerSecondVC
.На обоих контроллерах представления есть текстовое поле, и оба загружают значение из
someInt
свойства singlelton контейнера данных в текстовое поле своего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, чтобы ваш пользовательский интерфейс обновлялся каждый раз, когда отображается контроллер представления.
источник
Swift 4
Существует так много подходов к быстрой передаче данных. Здесь я добавляю некоторые из лучших подходов к этому.
1) Использование StoryBoard Segue
Сегменты раскадровки очень полезны для передачи данных между контроллерами представления источника и назначения и наоборот.
// 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 } }
источник
ViewControllerA
доViewControllerB
. Я просто вставил фрагмент кода в нижнюю часть своего файлаViewControllerA.swift
(гдеViewControllerA.swift
, конечно, называется то, что называется вашим файлом) прямо перед последней фигурной скобкой. "prepare
" на самом деле является специальной встроенной ранее существовавшей функцией в данном классе [которая ничего не делает], поэтому вы должны "override
" ееДругой альтернативой является использование центра уведомлений (NSNotificationCenter) и публикации уведомлений. Это очень слабая связь. Отправителю уведомления не нужно знать, кто его слушает. Он просто отправляет уведомление и забывает об этом.
Уведомления хороши для передачи сообщений «один ко многим», так как может быть произвольное количество наблюдателей, прослушивающих данное сообщение.
источник
Вместо создания синглтона контроллера данных я бы предложил создать экземпляр контроллера данных и передать его. Для поддержки внедрения зависимостей я бы сначала создал
DataController
протокол:protocol DataController { var someInt : Int {get set} var someString : String {get set} }
Затем я бы создал
SpecificDataController
класс (или любое другое имя, которое сейчас подходило бы):class SpecificDataController : DataController { var someInt : Int = 5 var someString : String = "Hello data" }
Тогда у
ViewController
класса должно быть поле для храненияdataController
. Обратите внимание, что типdataController
- это протоколDataController
. Таким образом легко отказаться от реализации контроллера данных:class ViewController : UIViewController { var dataController : DataController? ... }
В
AppDelegate
мы можем установить viewControllerdataController
:func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { if let viewController = self.window?.rootViewController as? ViewController { viewController.dataController = SpecificDataController() } return true }
Когда мы переходим к другому viewController, мы можем передать
dataController
:override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { ... }
Теперь, когда мы хотим переключить контроллер данных для другой задачи, мы можем сделать это в
AppDelegate
и нам не нужно изменять какой-либо другой код, который использует контроллер данных.Это, конечно, перебор, если мы просто хотим передать одно значение. В этом случае лучше всего использовать ответ nhgrif.
При таком подходе мы можем отделить представление от логической части.
источник
Как отметил @nhgrif в своем превосходном ответе, существует множество различных способов, которыми VC (контроллеры представления) и другие объекты могут связываться друг с другом.
Одноэлементный элемент данных, который я описал в своем первом ответе, на самом деле больше касается совместного использования и сохранения глобального состояния, чем прямого общения.
Ответ nhrif позволяет отправлять информацию непосредственно из источника в целевой виртуальный канал. Как я уже упоминал в ответе, также можно отправлять сообщения от места назначения к источнику.
Фактически, вы можете настроить активный односторонний или двусторонний канал между различными контроллерами представления. Если контроллеры представления связаны через переход раскадровки, время для настройки ссылок находится в методе prepareFor Segue.
У меня есть образец проекта на Github, который использует родительский контроллер представления для размещения двух разных табличных представлений в качестве дочерних. Контроллеры дочерних представлений связаны с помощью встроенных сегментов, а родительский контроллер представления связывает двусторонние связи с каждым контроллером представления в методе prepareForSegue.
Вы можете найти этот проект на github (ссылка). Однако я написал его на Objective-C и не конвертировал в Swift, поэтому, если вам неудобно работать с Objective-C, может быть немного сложно следовать
источник
SWIFT 3:
Если у вас есть раскадровка с определенными переходами, используйте:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
Хотя, если вы делаете все программно, включая навигацию между разными UIViewController, тогда используйте метод:
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 } }
источник
Это зависит от того, когда вы хотите получить данные.
Если вы хотите получать данные в любое время, можете использовать одноэлементный шаблон. Класс шаблона активен во время выполнения приложения. Вот пример шаблона singleton.
class AppSession: NSObject { static let shared = SessionManager() var username = "Duncan" } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print(AppSession.shared.username) } }
Если вы хотите получить данные после какого-либо действия, можете использовать 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") }
источник