Як ви ділитесь даними між контролерами перегляду та іншими об’єктами в Swift?


88

Скажімо, у моєму додатку Swift є кілька контролерів перегляду, і я хочу мати можливість передавати дані між ними. Якщо я перебуваю на декількох рівнях у стеку контролера перегляду, як передати дані іншому контролеру перегляду? Або між вкладками в контролері перегляду панелі вкладок?

(Зверніть увагу, це питання є "дзвіночком".) Його запитують так багато, що я вирішив написати підручник з цього питання. Дивіться мою відповідь нижче.


1
Спробуйте погугліти для делегатів
milo526

4
Я опублікував це, щоб міг надати рішення 10 000 випадків цього питання, які щодня з’являються тут, у SO. Дивіться мою самовідповідь. :)
Duncan C

Вибачте, я занадто швидко реагував :) добре, щоб мати змогу посилатися на це :)
milo526

2
Без турбот. Ви думали, що я №10001, правда? <grin>
Duncan C

4
@DuncanC Мені не подобається ваша відповідь. :( Це нормально-isn, як загальна відповідь на кожен сценарій відповіді ... так що, це буде працювати для кожного сценарію, але це також не правильний підхід майже для будь-якого сценарію. Незважаючи на це, ми зараз це зрозуміли що позначати будь-яке питання по темі як дублікат цього - це гарна ідея? Будь ласка, не варто.
nhgrif

Відповіді:


91

Ваше запитання дуже широке. Припускати, що існує одне просте загальне рішення для кожного сценарію, трохи наївно. Отже, давайте розглянемо деякі з цих сценаріїв.


Найпоширеніший сценарій, про який запитують щодо переповнення стека, на мій досвід, - це просте передавання інформації від одного контролера перегляду до іншого.

Якщо ми використовуємо раскадровку, наш перший контролер подання може замінити 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
}

Є ще кілька сценаріїв. Ця відповідь аж ніяк не охоплює всіх.


1
Я також додам, що іноді ви хочете, щоб канал надсилав інформацію назад із контролера перегляду призначення на контролер перегляду джерела. Поширеним способом вирішення цієї ситуації є додавання властивості делегата до пункту призначення, а потім у контролері вихідного подання pripremaForSegue встановіть для властивості делегата контролера подання призначення значення self. (і визначити протокол, який визначає повідомлення, які цільовий ВК використовує для надсилання повідомлень вихідному ВК)
Duncan C

1
nhgrif, я згоден. Порада новим розробникам повинна полягати в тому, що якщо вам потрібно передавати дані між сценами на раскадровці, використовуйте prepareForSegue. Шкода, що це дуже просте спостереження втрачається серед інших відповідей та відступів тут.
Роб

2
@Rob Yup. Одиночні та сповіщення повинні бути останнім вибором. Ми повинні віддавати перевагу prepareForSegueчи іншим прямим передачам інформації майже в кожному сценарії, а потім просто поводитися з новачками, коли вони з’являються зі сценарієм, для якого ці ситуації не працюють, і тоді нам доведеться навчати їх про ці більш глобальні підходи.
nhgrif

1
Це залежить. Але я дуже, дуже стурбований використанням делегата програми як нашої сміттєвої бази для коду, який ми не знаємо, куди ще покласти. Тут лежить шлях до божевілля.
nhgrif

2
@nhgrif. thx за вашу відповідь. що, якщо ви хочете, щоб дані передавалися між, скажімо, 4 або 5 контролерами перегляду. якщо я отримав, скажімо, 4-5 контролерів перегляду, які керують входом і паролем клієнта і т.д., і я хочу передати електронну пошту користувача між цими контролерами перегляду, чи є більш зручний спосіб зробити це, ніж оголошувати var у кожному контролері огляду, а потім передавати його в систему підготовки до мови. чи є спосіб, який я можу оголосити один раз, і кожен контролер перегляду може отримати до нього доступ, але таким чином, що також є хорошою практикою кодування?
lozflan

45

Це питання постійно виникає.

Однією з пропозицій є створення єдиного контейнера даних: об’єкт, який створюється один раз і лише один раз у житті вашої програми та зберігається впродовж життя вашої програми.

Цей підхід добре підходить для ситуації, коли у вас є глобальні дані програми, які повинні бути доступними / модифікованими для різних класів у вашій програмі.

Інші підходи, такі як встановлення односторонніх або двосторонніх зв’язків між контролерами перегляду, краще підходять для ситуацій, коли ви передаєте інформацію / повідомлення безпосередньо між контролерами перегляду.

(Див. Відповідь 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'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. Ви також можете використовувати NSCodingCore Data або різні інші методи для збереження даних стану.

Метод DataContainerSingleton initтакож намагається завантажити збережені значення для його властивостей.

Ця частина методу 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
Я не хочу голосувати за це, тому що вважаю чудовим, що ви витратили час на створення запитання та відповіді як ресурсу. Дякую. Незважаючи на це, я думаю, ми робимо велику погану послугу новим розробникам, щоб захищати одинаки для модельних об'єктів. Я не в таборі "одинокі - це зло" (хоча noobs повинні шукати в Google цю фразу, щоб краще оцінити проблеми), але я думаю, що дані моделей є сумнівним / дискусійним використанням одиноких.
Роб

я хотів би побачити чудовий запис, як ваш, про двосторонні посилання
Cmag

@Duncan C Привіт, Дункане. Я створюю статичний об'єкт у кожній моделі, тому отримую дані з будь-якого місця, де це правильно, або я повинен йти за вашим шляхом, тому що це здається дуже правильним.
Вірендра Сінгх Ратхоре

@VirendraSinghRathore, Глобальні статичні змінні є найгіршим можливим способом обміну даними в додатку. Вони міцно з’єднують частини вашого додатка та створюють серйозну взаємозалежність. Це прямо протилежність "дуже правильно".
Duncan C

@DuncanC - чи буде цей шаблон працювати для об'єкта CurrentUser - в основному одного користувача, який увійшов у ваш додаток? thx
timpone

9

Стрімкий 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
        }

    }

Для співробітників Google, які так само і повністю загублені щодо того, куди покласти відповіді StackOverflow: «Свіфт-фрагменти коду, як я, як здається, передбачається, що ви завжди повинні знати, куди вони роблять висновок про те, що робить код: я використовував варіант 1) для відправки з ViewControllerAдо ViewControllerB. Я просто закріпив фрагмент коду внизу мого ViewControllerA.swift(де ViewControllerA.swift, власне, те, що називається ваш файл, звичайно) безпосередньо перед останньою фігурною дужкою. " prepare" насправді є спеціальною вбудованою попередньо існуючою функцією у даному класі [яка нічого не робить], саме тому вам це потрібно " override"
velkoon

8

Інша альтернатива - використання центру сповіщень (NSNotificationCenter) та розміщення повідомлень. Це дуже вільна муфта. Відправник сповіщення не повинен знати або дбати про те, хто слухає. Він просто розміщує повідомлення і забуває про нього.

Повідомлення корисні для передачі повідомлення "один до багатьох", оскільки може бути довільна кількість спостерігачів, які прослуховують дане повідомлення.


2
Зверніть увагу, що використання центру сповіщень вводить зв’язок, який, можливо, занадто вільний. Це може дуже ускладнити відстеження потоку вашої програми, тому їй слід користуватися обережно.
Duncan C

2

Замість створення singelton контролера даних я б запропонував створити екземпляр контролера даних і передати його. Для підтримки введення залежностей я спочатку створив би 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
Привіт, цей підхід чистий, перевіряється, і те, що я використовую більшу частину часу в невеликих програмах, але у більших, де не кожному ВК (можливо, навіть не кореневому ВК) може знадобитися залежність (наприклад, DataController у цьому випадку). здається марнотратним для кожного ВК вимагати залежності просто для її передачі. Крім того, якщо ви використовуєте різні типи ВК (наприклад, звичайний UIVC проти NavigationVC), вам потрібно підкласувати ці різні типи, лише щоб додати цю змінну залежності. Як ви підходите до цього?
RobertoCuba

1

Як зазначив @nhgrif у своїй чудовій відповіді, існує безліч різних способів, за допомогою яких VC (контролери перегляду) та інші об'єкти можуть взаємодіяти між собою.

Синглтон даних, який я виклав у своїй першій відповіді, насправді більше стосується спільного використання та збереження глобального стану, ніж безпосереднього спілкування.

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

Насправді ви можете встановити активний односторонній або двосторонній канал між різними контролерами перегляду. Якщо контролери подання пов’язані за допомогою розкадрування segue, час налаштування посилань вказаний у методі prepaForFor Segue.

У мене є зразок проекту на Github, який використовує батьківський контролер подання для розміщення 2 різних подань таблиці як дочірні. Дочірні контролери подання пов’язані за допомогою вбудованих сегментів, а батьківський контролер подання підключає двосторонні посилання до кожного контролера подання в методі prepaForSegue.

Ви можете знайти цей проект на 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

Це залежить від того, коли ви хочете отримати дані.

Якщо ви хочете отримувати дані, коли завгодно, можете використати одношаровий шаблон. Клас шаблону активний під час роботи програми. Ось приклад одиночного візерунка.

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")
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.