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

88

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

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

Дункан С
источник
1
Попробуйте
поискать в
4
Я разместил это, чтобы предоставить решение для 10 000 экземпляров этого вопроса, которые ежедневно появляются здесь, на SO. Смотрите мой ответ. :)
Duncan C
Извините, я слишком быстро отреагировал :) хорошо, что могу дать ссылку на это :)
milo526
2
Без проблем. Вы думали, что я номер 10 001, не так ли? <grin>
Duncan C
4
@DuncanC Мне не нравится твой ответ. :( Это нормально - разве как универсальный ответ для каждого сценария ... в некоторых случаях это будет работать для каждого сценария, но это также не правильный подход практически для любого сценария. Несмотря на это, теперь мы ввели это в нашу голову что помечать любой вопрос по теме как дубликат этого - хорошая идея? Пожалуйста, не надо.
nhgrif

Ответы:

91

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


По моему опыту, наиболее распространенный сценарий, о котором спрашивают в 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
}

Есть несколько других сценариев. Этот ответ никоим образом не охватывает их всех.

nhgrif
источник
1
Я бы также добавил, что иногда вам нужно, чтобы канал отправлял информацию обратно от целевого контроллера представления к контроллеру исходного представления. Обычный способ справиться с этой ситуацией - добавить свойство делегата в место назначения, а затем в файле prepareForSegue исходного контроллера представления установить для свойства делегата целевого контроллера представления значение self. (и определить протокол, который определяет сообщения, которые целевой виртуальный канал использует для отправки сообщений исходному виртуальному каналу)
Дункан Ц.
1
nhgrif, согласен. Совет для новых разработчиков должен заключаться в том, что если вам нужно передавать данные между сценами в раскадровке, используйте prepareForSegue. Жаль, что это очень простое наблюдение теряется среди других ответов и отступлений здесь.
Роб
2
@ Роб Ага. Синглтоны и уведомления должны быть последними вариантами. Мы должны предпочесть prepareForSegueили другую прямую передачу информации почти в каждом сценарии, а затем просто быть в порядке с новичками, когда они появляются со сценарием, для которого эти ситуации не работают, и затем мы должны научить их этим более глобальным подходам.
nhgrif
1
Это зависит. Но меня очень, очень беспокоит использование делегата приложения в качестве свалки для кода, который мы не знаем, куда еще поместить. Вот и путь к безумию.
nhgrif
2
@nhgrif. спасибо за ваш ответ. что, если вы хотите, чтобы данные передавались, скажем, между 4 или 5 контроллерами просмотра. Если я получил, скажем, 4-5 контроллеров просмотра, управляющих логином и паролем клиента и т. д., и я хочу передать электронную почту пользователя между этими контроллерами просмотра, есть ли более удобный способ сделать это, чем объявление var в каждом контроллере просмотра, а затем передача его в prepareforsegue. есть ли способ, которым я могу объявить один раз, и каждый контроллер представления может получить к нему доступ, но таким образом, чтобы это также было хорошей практикой кодирования?
lozflan 05
45

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

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

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

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

(См. Ответ 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, а второй - настраиваемый подкласс UIViewController SecondVC.

На обоих контроллерах представления есть текстовое поле, и оба загружают значение из 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, чтобы ваш пользовательский интерфейс обновлялся каждый раз, когда отображается контроллер представления.

Дункан С
источник
8
Я не хочу голосовать против этого, потому что я думаю, что это прекрасно, что вы потратили время на создание вопроса и ответа в качестве ресурса. Спасибо. Несмотря на это, я думаю, что мы оказываем большую медвежью услугу новым разработчикам, защищая синглтоны для объектов модели. Я не в «одиночек лукавы» лагеря (хотя недоносков должен Google эту фразу , чтобы лучше понять проблемы), но я думаю , что модель данных является сомнительной / спорно использование одиночек.
Роб
хотелось бы увидеть
отличную статью
@Duncan C Привет, Дункан Я создаю статический объект в каждой модели, поэтому я получаю данные из любого места, где это правильный подход, или я должен следовать вашему пути, потому что это кажется очень правильным.
Вирендра Сингх Ратор
@VirendraSinghRathore, Глобальные статические переменные - это наихудший способ обмена данными в приложении. Они плотно связывают части вашего приложения вместе и создают серьезные взаимозависимости. Это полная противоположность «очень правильно».
Duncan C
@DuncanC - будет ли этот шаблон работать для объекта CurrentUser - в основном для одного пользователя, который вошел в ваше приложение? thx
timpone
9

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
        }

    }
Команда iOS
источник
Для Googlers , которые , как целиком и полностью потерянной, куда поместить фрагменты кода Swift StackOverflow ответов , как я, как это , кажется , предполагается , что вы всегда должны знать , где они вывести код идет: я использовал вариант 1) , чтобы отправить от ViewControllerAдо ViewControllerB. Я просто вставил фрагмент кода в нижнюю часть своего файла ViewControllerA.swift(где ViewControllerA.swift, конечно, называется то, что называется вашим файлом) прямо перед последней фигурной скобкой. " prepare" на самом деле является специальной встроенной ранее существовавшей функцией в данном классе [которая ничего не делает], поэтому вы должны " override" ее
velkoon
8

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

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

Дункан С
источник
2
Обратите внимание, что использование центра уведомлений приводит к слишком слабой связи. Это может очень затруднить отслеживание хода выполнения вашей программы, поэтому использовать его следует с осторожностью.
Duncan C
2

Вместо создания синглтона контроллера данных я бы предложил создать экземпляр контроллера данных и передать его. Для поддержки внедрения зависимостей я бы сначала создал 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мы можем установить 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:

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

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

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

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

Кристиина
источник
1
Здравствуйте, это чистый, проверяемый подход, который я использую большую часть времени в небольших приложениях, но в более крупных, где не каждому VC (возможно, даже корневому VC) может потребоваться зависимость (например, DataController в этом случае) Кажется расточительным для каждого венчурного капитала требовать, чтобы зависимость просто передавала его. Кроме того, если вы используете разные типы VC (например, обычный UIVC против NavigationVC), вам необходимо создать подкласс этих различных типов, чтобы добавить эту переменную зависимости. Как вы к этому подойдете?
RobertoCuba
1

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

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

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

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

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

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

Дункан С
источник
1

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

    }
}
Максим
источник
никогда не делай self.delegate = self
malhal
1

Это зависит от того, когда вы хотите получить данные.

Если вы хотите получать данные в любое время, можете использовать одноэлементный шаблон. Класс шаблона активен во время выполнения приложения. Вот пример шаблона 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")
}
Юсуф
источник