← Назад к вопросам

Будет ли нарушением принципа Барбары Лисков если мы пушнем UINavigationController в другой UINavigationController?

2.3 Middle🔥 71 комментариев
#UIKit и верстка#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Нарушение принципа SOLID при вложении UINavigationController

Да, такое действие прямо нарушает принцип подстановки Лисков (Liskov Substitution Principle, LSP), который является одним из пяти ключевых принципов SOLID в объектно-ориентированном программировании. LSP гласит: «Объекты в программе должны быть заменяемыми на экземпляры их подтипов без нарушения правильности программы». Рассмотрим, почему вложение контроллеров нарушает этот принцип и какие конкретные проблемы это вызывает.

Анализ через применимость LSP

UINavigationController в iOS — это специализированный контейнерный контроллер, который управляет стеком UIViewController и предоставляет специфичный интерфейс (панель навигации, кнопки назад). Его ключевые обязанности:

class UINavigationController: UIViewController {
    // Специфичные свойства и методы
    var viewControllers: [UIViewController]
    func pushViewController(_ viewController: UIViewController, animated: Bool)
    func popViewController(animated: Bool) -> UIViewController?
}

Когда мы помещаем UINavigationController внутрь другого UINavigationController, мы нарушаем следующие ожидания:

  • Нарушение контракта поведения: UINavigationController ожидает, что элементы его стека (viewControllers) будут UIViewController, которые могут самостоятельно управлять своим представлением и жизненным циклом. Однако, когда один UINavigationController становится элементом стека другого, он не может правильно функционировать как обычный UIViewController.
// Проблемный код — нарушение LSP
let parentNavController = UINavigationController()
let childNavController = UINavigationController(rootViewController: someVC)

// Здесь childNavController "подставляется" как обычный UIViewController
parentNavController.pushViewController(childNavController, animated: true)
// Но он не может корректно выполнять роль простого viewController!

Конкретные проблемы и нарушения

1. Нарушение управления стеком и жизненного цикла:

  • У каждого UINavigationController свой собственный стек viewControllers. Когда он становится элементом другого стека, возникает конфликт управления: какой контроллер должен отвечать за переходы назад? Кто управляет navigationBar?
  • Методы жизненного цикла (viewDidLoad, viewWillAppear) могут вызываться неправильно или дублироваться.

2. Нарушение пользовательского интерфейса и ожиданий:

  • UINavigationController добавляет свою собственную navigationBar. При вложении появляется две панели навигации, что сбивает пользователя и нарушает стандартные паттерны iOS.
  • Кнопка «Назад» может работать непредсказуемо: возвращать либо в родительский стек, либо в стек дочернего контроллера.

3. Нарушение принципов UIKit и Apple Human Interface Guidelines:

  • Apple прямо указывает, что UINavigationController должен быть корневым или верхнеуровневвым контроллером. Вложение приводит к нарушению рекомендаций HIG.
  • Архитектура UIKit рассчитана на то, что UINavigationController является контейнером, а не контентом.

Альтернативные решения, соблюдающие LSP

Чтобы избежать нарушения принципа Лисков, используйте следующие подходы:

1. Координаторы (Coordinators) или Router-паттерн:

// Создаем отдельный координатор для управления навигацией
class AppCoordinator {
    private let navigationController: UINavigationController
    
    func showDetailScreen() {
        let detailVC = DetailViewController()
        navigationController.pushViewController(detailVC, animated: true)
    }
}

2. Использование UITabBarController или кастомных переходов:

// Если нужна сложная навигация — используем таббар
let tabController = UITabBarController()
tabController.viewControllers = [navController1, navController2]

3. Кастомные контейнерные контроллеры:

// Создаем свой контейнер, который корректно обрабатывает вложенные потоки
class CustomContainerController: UIViewController {
    private var currentNavController: UINavigationController?
    
    func switchToNewNavigationFlow(rootVC: UIViewController) {
        let newNavController = UINavigationController(rootViewController: rootVC)
        // Корректная замена текущего потока
        transition(to: newNavController)
    }
}

Заключение

Вложение UINavigationController в другой UINavigationController является нарушением LSP, потому что:

  • UINavigationController не может выполнять роль обычного UIViewController без изменения ожидаемого поведения программы.
  • Это приводит к конкретным проблемам в архитектуре, пользовательском интерфейсе и жизненном цикле.
  • Такое решение противоречит рекомендациям Apple и принципам устойчивой архитектуры.

Для сложных навигационных потоков используйте координаторы, роутеры или другие паттерны, которые позволяют управлять навигацией без нарушения принципов SOLID и гарантируют заменяемость объектов согласно Liskov Substitution Principle.