Передача данных между экранами в iOS: от новичка до профи
Каждый, кто начинал писать под iOS, рано или поздно упирался в стену: "Как мне передать строку, массив или объект на другой экран?". Вопрос настолько популярен, что на Stack Overflow он собрал тысячи просмотров и десятки вариантов ответов. И это неспроста — архитектура iOS построена так, что каждый экран (ViewController) живет своей жизнью, и данные между ними нужно передавать осознанно.
В этой статье мы разберем 5 основных способов передачи данных между view controllers на Swift. Вы узнаете, когда использовать segue, а когда — делегаты или замыкания, и почему NotificationCenter — это не всегда зло. Поехали!
Почему это вообще проблема?
iOS использует строгую архитектуру MVC (Model-View-Controller). Каждый ViewController отвечает за свой кусочек экрана и не имеет прямого доступа к данным соседнего контроллера. Это сделано для безопасности и модульности, но для разработчика это означает, что нужно явно "протянуть" данные из точки А в точку Б.
Представьте: у вас есть список пользователей, и вы хотите показать детальную информацию при нажатии на ячейку. Вы нажимаете — открывается новый экран. Как передать ID пользователя? Вот тут и начинается магия.
Способ 1: Передача данных через Storyboard Segue (самый простой)
Самый прямолинейный способ — использовать prepare(for:sender:). Вы просто подготавливаете данные перед переходом. Идеально для простых приложений и прототипов.
// В исходном ViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
guard let destinationVC = segue.destination as? DetailViewController else { return }
destinationVC.userID = 42 // передаем число
destinationVC.userName = "Анна" // передаем строку
}
}
// В DetailViewController
var userID: Int = 0
var userName: String = ""
Плюсы: Минимум кода, всё в одном месте. Минусы: Жесткая привязка к Storyboard, сложно тестировать, легко запутаться в идентификаторах.
Способ 2: Программная навигация с передачей данных (продвинутый уровень)
Когда вы устали от segues и хотите контролировать всё из кода, используйте прямой инстанс контроллера.
// В исходном ViewController
func showDetail() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let detailVC = storyboard.instantiateViewController(withIdentifier: "DetailVC") as? DetailViewController else { return }
detailVC.userID = 42
detailVC.userName = "Анна"
navigationController?.pushViewController(detailVC, animated: true)
}
Этот способ даёт полный контроль: вы можете загрузить данные из сети, проверить их и только потом перейти. Отлично для сложной логики.
Способ 3: Делегаты (Delegate Pattern) — обратная связь
Что делать, если нужно передать данные обратно? Например, пользователь выбрал дату на втором экране, и мы хотим вернуть её на первый. Тут на помощь приходит делегат.
Шаг 1: Создаем протокол
protocol DatePickerDelegate: AnyObject {
func didSelectDate(_ date: Date)
}
Шаг 2: Добавляем делегат в дочерний контроллер
class DatePickerViewController: UIViewController {
weak var delegate: DatePickerDelegate?
@IBAction func saveButtonTapped(_ sender: UIButton) {
let selectedDate = datePicker.date
delegate?.didSelectDate(selectedDate)
dismiss(animated: true)
}
}
Шаг 3: Реализуем делегат в родительском контроллере
class MainViewController: UIViewController, DatePickerDelegate {
func didSelectDate(_ date: Date) {
print("Получена дата: \(date)")
// обновляем UI
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let datePickerVC = segue.destination as? DatePickerViewController {
datePickerVC.delegate = self
}
}
}
Делегаты — стандартный паттерн в iOS. UIKit сам использует его повсеместно (например, UITableViewDelegate). Он безопасен, тестируем и понятен другим разработчикам.
Способ 4: Замыкания (Closures) — современный и лаконичный
Если делегаты кажутся вам слишком многословными, используйте замыкания. Это тот же callback, но без отдельного протокола.
// В дочернем контроллере
class EditorViewController: UIViewController {
var onSave: ((String) -> Void)?
@IBAction func saveTapped(_ sender: UIButton) {
onSave?(textField.text ?? "")
dismiss(animated: true)
}
}
// В родительском контроллере
let editorVC = EditorViewController()
editorVC.onSave = { [weak self] text in
self?.label.text = text
}
present(editorVC, animated: true)
Обратите внимание на [weak self] — это важно, чтобы избежать retain cycle (утечки памяти). Замыкания особенно хороши для одноразовых действий: "сделай что-то и забудь".
Способ 5: NotificationCenter — глобальная шина событий
Иногда данные нужно передать между контроллерами, которые никак не связаны навигацией. Или когда один объект должен узнать о событии, произошедшем в другом месте. Тут в игру вступает NotificationCenter.
// Отправляем уведомление
extension Notification.Name {
static let userLoggedIn = Notification.Name("userLoggedIn")
}
NotificationCenter.default.post(name: .userLoggedIn, object: nil, userInfo: ["userID": 42])
// Подписываемся на уведомление (например, в viewDidLoad)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserLogin),
name: .userLoggedIn,
object: nil
)
@objc func handleUserLogin(_ notification: Notification) {
if let userID = notification.userInfo?["userID"] as? Int {
print("Пользователь с ID \(userID) вошел в систему")
}
}
Важно: Не забывайте отписываться от уведомлений, иначе будет утечка памяти. В современных проектах лучше использовать Combine или сторонние библиотеки, но для быстрых решений NotificationCenter — спасение.
Бонус: MVVM и реактивная передача данных
Если вы пишете не просто учебный проект, а серьёзное приложение, присмотритесь к архитектуре MVVM (Model-View-ViewModel). Вместо того чтобы передавать данные напрямую, вы используете наблюдаемые свойства (например, через Combine или сторонние библиотеки вроде RxSwift).
// ViewModel
class UserViewModel: ObservableObject {
@Published var userName: String = ""
@Published var userAge: Int = 0
}
// Первый ViewController
let viewModel = UserViewModel()
viewModel.userName = "Анна"
// Второй ViewController получает тот же viewModel
// и автоматически обновляется при изменениях
Этот подход разделяет логику и UI, делая код тестируемым и переиспользуемым. Но требует больше boilerplate-кода на старте.
Какой способ выбрать? Шпаргалка
- Простой переход