Как передавать данные между View Controllers на Swift?

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

Возможность легко передавать данные между контроллерами представлений зависит от выбора архитектуры приложения . В этой статье мы изучим 6 различных способов передачи данных между контроллерами представления, включая работу со свойствами, segue и NSNotificationCenter. Мы начнем с самого простого подхода, а затем перейдем к более сложным.

Передача данных с помощью свойств

Самый простой способ получить данные из контроллера представления A для контроллера B — использовать свойство.

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

Вот контроллер представления MainViewController со свойством text:

class MainViewController: UIViewController {
    var text: String = ""
 
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Всякий раз, когда вы создаете экземпляр MainViewController, вы можете присвоить значение свойству text:

let vc = MainViewController()
vc.text = "Новое значения для свойства."

Создадим новый класс контроллера представления, а также .xib файл. Вы можете сделать это, выбрав в Xcode File -> New File, затем Cocoa Touch класс, подкласс UIViewController. Не забудьте установить флажок «Also create XIB file».

Вот код для нового контроллера представления:

class SecondaryViewController: UIViewController {
    var text: String = ""
 
    @IBOutlet weak var textLabel: UILabel?
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        textLabel?.text = text
    }
}

В файле XIB добавьте UILabel и подключите его к textLabel. В viewDidLoad() свойство text присваивается textLabel.

Добавьте следующий метод к MainViewController:

@IBAction func onButtonTap() {
    let vc = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
    vc.text = "Наш новый текст"
    navigationController?.pushViewController(vc, animated: true)
}

Вот что происходит в этом фрагменте кода:

  • Мы создаем константу с именем vc и назначаем ей экземпляр SecondaryViewController. Мы передаем имя XIB в инициализаторе, чтобы убедиться, что контроллер представления использует правильный файл XIB.
  • Затем мы назначаем строку свойству text. Это и есть фактическая передача данных между контроллерами представления.
  • Наконец, мы вызываем новый контроллер с помощью pushViewController(_:animated:).

Передача данных с использованием segue

Если вы используете Storyboards, вы можете передавать данные между контроллерами представления с помощью segue, используя функцию prepare(for:sender:).

Storyboards позволяют вам создавать пользовательский интерфейс для вашего приложения. При этом вы можете создавать переходы между контроллерами представления без использования кода.

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

Рассмотрим пример использования segue от MainViewController к TertiaryViewController. Для этого мы используем тип segue типа Show. Создадим класс для TertiaryViewController и установим его для контроллера представления в Identity Inspector.

class TertiaryViewController: UIViewController {
    var username: String = ""
 
    @IBOutlet weak var usernameLabel: UILabel?
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        usernameLabel?.text = username
    }
}

Далее мы используем специальную функцию prepare(for:sender:), чтобы передать данные из MainViewController в TertiaryViewController.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.destination is TertiaryViewController {
        let vc = segue.destination as? TertiaryViewController
        vc?.username = "Артур Дент"
    }
}

С помощью оператора if и ключевого слова is мы проверяем, что целевой класс для перехода segue — это TertiaryViewController. Затем мы используем приведение типа с помощью as? для segue.destination, так как по умолчанию свойство destination для segue имеет тип UIViewController. Наконец, мы устанавливаем свойство username.

Мы можем сократить приведенный выше пример кода:

if let vc = segue.destination as? TertiaryViewController {
    vc.username = "Форд Перфект"
}

Мы также можем использовать свойство segue.identifier, которые для начала нужно установить в свойство segue в Storyboard.

if segue.identifier == "tertiaryVC" {
}

Передача данных с использованием свойств и функций

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

К примеру:

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

Вместо передачи данных из A → B мы хотим передать данные обратно из B → A.

Самый простой способ передать данные обратно — создать ссылку на контроллер представления A в контроллере представления B, а затем вызвать функцию из контроллера представления А в контроллер представления B.

Вот пример кода для второго контроллера представления:

class SecondaryViewController: UIViewController {
    var mainViewController: MainViewController?
 
    @IBAction func onButtonTap() {
        mainViewController?.onUserAction(data: "Передаем данные")
    }
}

Затем мы добавляем эту функцию к первому контроллеру представления:

func onUserAction(data: String) {
    print("Получены данные: \(data)")
}

Далее мы соединяем два контроллера представлений:

let vc = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
vc.mainViewController = self

В приведенном выше примере self присваивается свойство mainViewController. Второй контроллер представления теперь знает основной контроллер представления, поэтому он может вызвать любую из его функций, например onUserAction(data:).

Однако этот подход к передаче данных не самый идеальный. У него есть несколько существенных недостатков:

  • MainViewController и SecondaryViewController тесно связаны между собой. Нам нужно избегать тесной связи в разработке программного обеспечения, главным образом потому, что это уменьшает модульность вашего кода. Оба класса слишком зависят друг на друга, чтобы функционировать должным образом.
  • Приведенный выше пример кода создает retain-цикл. Второй контроллер представления не может быть удален из памяти до тех пор, пока первый контроллер не будет удален, но первый контроллер не может быть удален из памяти, пока второй контроллер не будет удален. Здесь решением может быть использование ключевого слово weak.

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

Передача данных с помощью делегирования

Делегирование — это важный и часто используемый шаблон проектирования в iOS SDK.

С делегированием базовый класс может передавать функциональность любому другому классу. Затем программист может реализовать дополнительный класс и реагировать на события из базового класса, используя протокол. При этом два класса остаются не связанными между собой.

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

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

Прежде чем вы и пекарь поймете друг друга, вам нужно определить протокол:

protocol PizzaDelegate {
    func onPizzaReady(type: String)
}

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

class MainViewController: UIViewController, PizzaDelegate {
    ...

Если мы хотим соответствовать протоколу, мы также должны реализовать метод onPizzaReady(type: String) в MainViewController:

func onPizzaReady(type: String) {
    print("Пицца типа \(type) готова!")
}

Когда мы создаем второй контроллер представления, мы также добавляем соединение с делегатом в MainViewController:

vc.delegate = self

Также мы добавляем свойство и некоторый код в класс, который должен делегировать функциональность:

weak var delegate: PizzaDelegate?
 
@IBAction func onButtonTap() {
    delegate?.onPizzaReady(type: "Овощная пицца")
}

Предположим, функция onButtonTap() вызывается, когда пекарь заканчивает готовить пиццу. Затем он вызывает onPizzaReady(type:) для делегата. Пекарю не важно, есть делегат или нет. Если нет делегата, с пиццей просто ничего не происходит. Если есть делегат, то пекарь отдает пиццу делагату, который может делать с ней все, что захочет.

Давайте посмотрим на ключевые компоненты делегирования:

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

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

Передача данных с помощью замыканий

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

Начнем с создания свойства на втором контроллере представления:

var completionHandler: ((String) -> Int)?

Данное замыкание является опциональным и имеет тип (String) -> Int. Это означает, что замыкание принимает один параметр типа String и возвращает одно значение типа Int.

Во втором контроллере представления мы вызываем замыкание по нажатию кнопки:

@IBAction func onButtonTap() {
    let result = completionHandler?("Привет!")
 
    print("completionHandler возвращает \(result)")
}

Затем в MainViewController вы можете определить замыкание следующим образом:

let vc = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil)
 
vc.completionHandler = { text in
 
    print("текст = \(text)")
 
    return text.characters.count
}

Это замыкание объявлено локально, поэтому вы можете использовать все локальные переменные, свойства и функции.

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

Замыкания могут быть полезны в следующих случаях:

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

Передача данных с помощью NotificationCenter

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

Центр уведомлений — это подход к шаблону разработки программного обеспечения через паттерн Observer-Observable.

Работа с Центром уведомлений состоит из трех ключевых компонентов:

  • Наблюдение за уведомлением.
  • Отправка уведомления.
  • Ответ на уведомление.

Начнем с наблюдения за уведомлением. Прежде чем вы сможете ответить на уведомление, вам нужно сообщить в Центр уведомлений, что вы хотите его увидеть. Затем Центр уведомлений сообщает вам обо всех обнаруженных уведомлениях.

Каждое уведомление должно иметь имя для идентификации. В MainViewController в верхней части класса мы добавляем следующее статическое свойство:

static let notificationName = Notification.Name("myNotificationName")

Это статическое свойство, также известное как свойство класса, доступно в любом месте кода путем вызова MainViewController.notificationName.

Далее мы добавляем наблюдение за этим уведомлением:

NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: MainViewController.notificationName, object: nil)

Данный код можно добавить в viewDidLoad(), чтобы наблюдение регистрировалось, когда на экран выводится контроллер представления. Вот что происходит в приведенном выше примере кода:

  • Мы используем NotificationCenter.default, который является центром уведомлений по умолчанию. Вы также можете создать свой собственный Центр уведомлений.
  • Затем мы вызываем функцию addObserver(_:selector:name:object:) в Центре уведомлений.
  • Первый параметр — это экземпляр, который осуществляет наблюдение, и это почти всегда self.
  • Второй параметр — это селектор, который мы хотим вызвать при обнаружении уведомления.
  • Третий параметр — это имя уведомления, которую мы передаем через статическую константу notificationName.
  • Четвертый параметр — это объект, уведомления от которого вы хотите получать.

Вы можете прекратить наблюдение за уведомлением следующим образом:

NotificationCenter.default.removeObserver(self, name: MainViewController.notificationName, object: nil)

Вы также можете прекратить наблюдать за всеми уведомлениями с помощью:

NotificationCenter.default.removeObserver(self)

Добавим функцию, которая будет вызываться при получении уведомления onNotification(notification:):

@objc func onNotification(notification:Notification) {
    print(notification.userInfo)
}

Ключевое слов @objc требуется, потому что NSNotificationCenter является частью кода Objective-C.

Затем мы можем вызвать уведомление:

NotificationCenter.default.post(name: MainViewController.notificationName, object: nil, userInfo: ["data": 42, "isImportant": true])
  • Мы вызываем функцию post(name:object:userInfo:) в Центре уведомлений по умолчанию.
  • Первый параметр функции — это имя уведомления, которое мы определили ранее.
  • Второй параметр — объект, отправляющий уведомление.
  • Третий параметр — это данные уведомления userInfo.

Центр уведомлений пригодится в нескольких случаях:

  • Контроллеры представления или другие классы, между которыми вы хотите передавать данные, не связаны между собой. Подумайте о контроллере табличного представления, который должен реагировать, когда REST API получает новые данные.
  • Контроллеры представления не обязательно должны существовать. Может случиться, что REST API получает данные до того, как табличное представление выведено на экран.
  • Многим контроллерам представлений необходимо отвечать на одно уведомление, или одному контроллеру представления необходимо отвечать на несколько уведомлений.
Читайте также:
Комментарии (3)
  1. Почему просто не вставить все файлы целиком? Хер поймешь к какому файлу относится конкретный блок кода, а это вообще то важно!

  2. Согласен с предыдущими комментариями. Попробовал сделать передачу данных с помощью замыкания, создал отдельный проект для теста, написал все точно так же как в статье — ничего не работает. Спасибо за потерянное время! 10/10

Добавить комментарий

Ваш адрес email не будет опубликован.