Avatar ·

Передача данных между контроллерами в iOS на Swift: лучшие практики

Всем привет! Я новичок в iOS разработке на Swift и никак не могу разобраться с передачей данных между View Controllers. Перечитал кучу статей, но всё равно путаюсь. У меня есть два экрана: на первом (MainViewController) текстовое поле и кнопка, на втором (DetailViewController) — метка, которая должна отображать введённый текст. Я создаю переход через Storyboard, но данные не передаются. Вот мой код:
class MainViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            let detailVC = segue.destination as! DetailViewController
            detailVC.receivedText = textField.text
        }
    }
    
    @IBAction func goButtonTapped(_ sender: UIButton) {
        performSegue(withIdentifier: "showDetail", sender: self)
    }
}

class DetailViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    var receivedText: String?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = receivedText
    }
}
Проблема в том, что label.text остаётся nil, хотя textField.text не пустой. Я пробовал разные варианты, но ничего не работает. Может быть, я не правильно создаю segue? Или есть более правильные способы передачи данных? Объясните, пожалуйста, как это сделать правильно, и какие вообще есть подходы для передачи данных между контроллерами в iOS. Спасибо! <<>>

Основная проблема вашего кода: порядок выполнения

Ваш код в целом правильный, но есть одна тонкость: свойство receivedText устанавливается до того, как загружается view у DetailViewController. Однако, если вы используете viewDidLoad() для установки текста label, это должно работать. Основная причина, по которой label.text остаётся nil — скорее всего, вы забыли соединить outlet для label в Storyboard, или же label создаётся программно после viewDidLoad.

Давайте разберём правильный подход и все возможные способы передачи данных.

1. Передача данных через prepare(for:sender:) (рекомендуемый для Storyboard)

Это стандартный способ при использовании segue. Вот исправленный и рабочий пример:

class MainViewController: UIViewController {
    @IBOutlet weak var textField: UITField!
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            guard let detailVC = segue.destination as? DetailViewController else { return }
            detailVC.receivedText = textField.text ?? "" // Используем nil-coalescing
        }
    }
    
    @IBAction func goButtonTapped(_ sender: UIButton) {
        performSegue(withIdentifier: "showDetail", sender: self)
    }
}

class DetailViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    var receivedText: String = "" // Лучше сделать non-optional с дефолтным значением
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Убедитесь, что outlet label подключен в Storyboard!
        label.text = receivedText
    }
}

Важные моменты:

  • Всегда проверяйте, что outlet для label подключен в Interface Builder (индикатор слева от кода должен быть заполненным кружком).
  • Используйте guard let или if let для безопасного приведения типа.
  • Лучше использовать non-optional свойства с дефолтным значением, чтобы избежать unwrapping.
  • prepare(for:sender:) вызывается до viewDidLoad, поэтому данные уже будут доступны.

2. Программная передача данных (без Storyboard)

Если вы создаёте контроллеры программно (через код), используйте инициализатор или свойство:

class MainViewController: UIViewController {
    func navigateToDetail() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let detailVC = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
        detailVC.receivedText = textField.text ?? ""
        navigationController?.pushViewController(detailVC, animated: true)
    }
}

3. Использование делегата (для обратной связи)

Когда нужно передать данные обратно (например, из Detail в Main), используйте протокол делегата:

protocol DetailViewControllerDelegate: AnyObject {
    func detailViewController(_ controller: DetailViewController, didEnterText text: String)
}

class DetailViewController: UIViewController {
    weak var delegate: DetailViewControllerDelegate?
    
    @IBAction func saveButtonTapped(_ sender: UIButton) {
        delegate?.detailViewController(self, didEnterText: textField.text ?? "")
        dismiss(animated: true)
    }
}

class MainViewController: UIViewController, DetailViewControllerDelegate {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let detailVC = segue.destination as? DetailViewController {
            detailVC.delegate = self
        }
    }
    
    func detailViewController(_ controller: DetailViewController, didEnterText text: String) {
        label.text = text
    }
}

4. Closure (замыкание) — современная альтернатива делегату

Более лаконичный способ для обратной передачи данных:

class DetailViewController: UIViewController {
    var onTextSaved: ((String) -> Void)?
    
    @IBAction func saveButtonTapped(_ sender: UIButton) {
        onTextSaved?(textField.text ?? "")
        dismiss(animated: true)
    }
}

class MainViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let detailVC = segue.destination as? DetailViewController {
            detailVC.onTextSaved = { [weak self] text in
                self?.label.text = text
            }
        }
    }
}

5. NotificationCenter (для глобальной связи)

Используйте только для событий, которые должны обрабатываться многими объектами (например, смена темы):

// В DetailViewController
NotificationCenter.default.post(name: NSNotification.Name("TextDidChange"), object: nil, userInfo: ["text": textField.text ?? ""])

// В MainViewController (в viewDidLoad)
NotificationCenter.default.addObserver(self, selector: #selector(handleTextChange(_:)), name: NSNotification.Name("TextDidChange"), object: nil)

@objc func handleTextChange(_ notification: Notification) {
    if let text = notification.userInfo?["text"] as? String {
        label.text = text
    }
}

Вывод

Для простой передачи данных вперёд используйте prepare(for:sender:). Для обратной связи — делегат или closure. NotificationCenter оставьте для редких случаев. И всегда проверяйте подключение outlet'ов в Storyboard!

<<>>

Альтернативный подход: использование паттерна Coordinator

Если вы работаете над большим проектом, рекомендую использовать Coordinator для управления навигацией и передачи данных. Это помогает избежать "макаронного кода" и делает архитектуру более чистой.

Пример:

protocol Coordinator: AnyObject {
    func start()
    func showDetail(with text: String)
}

class MainCoordinator: Coordinator {
    var navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        let vc = MainViewController.instantiate()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: false)
    }
    
    func showDetail(with text: String) {
        let vc = DetailViewController.instantiate()
        vc.receivedText = text
        navigationController.pushViewController(vc, animated: true)
    }
}

class MainViewController: UIViewController {
    weak var coordinator: MainCoordinator?
    
    @IBAction func goButtonTapped(_ sender: UIButton) {
        coordinator?.showDetail(with: textField.text ?? "")
    }
}

Преимущества:

  • Контроллеры не знают друг о друге — только о coordinator.
  • Легко тестировать и переиспользовать контроллеры.
  • Логика навигации централизована.

Этот подход особенно полезен, когда у вас сложные сценарии переходов (например, условная навигация, глубокие ссылки).