Avatar ·

Как исправить deprecated 'keyWindow' в iOS 13 и выше (Swift)

Всем привет! Я начинающий iOS-разработчик, пишу приложение на Swift. Недавно обновил Xcode до версии 12, и мой проект перестал компилироваться. Я использую старый способ получения главного окна приложения через `UIApplication.shared.keyWindow`, но компилятор выдает предупреждение: `'keyWindow' was deprecated in iOS 13.0`. Вот мой код: ```swift if let keyWindow = UIApplication.shared.keyWindow { // Пытаюсь показать какой-то контроллер поверх всего let alert = UIAlertController(title: "Ошибка", message: "Что-то пошло не так", preferredStyle: .alert) keyWindow.rootViewController?.present(alert, animated: true) } ``` Я понимаю, что Apple что-то изменила в iOS 13 с многозадачностью и сценами (UIScene), но не могу разобраться, как теперь правильно получить окно. Пробовал читать документацию, но там слишком сложно для новичка. Подскажите, как исправить этот код, чтобы он работал на iOS 13 и выше, но при этом не сломал поддержку старых версий (iOS 12 и ниже)? Заранее спасибо! <<>>

Проблема и её причина

В iOS 13 Apple ввела поддержку нескольких окон (сцен) через UIScene. Раньше у приложения было одно главное окно, доступное через keyWindow. Теперь же окон может быть несколько (например, на iPad), и свойство keyWindow стало неоднозначным. Поэтому его объявили устаревшим. Однако на iPhone в 99% случаев у вас всё ещё одно окно, и задача — получить его правильно.

Решение для iOS 13+

Самый надёжный способ — получить окно через активные сцены приложения. Вот универсальная функция, которая работает на iOS 13 и выше, а также на более старых версиях (iOS 12 и ниже). Она возвращает опциональное окно.

import UIKit

extension UIApplication {
    var currentKeyWindow: UIWindow? {
        // Для iOS 13+ используем connectedScenes
        if #available(iOS 13.0, *) {
            return self.connectedScenes
                .filter { $0.activationState == .foregroundActive }
                .compactMap { $0 as? UIWindowScene }
                .first?.windows
                .filter { $0.isKeyWindow }
                .first
        } else {
            // Для старых версий — старый способ
            return self.keyWindow
        }
    }
}

Как использовать

Теперь в вашем коде замените UIApplication.shared.keyWindow на UIApplication.shared.currentKeyWindow:

if let keyWindow = UIApplication.shared.currentKeyWindow {
    let alert = UIAlertController(title: "Ошибка", message: "Что-то пошло не так", preferredStyle: .alert)
    keyWindow.rootViewController?.present(alert, animated: true)
}

Объяснение кода

  • connectedScenes — массив всех сцен (окон) приложения.
  • фильтр по activationState — мы берём только активные сцены, которые видны пользователю.
  • compactMap { $0 as? UIWindowScene } — преобразуем сцены в тип UIWindowScene.
  • .first?.windows — получаем все окна этой сцены.
  • .filter { $0.isKeyWindow } — находим среди них ключевое (главное) окно.
  • #available(iOS 13.0, *) — проверка версии, чтобы код работал на iOS 12 и ниже.

Этот подход полностью соответствует рекомендациям Apple и не вызывает предупреждений компилятора.

Важные замечания

  • Не используйте UIApplication.shared.windows.first — это может вернуть не то окно (например, системное или окно другого приложения).
  • На iPad с несколькими сценами (Split View, Slide Over) может быть несколько активных окон. В этом примере мы берём первое активное — обычно это то, с которым работает пользователь.
  • Если вам нужно показать контроллер поверх всего, лучше использовать UIApplication.shared.topMostViewController() (см. альтернативный ответ).
<<>>

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

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

extension UIApplication {
    func topMostViewController() -> UIViewController? {
        guard let rootVC = self.currentKeyWindow?.rootViewController else { return nil }
        return topMost(from: rootVC)
    }
    
    private func topMost(from vc: UIViewController) -> UIViewController {
        if let presented = vc.presentedViewController {
            return topMost(from: presented)
        }
        if let nav = vc as? UINavigationController {
            return topMost(from: nav.topViewController ?? vc)
        }
        if let tab = vc as? UITabBarController {
            return topMost(from: tab.selectedViewController ?? vc)
        }
        return vc
    }
}

Использование:

if let topVC = UIApplication.shared.topMostViewController() {
    let alert = UIAlertController(title: "Ошибка", message: "Что-то пошло не так", preferredStyle: .alert)
    topVC.present(alert, animated: true)
}

Этот метод рекурсивно обходит иерархию контроллеров (навигационные, таб-бары, модальные) и возвращает тот, который видит пользователь. Он работает на всех версиях iOS, включая 13+, если вы используете функцию currentKeyWindow из первого ответа.

Минус: требует больше кода, но зато даёт прямой доступ к контроллеру, а не к окну. Выбирайте подходящий способ под свою задачу.