Как решить: "keyWindow" устарела в iOS 13.0

Я использую Core Data с Cloud Kit, и поэтому должен проверять статус пользователя iCloud во время запуска приложения. В случае возникновения проблем я хочу выдать диалог пользователю, и я делаю это, используя UIApplication.shared.keyWindow?.rootViewController?.present(...) до сих пор.

В Xcode 11 beta 4 появилось новое сообщение об устаревании:

"keyWindow" устарело в iOS 13.0: его не следует использовать для приложений, которые поддерживают несколько сцен, поскольку оно возвращает ключевое окно для всех связанных сцен

Как мне представить диалог вместо этого?

32 ответа

Решение

Это мое решение:

let keyWindow = UIApplication.shared.connectedScenes
        .filter({$0.activationState == .foregroundActive})
        .map({$0 as? UIWindowScene})
        .compactMap({$0})
        .first?.windows
        .filter({$0.isKeyWindow}).first

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

keyWindow?.endEditing(true)

Принятый ответ, хотя и гениальный, может быть излишне сложным. Точно такой же результат можно получить гораздо проще:

UIApplication.shared.windows.filter {$0.isKeyWindow}.first

Я также хотел бы предупредить, что прекращение поддержки keyWindowне следует воспринимать слишком серьезно. Полное предупреждение гласит:

'keyWindow' устарело в iOS 13.0: не должно использоваться для приложений, поддерживающих несколько сцен, поскольку он возвращает ключевое окно для всех связанных сцен.

Поэтому, если вы не поддерживаете несколько окон на iPad, нет никаких возражений против продолжения использования keyWindow.

Немного улучшив отличный ответ Мэтта, он стал еще проще, короче и элегантнее:

UIApplication.shared.windows.first { $0.isKeyWindow }

Вот обратно совместимый способ обнаружения keyWindow:

extension UIWindow {
    static var key: UIWindow? {
        if #available(iOS 13, *) {
            return UIApplication.shared.windows.first { $0.isKeyWindow }
        } else {
            return UIApplication.shared.keyWindow
        }
    }
}

Применение:

if let keyWindow = UIWindow.key {
    // Do something
}

Обычно используют

Swift 5

UIApplication.shared.windows.filter {$0.isKeyWindow}.first

Вдобавок , в UIViewController:

self.view.window

view.window текущее окно цен

WWDC 2019:

Ключевые окна

  • Отслеживайте окна вручную

Для решения Objective-C

+(UIWindow*)keyWindow
{
    UIWindow        *foundWindow = nil;
    NSArray         *windows = [[UIApplication sharedApplication]windows];
    for (UIWindow   *window in windows) {
        if (window.isKeyWindow) {
            foundWindow = window;
            break;
        }
    }
    return foundWindow;
}

А UIApplication расширение:

extension UIApplication {

    /// The app's key window taking into consideration apps that support multiple scenes.
    var keyWindowInConnectedScenes: UIWindow? {
        return windows.first(where: { $0.isKeyWindow })
    }

}

Применение:

let myKeyWindow: UIWindow? = UIApplication.shared.keyWindowInConnectedScenes

Если вы хотите использовать его в любом ViewController, вы можете просто использовать.

self.view.window

Поддерживает iOS 13 и более поздние версии.

Продолжать использовать тот же синтаксис, что и в старых версиях iOS. UIApplication.shared.keyWindowсоздайте это расширение:

      extension UIApplication {
    var mainKeyWindow: UIWindow? {
        get {
            if #available(iOS 13, *) {
                return connectedScenes
                    .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
                    .first { $0.isKeyWindow }
            } else {
                return keyWindow
            }
        }
    }
}

Применение

      if let keyWindow = UIApplication.shared.mainKeyWindow {
    // Do Stuff
}

В идеале, поскольку он устарел, я бы посоветовал вам сохранить окно в SceneDelegate. Однако, если вам нужен временный обходной путь, вы можете создать фильтр и получить keyWindow точно так же.

let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first

Я ответил на вопрос о дублирующем фиде , и, поскольку я не смог найти здесь ответа, предоставив столько кода (с комментариями), вот мой вклад:

(Протестировано с iOS 15.2, работающей на Xcode 13.2.1)

      extension UIApplication {
    
    var keyWindow: UIWindow? {
        // Get connected scenes
        return UIApplication.shared.connectedScenes
            // Keep only active scenes, onscreen and visible to the user
            .filter { $0.activationState == .foregroundActive }
            // Keep only the first `UIWindowScene`
            .first(where: { $0 is UIWindowScene })
            // Get its associated windows
            .flatMap({ $0 as? UIWindowScene })?.windows
            // Finally, keep only the key window
            .first(where: \.isKeyWindow)
    }
    
}

Если вы хотите найти представленное в ключе UIWindow , вот еще один, который может оказаться полезным:

      extension UIApplication {
    
    var keyWindowPresentedController: UIViewController? {
        var viewController = self.keyWindow?.rootViewController
        
        // If root `UIViewController` is a `UITabBarController`
        if let presentedController = viewController as? UITabBarController {
            // Move to selected `UIViewController`
            viewController = presentedController.selectedViewController
        }
        
        // Go deeper to find the last presented `UIViewController`
        while let presentedController = viewController?.presentedViewController {
            // If root `UIViewController` is a `UITabBarController`
            if let presentedController = presentedController as? UITabBarController {
                // Move to selected `UIViewController`
                viewController = presentedController.selectedViewController
            } else {
                // Otherwise, go deeper
                viewController = presentedController
            }
        }
        
        return viewController
    }
    
}

Вы можете поместить это куда угодно, но я лично добавил это как extensionк .

Это позволяет мне добавлять больше полезных расширений, например, для представления UIViewControllers проще, например:

      extension UIViewController {
    
    func presentInKeyWindow(animated: Bool = true, completion: (() -> Void)? = nil) {
        DispatchQueue.main.async {
            UIApplication.shared.keyWindow?.rootViewController?
                .present(self, animated: animated, completion: completion)
        }
    }
    
    func presentInKeyWindowPresentedController(animated: Bool = true, completion: (() -> Void)? = nil) {
        DispatchQueue.main.async {
            UIApplication.shared.keyWindowPresentedController?
                .present(self, animated: animated, completion: completion)
        }
    }
    
}

Попробуйте с этим:

UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController!.present(alert, animated: true, completion: nil)

Как многие разработчики просят код Objective C для замены этой устаревшей версии. Вы можете использовать приведенный ниже код для использования keyWindow.

+(UIWindow*)keyWindow {
    UIWindow        *windowRoot = nil;
    NSArray         *windows = [[UIApplication sharedApplication]windows];
    for (UIWindow   *window in windows) {
        if (window.isKeyWindow) {
            windowRoot = window;
            break;
        }
    }
    return windowRoot;
}

Я создал и добавил этот метод в AppDelegate class как метод класса и используйте его очень простым способом, описанным ниже.

[AppDelegate keyWindow];

Не забудьте добавить этот метод в класс AppDelegate.h, как показано ниже.

+(UIWindow*)keyWindow;

Для решения Objective-C тоже

@implementation UIWindow (iOS13)

+ (UIWindow*) keyWindow {
   NSPredicate *isKeyWindow = [NSPredicate predicateWithFormat:@"isKeyWindow == YES"];
   return [[[UIApplication sharedApplication] windows] filteredArrayUsingPredicate:isKeyWindow].firstObject;
}

@end

Как вы, наверное, знаете, ключевое окно устарело из-за возможного наличия нескольких сцен. Наиболее удобное решение — предоставитьcurrentWindowкак расширение, затем поиск и замена.

      extension UIApplication {
    var currentWindow: UIWindow? {
        connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }
    }
}

Вдохновленный ответом Берни

let keyWindow = Array(UIApplication.shared.connectedScenes)
        .compactMap { $0 as? UIWindowScene }
        .flatMap { $0.windows }
        .first(where: { $0.isKeyWindow })

Я решил с:

      let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first

Код Берни хорош, но он не работает, когда приложение возвращается из фона.

Это мой код:

      class var safeArea : UIEdgeInsets
{
    if #available(iOS 13, *) {
        var keyWindow = UIApplication.shared.connectedScenes
                .filter({$0.activationState == .foregroundActive})
                .map({$0 as? UIWindowScene})
                .compactMap({$0})
                .first?.windows
                .filter({$0.isKeyWindow}).first
        // <FIX> the above code doesn't work if the app comes back from background!
        if (keyWindow == nil) {
            keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
        }
        return keyWindow?.safeAreaInsets ?? UIEdgeInsets()
    }
    else {
        guard let keyWindow = UIApplication.shared.keyWindow else { return UIEdgeInsets() }
        return keyWindow.safeAreaInsets
    }
}
NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes;
for (UIScene *scene in connectedScenes) {
    if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
        UIWindowScene *windowScene = (UIWindowScene *)scene;
        for (UIWindow *window in windowScene.windows) {
            UIViewController *viewController = window.rootViewController;
            // Get the instance of your view controller
            if ([viewController isKindOfClass:[YOUR_VIEW_CONTROLLER class]]) {
                // Your code here...
                break;
            }
        }
    }
}
- (UIWindow *)mainWindow {
    NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator];
    for (UIWindow *window in frontToBackWindows) {
        BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen;
        BOOL windowIsVisible = !window.hidden && window.alpha > 0;
        BOOL windowLevelSupported = (window.windowLevel >= UIWindowLevelNormal);
        BOOL windowKeyWindow = window.isKeyWindow;
        if(windowOnMainScreen && windowIsVisible && windowLevelSupported && windowKeyWindow) {
            return window;
        }
    }
    return nil;
}

если вы используете SwiftLint с правилом first_where и хотите заставить замолчать враждующих сторон:

      UIApplication.shared.windows.first(where: { $0.isKeyWindow })

Решение цели C:

      UIWindow *foundWindow = nil;
NSSet *scenes=[[UIApplication sharedApplication] connectedScenes];
NSArray *windows;
for(id aScene in scenes){  // it's an NSSet so you can't use the first object
    windows=[aScene windows];
    if([aScene activationState]==UISceneActivationStateForegroundActive)
         break;
}
for (UIWindow  *window in windows) {
    if (window.isKeyWindow) {
        foundWindow = window;
        break;
    }
}
 // and to find the parent viewController:
UIViewController* parentController = foundWindow.rootViewController;
while( parentController.presentedViewController &&
      parentController != parentController.presentedViewController ){
    parentController = parentController.presentedViewController;
}

Если ваше приложение не было обновлено для принятия жизненного цикла приложения на основе сцены, другой простой способ получить объект активного окна - через UIApplicationDelegate:

      let window = UIApplication.shared.delegate?.window
let rootViewController = window??.rootViewController

Я столкнулся с проблемой, когда .foregroundActive сцены были пустыми

Итак, вот мой обходной путь

      public extension UIWindow {
    @objc
    static var main: UIWindow {
        // Here we sort all the scenes in order to work around the case
        // when no .foregroundActive scenes available and we need to look through
        // all connectedScenes in order to find the most suitable one
        let connectedScenes = UIApplication.shared.connectedScenes
            .sorted { lhs, rhs in
                let lhs = lhs.activationState
                let rhs = rhs.activationState
                switch lhs {
                case .foregroundActive:
                    return true
                case .foregroundInactive:
                    return rhs == .background || rhs == .unattached
                case .background:
                    return rhs == .unattached
                case .unattached:
                    return false
                @unknown default:
                    return false
                }
            }
            .compactMap { $0 as? UIWindowScene }

        guard connectedScenes.isEmpty == false else {
            fatalError("Connected scenes is empty")
        }
        let mainWindow = connectedScenes
            .flatMap { $0.windows }
            .first(where: \.isKeyWindow)

        guard let window = mainWindow else {
            fatalError("Couldn't get main window")
        }
        return window
    }
}
      let allScenes = UIApplication.shared.connectedScenes
                    let scene = allScenes.first { $0.activationState == .foregroundActive }
                    
                    if let windowScene = scene as? UIWindowScene {
                        windowScene.keyWindow?.rootViewController?.present(SFSafariViewController(url: url, configuration: conf), animated: isAnimated, completion: nil)
                    }

Я реализовал это решение для однооконных приложений:

var mainWindow: UIWindow? {
    if #available(iOS 13.0, *) {
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            return windowScene.windows.first
        }
    }
    return UIApplication.shared.keyWindow
}

Извините, но все эти решения, похоже, пытаются найти первое ключевое окно, но если две сцены являются параллельными многозадачными, то не будут ли они обе ключевыми, и теперь шансы получить 50/50 правый?

Разве нам не нужно применять подход, ориентированный на конкретное приложение, когда мы определяем, какая сцена имеет отношение к работающему коду?

Я думаю, что решение включает в себя такую ​​архитектуру кода, чтобы, когда нам нужно представить что-то в пользовательском интерфейсе, обработка кода, которая должна иметь доступ к UIViewController или UIView, к которому пользователь инициировал действие или к которому перешел, поэтому мы используем это в качестве основы для определения того, что показывать.

      public extension UIApplication {
    func currentUIWindow() -> UIWindow? {
        let connectedScenes = UIApplication.shared.connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .compactMap { $0 as? UIWindowScene }
        
        let window = connectedScenes.first?
            .windows
            .first { $0.isKeyWindow }

        return window
    }
    
    static func setRootViewVC(_ viewController: UIViewController){
        UIApplication.shared.currentUIWindow()?.rootViewController = viewController
    }
}

поэтому мы можем использовать

      func redirectingToMainScreen(){
    
    let mainVC = UIStoryboard.loadMainScreen()
    UIApplication.setRootViewVC(mainVC)
} 

Мое решение следующее, работает в iOS 15

      let window = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first

Для iOS 16 я использовал следующее:

      let keyWindow = UIApplication.shared.currentUIWindow()?.windowScene?.keyWindow
Другие вопросы по тегам