Avatar ·

Как программно вернуться на предыдущий ViewController в Swift? (UINavigationController, unwind segue)

Всем привет! Я новичок в iOS разработке на Swift и столкнулся с проблемой навигации между экранами. У меня есть приложение с двумя ViewController'ами, соединёнными через Storyboard с помощью обычного Show (Push) segue. Первый экран — это список элементов, а второй — детальная информация. На втором экране есть кнопка "Назад", которая должна возвращать пользователя обратно. Я знаю, что в UINavigationController есть кнопка "Back" по умолчанию, но мне нужно сделать кастомную кнопку, которая программно вызывает переход назад. Я пробовал использовать `self.dismiss(animated: true, completion: nil)` и `self.navigationController?.popViewController(animated: true)`, но в некоторых случаях ничего не происходит, а в других — приложение вылетает с ошибкой. Вот код моей кнопки: ```swift @IBAction func backButtonTapped(_ sender: UIButton) { // Пробовал так: self.dismiss(animated: true, completion: nil) // И так: self.navigationController?.popViewController(animated: true) } ``` Также я слышал про unwind segues, но не совсем понимаю, как их правильно настроить в коде, а не через Interface Builder. Подскажите, пожалуйста, какой способ является правильным и надёжным для возврата на предыдущий экран? Заранее спасибо! <<>>

Основные способы программного возврата на предыдущий ViewController

Проблема, с которой вы столкнулись, типична для начинающих разработчиков. Главное — понять, как именно был открыт ваш второй экран. В iOS есть два основных механизма навигации: модальное представление (present/dismiss) и навигационный стек (push/pop). Рассмотрим оба случая.

1. Если вы используете UINavigationController (Show/Push segue)

В этом случае правильный способ — использовать метод popViewController(animated:). Он удаляет текущий ViewController из навигационного стека и возвращает к предыдущему.

@IBAction func backButtonTapped(_ sender: UIButton) {
    // Проверяем, что navigationController существует
    guard let navController = self.navigationController else {
        print("Ошибка: ViewController не находится в UINavigationController")
        return
    }
    navController.popViewController(animated: true)
}

Важно: если вы используете Show segue из Storyboard, то ваш второй экран автоматически помещается в стек UINavigationController. Метод dismiss здесь не сработает, потому что экран не был представлен модально.

2. Если вы используете модальный переход (Present modally)

В этом случае экран был представлен с помощью present(_:animated:completion:). Тогда для возврата нужно использовать dismiss(animated:completion:):

@IBAction func backButtonTapped(_ sender: UIButton) {
    self.dismiss(animated: true, completion: nil)
}

При этом dismiss должен вызываться на том ViewController, который был представлен (или на его parent-контроллере).

3. Универсальное решение

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

@IBAction func backButtonTapped(_ sender: UIButton) {
    if let navController = self.navigationController {
        // Если есть navigationController — используем pop
        navController.popViewController(animated: true)
    } else {
        // Иначе — dismiss (модальное представление)
        self.dismiss(animated: true, completion: nil)
    }
}

4. Unwind Segue (возврат через Storyboard)

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

  • В первом ViewController (куда хотите вернуться) объявите метод с атрибутом @IBAction и параметром типа UIStoryboardSegue:
@IBAction func unwindToFirstViewController(_ segue: UIStoryboardSegue) {
    // Здесь можно обработать данные, переданные обратно
    if let sourceVC = segue.source as? SecondViewController {
        print("Получены данные: \(sourceVC.someData)")
    }
}
  • В Storyboard создайте unwind segue: перетащите от кнопки "Назад" к значку "Exit" (зелёный квадрат с стрелкой) в вашем втором ViewController и выберите созданный метод.
  • В коде кнопки просто вызовите performSegue(withIdentifier:sender:):
@IBAction func backButtonTapped(_ sender: UIButton) {
    self.performSegue(withIdentifier: "unwindToFirst", sender: self)
}

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

5. Распространённые ошибки и их решение

  • Приложение падает с ошибкой: проверьте, что ваш ViewController действительно находится в UINavigationController. Если вы создали его напрямую через Storyboard без встраивания в Navigation Controller, то navigationController будет nil.
  • Ничего не происходит: убедитесь, что вы не вызываете dismiss на уже отпущенном контроллере. Используйте guard для проверки.
  • Использование dismiss вместо pop: если вы использовали Show segue, dismiss не сработает, так как экран не был представлен модально.

Рекомендую для простых случаев использовать popViewController (если есть NavigationController) или dismiss (для модальных окон). Для сложной навигации — unwind segues.

<<>>

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

Часто при возврате на предыдущий экран нужно передать какие-то данные (например, выбранный элемент). Вместо того чтобы полагаться только на механизм навигации, можно использовать паттерн делегирования или замыкания (closures).

Пример с замыканием:

class SecondViewController: UIViewController {
    var onBack: ((String) -> Void)? // Замыкание для передачи данных
    
    @IBAction func backButtonTapped(_ sender: UIButton) {
        // Передаём данные и возвращаемся назад
        onBack?("Данные с второго экрана")
        self.navigationController?.popViewController(animated: true)
    }
}

// В первом ViewController:
class FirstViewController: UIViewController {
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let secondVC = segue.destination as? SecondViewController {
            secondVC.onBack = { [weak self] data in
                self?.receivedData = data
                self?.tableView.reloadData()
            }
        }
    }
}

Этот подход особенно полезен, когда нужно обновить данные на первом экране после возврата. Он работает как с push, так и с модальными переходами. Главное — не забыть использовать [weak self] в замыкании, чтобы избежать утечек памяти.