Правильная передача данных

Туториалы

Правильная передача данных

Доброго времени суток, друзья!

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

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

В этой статье я покажу вам такие практики передачи данных:

1. Вперед

  • между ViewController с использованием segues
  • между ViewController без segues

2. Назад

  • передача через unwind segue
  • передача при помощи делегата

3. Продвинутые

  • обратная передача при помощи замыканий (closures)

4. А также расскажу о неправильных техниках

И так поехали!

Передача данных вперед

Передача данных происходит каждый раз, когда на экране появляется новый ViewController.

Это может произойти через segue или программно.

Передача данных вперед между ViewController с использованием segues

Создаем 2 контроллера в сториборде: FirstViewController и SecondViewController.

В FirstViewController добавляем кнопку, и от кнопки перетаскиваем segue на SecondViewController. И даем имя segue (это очень важно!).

В SecondViewController создаем UILabel, привязываем наш @IBOutlet и создаем переменную с именем name. И подготовка окончена.

Главный метод, с которым Вы будете работать - это prepare(for segue:).
Всегда нужно проверять соответствует ли segue.identifier названию вашего segue, (guard segue.identifier == “showSegue”) который мы задавали ранее. Если все в порядке, то мы идем дальше и устанавливаем destination, наш “пункт назначения”.
Берем наш segue вызываем destination и обязательно кастим до нужного ViewController, иначе ничего не получиться (guard let destination = segue.destination as? SecondViewController) И если Вы все правильно сделали, то написав destination, Вы получите все свойства и методы SecondViewController. Давайте передадим в переменную name имя Андрей (destination.name = “Aндрей”).
Готово! Теперь при нажатии на кнопку, мы будет передавать имя в следующий контроллер!

class FirstViewController: UIViewController {

    @IBOutlet var username: UILabel!
      
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard segue.identifier == "showSecond" else { return }
        guard let destination = segue.destination as? SecondViewController else { return }
        destination.name = "Андрей"
    }
}
class SecondViewController: UIViewController {

    var name = ""
    
    @IBOutlet var username: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        username.text = name
    }
}

Заметка

Всегда правильно задавайте идентификатор для segue. Хорошей практикой будет называть идентификатор тем, что он делает. Потому что может быть ситуации когда Вам нужно передать 2 и более segue в один контроллер. В дальнейшем это поможет свободно ориентироваться в проекте.

Передача данных вперед между ViewController без segues

Иногда вы можете подключить ViewController программно, а не использовать segue.

Первый пример:
Создаем создаем свойство storyboard, где name - это имя storyboard, в котором находится необходимый ViewController (let storyboard = UIStoryboard(name: “Main”, bundle: nil).

Далее создаем необходимый ViewController. Для этого используем метод instantiateViewController(identifier: String) и обязательно кастим до требуемого SecondViewController (guard let secondViewController = storyboard.instatiateViewController(identifier: “SecondViewController”) as? SecondViewController else { return })

Теперь мы также можем достучаться до переменной name и задать ей нужное значение. И так же не забываем вызвать метод show(vc: UIViewController, sender: Any?), который как раз и откроет нам данный контролер.

И готово! Так как в SecondViewController не поменялся код, показываем код FirstViewController:

class FirstViewController: UIViewController {
    
    @IBAction func passData() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        guard let secondViewController = storyboard.instantiateViewController(identifier: "SecondViewController") as? SecondViewController else { return }
        secondViewController.name = "Ivan"
        
        show(secondViewController, sender: nil)
    }
}

Как видите поскольку нет никакого segue, метод prepare (for: sender 🙂 также не вызывается.

Пример второй:

Если вы работаете с xib файлами, то можно тоже довольно легко и просто передавать данные.
Создаем ThirdViewController и оставляю галочку, чтобы с контроллером создался xib файл. Перетягиваем UILabel. Подготовка окончена.
В этом примере я покажу, не простую передачу данных, как мы делали, а через инициализатор.
Создаем свойство text: String, и создаем инициализатор к нему init(text: String). Так как это xib файл, то давайте добавим еще 2 свойства nibName: String?, bundle: Bundle?, это позволит нам инициализировать ThirdViewController через тот самый xib файл.
Дальше все по стандарту присуждаем self.text = text. И вызываем super.init(nibName: String, bundle: Bundle?), так как без инициализации “дизайна” контроллера ничего не получится. И исправляем ошибку с required init?(coder: NSCoder). Наш ThirdViewController - готов для получения данных!

class ThirdViewController: UIViewController {
    let text: String
    
    init(text: String, nibName: String?, bundle: Bundle?) {
        self.text = text
        super.init(nibName: nibName, bundle: bundle)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    @IBOutlet var textLabel: UILabel!
    
    override viewDidLoad() {
        super.viewDidLoad()

        textLabel.text = text
    }
}

Возвращаемся к нашему SecondViewController. Перетаскиваем кнопку и создаем @IBAction. В нем создаем экземпляр класса и инициализируем по нашему кастомному инициализатору, который мы создали ранее (let thirdVC = ThirdViewController(text: “Alexey”, nibName: “ThirdViewController”, bundle: nil). Прошу заметить, что nibName - это название xib файла, поэтому если вы создаете xib файл отдельно от контроллера, имейте это ввиду.
И после этого вызываем метод show(vc: UIViewController, sender: Any?).
Наш SecondViewController готов, для передачи данных.

class SecondViewController: UIViewController {

    var name = ""
    
    @IBOutlet var username: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        username.text = name
    }
    
    @IBAction func goToThirdVC() {
        let thirdVC = ThirdViewController(text: "Alexey", nibName: "ThirdViewController", bundle: nil)
        show(thirdVC, sender: nil)
    }
}

Передача данных в обратном направлении

Передача данных назад в приложении iOS так же важна, как и их перемещение вперед. Пользователи часто возвращаются к предыдущему экрану, который они посетили.

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

Передача данных в обратном направлении через unwind segue

Для этого примера создадим новые FirstViewController и SecondViewController.

Начинаем настраивать SecondViewController. Создаем UILabel и UIButton в сториборде и привызываем к контроллеру. В контроллере создаем переменную text. И добавляем метод prepare (for: sender 🙂, где меняем значение переменной text на “Data was passed”.

Теперь настраиваем FirstViewController. Так же создаем UILabel и UIButton в сториборде и привызываем к контроллеру. Перетягиваем от кнопки segue на SecondViewController. И создаем @IBAction для нашей кнопки, но с параметром (_ unwindSegue: UIStoryboardSegue), который и является главное фишкой данного примера.
Далее все по стандарту: делаем проверку на segue.identifier, и кастим наш SecondViewController. Только обратите внимание, что при получении данных вместо свойства destination, мы работаем с source. Таким образом мы сообщаем, что SecondViewController будет источником данных. Ну и присуждаем нашему UILabel переменную text из SecondViewController(textLabel.text = source.text).
И главное - это в сториборде в SecondViewController от нашего UIButton перетягиваем segue, не на FirstViewController, а на кнопку Exit, который находится рядом с кнопкой First Responder выше контролера. И не забываем про segue.identifier.
Готово! Вот примерный код, который должен у Вас получиться.

class FirstViewController: UIViewController {
    
    @IBOutlet var textLabel: UILabel!
    
    @IBAction func saveData(_ unwindSegue: UIStoryboardSegue) {
        guard unwindSegue.identifier == "passDataToFirstVC" else {
            return
        }
        guard let source = unwindSegue.source as? SecondViewController else { return }
        textLabel.text = source.text
    }
}
class SecondViewController: UIViewController {

    var text = ""
    
    @IBOutlet var textLabel: UILabel!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        text = "Data was passed"
    }
}

Передача данных в обратном направлении при помощи делегата

Иногда техники, которые Вы видели, все еще недостаточны.

Данные, которые вы хотите передать, могут быть временными и не принадлежать общему состоянию приложения.
Когда пользователь возвращается в UINavigationController, segue не запускается. Возможно, вы захотите, чтобы передача происходила в любой момент, а не только при переходе. Правильным решением будет делегирование.
Делегирование позволяет нам создать ссылку на предыдущий контроллер представления, не зная его типа. Мы делаем это с помощью протокола, который определяет интерфейс, с которым нам нужно взаимодействовать.
Создаем протокол FirstViewControllerDelegate и подписываем его под class (что означает AnyObjects). Это позволит в будущем создать слабую (weak) ссылку на протокол.
Любое свойство delegate должно быть слабым (weak), чтобы избежать сильных (strong) ссылочных циклов. Если вы не знаете, что это такое, то я советую прочитать статью для более подробной информации о слабых и сильных ссылках.

В протоколе создаем метод update с принимающим параметром text: String (update(text: String), который будет обновлять наш UILabel. Далее подписываем FirstViewController под протокол и выполняем этот метод (textLabel.text = text)

Далее переходим в SecondViewController, создаем ту самую слабую ссылку на протокол weak var delegate: FirstViewControllerDelegate и создаем @IBAction, где указываем, что должно произойти при нажатии этой кнопки. В нашем случаем будет происходить обновление текста на предыдущем контроллере без перехода назад (delegate?.update(text: “Text was changed”).

Если сейчас запустим приложение, нажмем кнопку и вернемся на предыдущий экран, то ничего не случится, так как мы не подписались на делегат, который находится в SecondViewController. Это частая ошибка всех программистов, поэтому если у Вас что-то не получается, проверяйте подписан ли принимающий контролер на делегат контроллера-отправителя.
Так как мы осуществляем переход на следующий контроллер через segue (при нажатии на UIButton), то подпишемся именно здесь. В методе prepare (for: sender 🙂 получаем destination к SecondViewController и именно там подписываемся на делегат (destination.delegate = self). Таким образом FirstViewController получает доступ к выполнению реализации протокола в SecondViewController.
Готово! Нам удалось получить данные обратно, независимо будем ли мы возвращаться на предыдущий экран или нет.
Весь код из примера, который должен получиться:

protocol FirstViewControllerDelegate: class {
    func update(text: String)
}

class FirstViewController: UIViewController, FirstViewControllerDelegate {

    @IBOutlet weak var textLabel: UILabel!
      
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let destination = segue.destination as? SecondViewController else { return }
        destination.delegate = self
    }
    
    func update(text: String) {
        textLabel.text = text
    }
}
class SecondViewController: UIViewController {

    weak var delegate: FirstViewControllerDelegate?
    
    @IBOutlet weak var textLabel: UILabel!
    
    @IBAction func changeDataInFirstVC() {
        delegate?.update(text: "Text was changed")
    }
}

Заметка

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

Для закрепления можете сами попробовать сделать это, взяв примеры из 1.2 Передача данных вперед между ViewController без segues.

Продвинутые техники

Замена делегирования на замыкания (closures) Swift

Некоторые разработчики используют замыкания (closures) Swift для передачи данных назад между ViewControllers. Этот метод похож на делегирование, но более гибкий. Это также причина, почему я обычно рекомендую не использовать его.

Используя замыкания, вы можете определить интерфейс через свойства хранения, содержащие замыкания. Поэтому создадим в SecondViewController переменную с названием closure: ((String) -> ())?.
В SecondViewController, для быстрого примера, вызовем в методе viewDidLoad то самое замыкание, и поместим в него текст (closure?(“I can pass data by closure!”)) Обратите внимание, что как и с делегатом, замыкание должно быть опциональное.

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

class FirstViewController: UIViewController {

    @IBOutlet var textLabel: UILabel!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let destination = segue.destination as? SecondViewController else { return }
        destination.closure = { [weak self] text in
            self?.textLabel.text = text
        }
    }
}
class SecondViewController: UIViewController {

    var closure: ((String) -> ())?
    
    @IBOutlet var textLabel: UILabel!
    
    @IBAction func changeDataInFirstVC() {
        closure?("I can pass data by closure")
    }
}

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

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

Если вам нужно более одного замыкания для связи с предыдущим контроллером представления, вам нужно сохранить свойство для каждого из них. При делегировании весь интерфейс выделяется внутри протокола, и вам нужно только одно свойство делегата.

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

Неправильные техники

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

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

Не используйте UserDefaults iOS

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

Все, что хранится в UserDefaults, остается там до тех пор, пока вы не удалите приложение из телефона, поэтому это не механизм для передачи данных между объектами.

Кроме того, вы можете хранить только простые типы данных в UserDefaults в форме списков свойств. Это означает, что вам нужно преобразовать любой пользовательский тип, прежде чем вы сможете поместить его туда.

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

Не используйте Notifications

Notifications в iOS дают вам канал, по которому какой-то код может отправлять сообщение другим объектам, на которые он не имеет прямой ссылки.

Я видел, как многие разработчики используют Notifications для передачи данных между контроллерами представления. Это не то, для чего они нужны!

Notifications создают косвенность и усложняют выполнение вашего кода. Когда вы публикуете notifications, вы не можете быть уверены, какие объекты получат его и в каком порядке. Это часто приводит к неожиданному поведению.
Notifications могут быть полезны иногда. Но вы должны использовать их экономно, и только когда это необходимо.

Выводы

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

Поэтому выбирайте правильный подход и двигайтесь дальше!

 

Автор статьи: Михаил Цейтлин

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: