Как получить самый верхний (top-most) UIViewController на экране iOS приложения?
Расширенное решение: рекурсивный поиск 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
Этот подход полезен, если вам нужно отслеживать изменения контроллера в реальном времени (например, для аналитики). Однако он не учитывает модальные презентации и таб-бары, поэтому его стоит комбинировать с первым решением для полной надежности.