Avatar ·

Как получить самый верхний (top-most) UIViewController на экране iOS приложения?

Всем привет! Пишу свое первое iOS приложение на Swift и столкнулся с проблемой. У меня есть несколько модальных окон (UIAlertController, UIImagePickerController и кастомные вью-контроллеры), которые иногда открываются друг над другом. Мне нужно программно получить ссылку на тот контроллер, который сейчас находится на самом верху иерархии, чтобы, например, показать новый алерт или проверить, не открыт ли уже какой-то модальный экран. Пробовал использовать `UIApplication.shared.keyWindow?.rootViewController`, но это дает только корневой контроллер, а не текущий видимый. Если я открываю модальный контроллер, то rootViewController не меняется. Пробовал рекурсивно проходить по `presentedViewController`, но это не всегда работает, особенно если есть UINavigationController или UITabBarController. Вот пример моего кода, который находит контроллер не всегда корректно: ```swift func getTopViewController() -> UIViewController? { if var topController = UIApplication.shared.keyWindow?.rootViewController { while let presentedViewController = topController.presentedViewController { topController = presentedViewController } return topController } return nil } ``` Этот код не учитывает, что внутри UINavigationController или UITabBarController могут быть вложенные контроллеры. Как правильно получить самый верхний UIViewController в любой ситуации? Заранее спасибо! <<>>

Расширенное решение: рекурсивный поиск top-most UIViewController

Ваша проблема типична для начинающих iOS разработчиков. Базовый цикл while let действительно не обрабатывает вложенные контроллеры в UINavigationController и UITabBarController. Вот надежное решение, которое учитывает все основные случаи:

extension UIViewController {
    /// Рекурсивно получает самый верхний видимый контроллер
    var topMostViewController: UIViewController {
        // 1. Если контроллер представлен модально - идем вглубь
        if let presented = presentedViewController {
            return presented.topMostViewController
        }
        
        // 2. Если это UINavigationController - берем видимый контроллер
        if let navigation = self as? UINavigationController {
            return navigation.visibleViewController?.topMostViewController ?? self
        }
        
        // 3. Если это UITabBarController - берем выбранный контроллер
        if let tabBar = self as? UITabBarController {
            return tabBar.selectedViewController?.topMostViewController ?? self
        }
        
        // 4. Если это UIAlertController или другой базовый - возвращаем себя
        return self
    }
}

// Удобный метод для получения top-most из AppDelegate или любого места
extension UIApplication {
    func topMostViewController() -> UIViewController? {
        let keyWindow = connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .compactMap { $0 as? UIWindowScene }
            .first?.windows
            .first(where: { $0.isKeyWindow })
        
        return keyWindow?.rootViewController?.topMostViewController
    }
}

Как это работает

  • Шаг 1 (модальные презентации): Проверяем, есть ли presentedViewController. Если да — рекурсивно вызываем метод на нем, чтобы пройти всю цепочку модальных окон.
  • Шаг 2 (навигация): Если контроллер — UINavigationController, берем его visibleViewController (это может быть последний контроллер в стеке или контроллер, который сейчас показывается).
  • Шаг 3 (таб-бар): Если это UITabBarController, используем selectedViewController — активную вкладку.
  • Шаг 4 (базовый случай): Если ни одно из условий не подошло (например, UIAlertController или простой UIViewController), возвращаем сам контроллер.

Пример использования

// Где-то в коде
if let topVC = UIApplication.shared.topMostViewController() {
    let alert = UIAlertController(title: "Привет", message: "Ты на экране: \(type(of: topVC))", preferredStyle: .alert)
    topVC.present(alert, animated: true)
}

Важные нюансы

  • UIAlertController: Он сам является UIViewController, поэтому если алерт уже показан, метод вернет его. Это правильно — нельзя показывать два алерта одновременно без обработки.
  • Многозадачность (iPad): На iPad может быть несколько окон. В коде выше мы берем активное окно с помощью connectedScenes.
  • UISplitViewController: Если используете Split View, нужно добавить дополнительную проверку, но это редкий случай.

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

<<>>

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

Если ваше приложение использует единый UINavigationController в качестве корневого контроллера, можно упростить задачу. Вместо рекурсивного поиска по всей иерархии, используйте делегат навигации:

class NavigationManager: NSObject, UINavigationControllerDelegate {
    static let shared = NavigationManager()
    private weak var currentTopVC: UIViewController?
    
    func navigationController(_ nc: UINavigationController, 
                              willShow viewController: UIViewController, 
                              animated: Bool) {
        currentTopVC = viewController
    }
    
    func getCurrentTopVC() -> UIViewController? {
        return currentTopVC
    }
}

Настройте делегат в AppDelegate или сцене:

navigationController.delegate = NavigationManager.shared

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