[Перевод] Как передавать данные между вью контроллерами в Swift
Если ваше приложение имеет много пользовательских интерфейсов (UI), Вам наверняка потребуется передать данные из одного UI в другой. В этой статье вы найдете ответ как настроить общение между вью контроллерами в Swift.
Передача данных между вью контроллерами — важная часть iOS разработки. Для этого можно использовать несколько способов и все они имею свои преимущества и недостатки. На возможность легко передавать данные между вью контроллерами влияет выбранная архитектура приложения. Архитектура приложения влияет на то, как Вы работаете с вью контроллерами и наоборот.
В этом туториале будут разобраны шесть различных методов передачи данных между вью контроллерами, включая работу со свойствами, переходами и NSNotificationCenter. Начнем с самого простого подхода, а затем перейдем к более сложным практикам.
Оглавление
Передача данных между вью контроллерами с помощью Properties (Свойств) (A → B)
Передача данных между вью контроллерами с помощью Segues (Переходов) (A → B)
Передача данных обратно с помощью Properties (Свойств) и Functions (Функций) (A ← B)
Передача данных обратно с помощью Delegation (Делегирования)
Передача данных обратно с помощью Closure (Замыканий)
Передача данных между вью контроллерами с помощью NotificationCenter
Заключение
Передача данных между вью контроллерами с помощью Properties (Свойств) (A → B)
В Swift можно передать данные между вью контроллерами шестью способами:
Используя instance property (свойство экземпляра) (A → B)
Используя segues (переходы) в Storyboard
Используя свойства и функции (A ← B)
Используя паттерн делегирования
Используя замыкания или completion handler
Используя NotificationCenter и Observer pattern
Как вскоре станет понятно, некоторые из этих подходов являются односторонними, поэтому передача данных осуществляется только в одну сторону, но не наоборот. Они, так сказать, не двунаправленные. Но в большинстве случаев нам будет нужна только односторонняя коммуникация!
Простейший способ передачи данных из вью контроллера А во вью контроллер В (вперед) — это использование свойств (properties).
Свойство — это переменная, которая является частью класса. Каждый экземпляр класса будет иметь свойство, которому можно присвоить значение. Вью контроллеры типа UIViewController также имеют свойства как любой другой класс.
Ниже пример вью контроллера MainViewController со свойством text:
class TertiaryViewController: UIViewController
{
var username:String = "”
@IBOutlet weak var usernameLabel:UILabel?
override func viewDidLoad()
{
super.viewDidLoad()
usernameLabel?.text = username
}
}
Ничего особенного — здесь просто устанавливается лейбл usernameLabel с текстом из свойства username.
Свойства — это одни из самых простых способов передачи данных между вью контроллерами. Они особенно полезны, когда передача данных осуществляется только в одну сторону, из исходного вью контроллера ко вью контроллеру, к которому осуществляется переход.
При использовании свойств для передачи данных:
Инициализация: убедитесь, что свойства вью контроллера, к которому осуществляется переход, инициализированы перед переходом. Это можно сделать в prepare (for: sender:) методе базового вью контроллера.
Безопасность: всегда проверяйте значение на nil, когда получаете доступ, к свойствам вью контроллера, к которому осуществляется переход. Использование optional binding (if let или guard let) поможет безопасно развернуть эти значения.
Типы данных: Свойства могут иметь любой тип данных, включая кастомные объекты. Убедитесь, что тип данных свойства совпадает с типом данных, которые Вы хотите передать.
Производительность: Передача данных с использованием свойств эффективна, поскольку не требует каких-либо дополнительных затрат, таких как сериализация или операции с БД. Однако будьте осторожны при передаче больших наборов данных, поскольку это может повлиять на использование памяти.
Итак, для передачи данных из MainViewController в TertiaryViewController можно использовать специальную функцию prepare (for: sender:). Этот метод вызывается перед переходом, поэтому его можно настроить.
Вот код перехода в действии:
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.destination is TertiaryViewController {
let vc = segue.destination as? TertiaryViewController
vc?.username = "Arthur Dent”
}
}
И вот что произошло:
Во-первых, с помощью оператора if и ключевого слова is проверяется, относится ли переход к классу TertiaryViewController. Вам нужно определить, является ли этот переход тем, который Вы хотите настроить, потому что все они проходят через функцию prepare (for: sender:)
Затем, простым приведением типов (кастингом) segue.destination к TertiaryViewController мы получаем доступ к свойству username.
И наконец, свойство username устанавливается, как показано в примере.
Самое забавное в функции prepare (for: sender:) то, что на этом больше ничего не нужно делать. Функция просто подключается к переходу, но ей не нужно сообщать, чтобы она продолжила переход. Также не нужно возвращать настроенный вью контроллер.
Приведенный выше пример кода можно улучшить следующим образом:
if let vc = segue.destination as? TertiaryViewController {
vc.username = "Ford Prefect”
}
Вместо использования ключевого слова is для проверки типа перехода и дальнейшего кастинга теперь можно делать это за один раз с опциональным кастингом. Когда segue.destination не соответствует типу TertiaryViewController выражение as? возвращает nil и поэтому условие не выполняется. Easy-peasy!
Если Вы не хотите использовать приведение типов, вместо этого можно использовать свойство segue.identifier. Установите идентификатор «tertiaryVC» в сториборде, а после используйте:
if segue.identifier == "tertiaryVC” {
// Do stuff…
}
Итак…Это все что нужно для передачи данных между вью контроллерами с использованием переходов (segues) и сториборда!
Для многих приложений сториборд ограничивает возможность использования различных переходов между вью контроллерами. Сториборд зачастую слишком усложняет создание интерфейсов в Xcode, но с очень небольшими бенефитами. Кроме того, Interface Builder становится медленным и тормозит, если в проекте есть сложные сториборды или XIB-ы.
Все, что Вы делаете с помощью сториборда, можно также сделать с помощью кода с гораздо большим контролем и минимальными дополнительными усилиями. Это не значит, что про сториборд нужно забыть и верстать только кодом! Используйте один XIB на контроллер и сабклассы, такие как UITableViewCell.
В конечном счете, как разработчик, Вы самостоятельно решите, что Вам больше нравится — табуляция или пробелы, сториборд или XIB-ы, Core Data или Realm — решать Вам!
Интересный факт: Каждый разработчик по-своему произносит слово «segue». Некоторые произносят se- как «say» или «set», а -gue как «gue_rilla», другие просто произносят это как _seg-way (например, Segway — гироскутер с рулем).
Передача данных обратно с помощью Properties (Свойств) и Functions (Функций) (A ← B)
А теперь… что, если необходимо вернуть данные из дочернего вью контроллера в главный вью контроллер?
Передача данных между вью контроллерами с использованием свойств дочернего вью контроллера, как описано в первой главе, довольно проста. Следующий вопрос — как передать данные обратно из второго вью контроллера в первый? Это можно сделать несколькими способами.
Сценарий следующий:
Пользователь приложения перешел из вью контроллера А в дочерний вью контроллер В.
В дочернем вью контроллере пользователь взаимодействует с фрагментом данных, и необходимо вернуть эти данные обратно во вью контроллер А.
Другими словами: вместо передачи данных от A → B нужно вернуть данные из В → А.
Простейший способ вернуть данные обратно — создать ссылку на вью контроллер А внутри вью контроллера В, и затем вызвать функцию из вью контроллера А внутри вью контроллера В.
Теперь дочерний вью контроллер выглядит так:
class SecondaryViewController: UIViewController
{
var mainViewController:MainViewController?
@IBAction func onButtonTap()
{
mainViewController?.onUserAction(data: "The quick brown fox jumps over the lazy dog”)
}
}
Затем эта функция добавляется в MainViewController:
func onUserAction(data: String)
{
print("Data received: \(data)”)
}
Когда вью контроллер перемещается в navigation stack, как и в предыдущих примерах, устанавливается соединение между главным вью контроллером и дочерним вью контроллером.
let vc = SecondaryViewController(nibName: "SecondaryViewController”, bundle: nil)
vc.mainViewController = self
В приведенном выше примере self присваивается свойству mainViewController. Теперь дочерний вью контроллер «знает» о главном вью контроллере, и поэтому он может вызвать любую из его функций — например, onUserAction (data:).
Передача данных обратно, используя свойства и функции, — это прямой подход и важно знать о его последствиях:
Связь. Этот метод вводит тесную связь между двумя вью контроллерами. Хотя это и просто, такой подход может сделать код менее модульным и более сложным в больших проектах.
Управление памятью. Будьте осторожны с retain cycles, когда используете этот подход. Если оба вью контроллера имеют сильные ссылки друг на друга, они могут быть не освобождены из памяти и, как следствие, это приведет к утечкам памяти. Использование слабых ссылок поможет избежать этого.
Целостность данных. Убедитесь, что переданные обратно данные безопасны и валидны, особенно если они используются для критических операций или для UI.
Вызовы функций. При использовании функций для передачи данных обратно убедитесь, что функция в главном вью контроллере подготовлена к обработке нескольких вызовов или для обработки непредвиденных данных, так как дочерний вью контроллер может запускать эту функцию при различных сценариях.
Вот и все, что нужно знать. Но… Это не самый лучший подход к передаче данных. У него есть несколько существенных недостатков:
Теперь MainViewController и SecondaryViewController тесно связаны между собой. Как правило, таких связей нужно избегать при разработке, в основном, потому что это снижает модульность кода. Оба класса становятся слишком запутанными и полагаются друг на друга, чтобы функционировать должным образом, что приводит к «макаронному коду».
В приведенном выше примере кода создается зацикливание (retain cycle). Дочерний вью контроллер не может быть удален из памяти, пока не удален главный вью контроллер, но главный вью контроллер не может быть удален из памяти, пока дочерний вью контроллер не будет удален. (Решением будет использование weak свойств).
Два разработчика не могут легко работать отдельно над MainViewController и SecondaryViewController, поскольку работа вью контроллеров завязана друг на друге. Нет никакого разделения интересов.
Полагаю, Вы захотите избежать подобных сценариев с прямыми ссылками на классы, экземпляры и функции. Поддержание такого кода становится просто кошмаром. Это часто приводит к «макаронному коду», когда при изменении одного фрагмента кода ломается другой, казалось бы, несвязанный фрагмент кода…
Итак, какая идея лучше? Делегирование!
Совет: если нужно передать переменные, которые будут использоваться в нескольких вью контроллерах, не создавайте отдельно свойства в каждом из них. Вместо этого создайте структуру или класс (модель), которая обернет все данные и передайте экземпляр этого класса в одном свойстве.
Передача данных обратно с помощью делегирования
Делегирование — важный и часто используемый паттерн проектирования в iOS SDK. Критически важно его понимать, если Вы разрабатываете iOS приложения!
Благодаря делегированию базовый класс может передать часть функциональности дочернему классу. Затем разработчик может реализовать этот дочерний класс и реагировать на события из базового класса, используя протокол. Что приводит к разделению ответственности!
Вот небольшой пример:
Представьте, что Вы работаете в пиццерии. У Вас есть повар, который готовит пиццу.
Ваши посетители могут делать с пиццей, все что захотят: съесть ее, положить в морозилку или поделиться пиццей с друзьями.
Раньше повар заботился о том, что посетители делают со своей пиццей, но теперь он отделился от процесса поедания пиццы и просто выдает Вам пиццу, когда она готова. Он делегирует процесс «обработки» пиццы и занимается только выпечкой пиццы!
Прежде чем Вы с поваром сможете понять друг друга, необходимо определить протокол:
protocol PizzaDelegate {
func onPizzaReady(type: String)
}
Протокол — это соглашение о том, какие функции должен реализовывать класс, если он хочет соответствовать протоколу. Его можно добавить в класс следующим образом:
class MainViewController: UIViewController, PizzaDelegate
{
Теперь в этом определении класса сказано:
Класс назван MainViewController
Класс является наследником UIViewController
Класс реализует протокол PizzaDelegate
Для соответствия протоколу необходимо его реализовать. Для этого добавим функцию делегата в MainViewController:
func onPizzaReady(type: String)
{
print("Pizza ready. The best pizza of all pizzas is… \(type)”)
}
При создании дочернего вью контроллера, ему необходимо присвоить делегат:
vc.delegate = self
Итак, вот ключевой аспект делегирования. Теперь необходимо добавить свойство и код, который должен делегировать функциональность.
Во-первых, свойство:
weak var delegate:PizzaDelegate?
Затем код:
@IBAction func onButtonTap()
{
delegate?.onPizzaReady(type: "Pizza di Mama”)
}
Допустим функция onButtonTap () вызывается, когда повар заканчивает готовить пиццу. Затем он вызывает onPizzaReady (type:) в делегате.
Повару все равно, есть делегат или его нет. Если делегата нет, пиццу просто выбрасывают. Если делегат на месте, то повар только отдает ему пиццу — Вы можете делать с ней все, что хотите!
Итак, давайте посмотрим на ключевые компоненты делегирования:
Вам нужен протокол делегата.
Вам нужно свойство делегата.
Класс, которому Вы хотите делегировать данные, должен соответствовать этому протоколу.
Класс, из которого Вы хотите делегировать, должен вызвать функцию, определенную протоколом.
Чем это отличается от предыдущего примера с передачей данных обратно через свойства?
Разработчикам, которые работают над отдельными классами, нужно только соответствовать протоколу и его функциям. Они могут работать раздельно, не мешая при этом друг другу.
Нет прямой связи между главным вью контроллером и дочерним вью контроллером, а это значит, что они лучше разделены, чем в предыдущем примере.
Протокол может быть реализован любым классом, а не только в MainViewController.
Превосходно! А теперь давайте посмотрим на другой пример… использование замыканий.
Почему свойство делегата объявлено как weak? Ответ здесь: Automatic Reference Counting (ARC) в Swift.
Передача данных обратно с помощью замыканий
Использование замыканий для передачи данных между вью контроллерами мало чем отличается от использования свойств или делегирования.
Самое большое преимущество использования замыканий заключается в том, что они относительно просты в использовании и их можно объявить локально — нет необходимости использовать функции или протоколы.
Начать следует с создания свойства в дочернем вью контроллере:
var completionHandler: ((String) -> Int)?
Здесь completionHandler имеет тип замыкания, оно объявлено как опционал, а также имеет один параметр типа String и возвращает одно значение типа Int.
Еще раз, в дочернем вью контроллере мы вызываем замыкание по нажатию кнопки:
@IBAction func onButtonTap()
{
let result = completionHandler?("FUS-ROH-DAH!!!”)
print("completionHandler returns… \(result)”)
}
В приведенном выше примере происходит следующее:
Замыкание completionHandler вызывается с одним аргументом типа String. Его результат присваивается результату.
Результат выводится в print ().
Затем в MainViewController определяем замыкание следующим образом:
vc.completionHandler = { text in
print("text = \(text)”)
return text.characters.count
}
Замыкание объявлено локально, поэтому можно использовать все локальные переменные, свойства и функции.
В этом замыкании параметр text выводится в print (), а затем длина строки возвращается в качестве результата выполнения функции.
Вот здесь становится интересно. Замыкание позволяет передавать данные между вью контроллерами в двух направлениях! Таким образом можно определить замыкание, работать с поступающими в него данными и возвращать данные в код, который вызывает замыкание.
Здесь можно отметить, что вызов функции с помощью делегирования или напрямую через свойство также позволяет вернуть значение в вызывающую функцию.
Замыкания могут пригодиться в следующих случаях:
Вам не нужен комплексный подход паттерна делегирования с протоколом и т.д., а нужна просто «быстрая функция».
Вы хотите передать замыкание через несколько классов. Без замыкания пришлось бы создавать каскадный набор вызовов функций, но с замыканием можно просто передать блок кода напрямую.
Вам нужно локально определить блок кода с замыканием, потому что данные существуют только локально.
Один из рисков использования замыканий для передачи данных между вью контроллерами заключается в том, что код может стать очень громоздким. Разумнее всего использовать замыкания для передачи данных между вью контроллерами только в том случае, если есть смысл использовать замыкания вместо любого другого метода — вместо того, чтобы просто использовать замыкания, потому что они очень удобны!
Итак… что, если нужно передавать данные между вью контроллерами, которые не связаны между собой?
Передача данных между вью контроллерами с помощью NotificationCenter
Передача данных между вью контроллерами также возможна с помощью Центра Уведомлений, через его класс NotificationCenter
Центр Уведомлений обрабатывает уведомления и пересылает входящие уведомления его слушателями. Центр уведомлений — это подход iOS SDK к паттерну проектирования Observer-Observable.
Примечание: Начиная с Swift 3, он называется NotificationCenter, поэтому префикса «NS» нет. И имейте в виду, что эти «уведомления» не являются push-notifications.
Работа с Центром Уведомлений состоит из трех ключевых компонентов:
Наблюдение за уведомлением
Отправка уведомления
Ответ на уведомление
Давайте сначала начнем с наблюдения за уведомлением. Прежде чем мы сможем ответить на уведомление, необходимо сообщить Центру Уведомлений, что мы за ним наблюдаем. Затем Центр Уведомлений сообщает обо всех уведомлениях, с которыми он сталкивается, поскольку мы указали, что ищем их.
Каждое уведомление имеет имя, позволяющее его идентифицировать. В MainViewController следует добавить следующее статическое свойство в начало класса:
static let NotificationName = Notification.Name("myNotificationName")
Это статическое свойство, также известное как свойство класса, доступно в любом месте кода путем вызова MainViewController.notificationName. Вот так просто можно идентифицировать уведомление с помощью всего одной константы и не перепутать свои уведомления, опечатавшись где-нибудь!
Вот как можно наблюдать за этим уведомлением:
NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: MainViewController.notificationName, object: nil)
Обычно это добавляется во viewDidLoad () или viewWillAppear (_:), чтобы наблюдение уже было зарегистрировано, когда вью контроллер появляется на экране. Вот что происходит в вышеуказанном примере кода:
Здесь используется NotificationCenter.default, который является Центром уведомлений по умолчанию. Также можно создать свой собственный Центр Уведомлений, например, для определенных видов уведомлений, но, скорее всего, подойдет и центр по умолчанию.
Затем вызывается функция addObserver (_: selector: name: object:) в Центре Уведомлений.
Первый аргумент — это экземпляр, который выполняет наблюдение, и почти всегда это self.
Второй аргумент — это селектор, который вызывается при обнаружении уведомления, и часто это функция текущего класса.
Третий параметр — это имя уведомления, поэтому туда передается статическая константа notificationName.
Четвертый параметр — это объект за которым происходит наблюдение. Обычно здесь передается nil, но его можно использовать для наблюдения только за уведомлениями от одного конкретного объекта.
Позже можно остановить просмотр уведомления следующим образом:
NotificationCenter.default.removeObserver(self, name: MainViewController.notificationName, object: nil)
Также можно остановить наблюдение за всеми зарегистрированными уведомлениями с помощью:
NotificationCenter.default.removeObserver(self)
Помните, что уведомления являются явными, поэтому наблюдение всегда происходит за одним типом уведомления, что приводит к вызову одной функции на одном объекте (обычно self) при возникновении уведомления.
Функция, которая будет вызываться для получения уведомления, — это onNotification (notification:), поэтому давайте добавим ее в класс:
@objc func onNotification(notification:Notification)
{
print(notification.userInfo)
}
Ключевое слово @objc требуется начиная с Swift 4, поскольку фреймворк NSNotificationCenter представляет собой код Objective-C. В этой функции просто распечатывается полезная информация уведомления с помощью Notification.userInfo.
Теперь опубликовать уведомление будет просто. Вот как это сделать:
NotificationCenter.default.post(name: MainViewController.notificationName, object: nil, userInfo: ["data": 42, "isImportant": true])
Опять же, здесь есть несколько составляющих:
В этом примере вызывается функция post (name: object: userInfo:) в Центре Уведомлений по умолчанию, точно в том же центре, который использовался раньше.
Первый аргумент функции — это имя уведомления, та статическая константа, которая была определена ранее.
Второй аргумент — это объект, отправляющий уведомление. Часто здесь можно оставить nil, но, если ранее сюда был передан объект, здесь его можно использовать, чтобы наблюдать и публиковать сообщения исключительно для этого объекта.
Третий аргумент — это полезная информация userInfo. Вы можете передать словарь с любыми данными. В этом примере — это логическое значение и some data.
Вот и все!
Центр Уведомлений пригодится в следующих случаях:
Вью контроллеры или другие классы, между которыми осуществляется передача данных, не связаны тесно между собой. Примером может служить тэйбл вью контроллер, который должен реагировать всякий раз, когда REST API получает новые данные.
Вью контроллеры еще не обязательно должны существовать. Может случиться так, что REST API получит данные до того, как тэйбл вью появится на экране. Наблюдение за уведомлениями является не обязательным, что является преимуществом, если части приложения являются эфемерными.
Множество вью контроллеров должны реагировать на одно уведомление или один контроллер должен реагировать на несколько уведомлений. Центр Уведомлений работает по принципу «many-to-many».
Вы можете представить себе Центр Уведомлений как автомагистраль для информации, где уведомления постоянно отправляются по ее полосам, в разных направлениях и конфигурациях.
Если Вам просто нужен какой-то «локальный трафик» между вью контроллерами, то использование Центра Уведомлений не имеет смысла — вместо этого используйте простой делегат, свойства или замыкания. Но если Вы хотите неоднократно и регулярно отправлять данные из одной части приложения в другую, Центр Уведомлений — отличное решение.
Заключение
Передача данных между вью контроллерами — фундаментальный аспект разработки iOS, обеспечивающий удобство взаимодействия с пользователем и динамическое взаимодействие внутри приложений. Независимо от того, используете ли Вы свойства, переходы, делегирование или другие методы, крайне важно выбрать тот подход, который лучше всего соответствует конкретным потребностям Вашего приложения.
В заключение, хотя механика передачи данных может показаться простой, решение о выборе метода использования может существенно повлиять на надежность и эффективность приложения. Всегда тщательно тестируйте, учитывайте пользовательский опыт и стремитесь к чистому и поддерживаемому коду.