如何解决:'keyWindow'在iOS 13.0中已弃用

我使用核心数据与云工具包,因此要检查iCloud用户状态在应用程序启动。为了防止出现问题,我想向用户发出一个对话框,到目前为止我使用UIApplication.shared.keyWindow?.rootViewController?.present(...)来实现。

在Xcode 11 beta 4中,现在有一个新的弃用消息,告诉我:

'keyWindow'在iOS 13.0中已弃用:不应该用于支持多个场景的应用程序,因为它在所有连接的场景中返回一个键窗口

我应该如何呈现对话呢?

141776 次浏览

这是我的解决方案:

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

使用如:

keyWindow?.endEditing(true)

编辑我在这里提出的建议在iOS 15中已弃用。现在怎么办?好吧,如果一个应用程序没有自己的多个窗口,我假设接受的现代方式将是获得应用程序的connectedScenes的第一个,强制到一个UIWindowScene,并获得它的第一个窗口。但这几乎正是公认的答案所做的!所以我的变通办法在这一点上感觉相当无力。但是,由于历史原因,我还是让它保持原样。


公认的答案虽然巧妙,但可能过于详尽。你可以更简单地得到完全相同的结果:

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

我还要提醒大家,不要过于认真地对待keyWindow的弃用。完整的警告信息如下:

'keyWindow'在iOS 13.0中已弃用:不应该用于支持多个场景的应用程序,因为它在所有连接的场景中返回一个键窗口

因此,如果你不支持iPad上的多窗口,继续使用keyWindow是没有异议的。

iOS 16,兼容至iOS 15

由于这个帖子在三年后仍有流量,我想分享我认为目前功能最优雅的解决方案。它也适用于SwiftUI。

UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.first

iOS 15和16,兼容至iOS 13

UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }

注意,connectedScenes仅在iOS 13之后可用。如果你需要支持早期版本的iOS,你必须把它放在if #available(iOS 13, *)语句中。

变体:更长,但更容易理解的变体:

UIApplication
.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }

iOS 13和14

下面的历史答案在iOS 15上仍然有效,但应该被替换,因为UIApplication.shared.windows已弃用。感谢@matt指出这一点!

<强>原始答:< / >强

在matt的精彩回答基础上稍作改进,这个回答更简单、更简短、更优雅:

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

理想情况下,由于它已经被弃用,我建议你将窗口存储在SceneDelegate中。但是,如果您确实想要一个临时的解决方案,您可以创建一个过滤器并像这样检索keyWindow。

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

对于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;
}
我也遇到了同样的问题。 我为视图分配了newWindow,并将其设置为[newWindow makeKeyAndVisible]; 当完成使用时,将其设置为[newWindow resignKeyWindow]; 然后尝试直接通过[UIApplication sharedApplication].keyWindow来显示原始的键窗口

在iOS 12上一切正常,但在iOS 13上原始的键窗口不能正常显示。它显示整个白色屏幕。

我通过以下方法解决了这个问题:

UIWindow *mainWindow = nil;
if ( @available(iOS 13.0, *) ) {
mainWindow = [UIApplication sharedApplication].windows.firstObject;
[mainWindow makeKeyWindow];
} else {
mainWindow = [UIApplication sharedApplication].keyWindow;
}

希望能有所帮助。

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
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;
}
}
}
}

贝尔尼的答案启发

let keyWindow = Array(UIApplication.shared.connectedScenes)
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first(where: { $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
}

试试这个:

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

通常使用

斯威夫特5

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

另外,在UIViewController中:

self.view.window

view.window是当前的场景窗口

< p > WWDC 2019: enter image description here < / p >

关键的窗口

  • 手动跟踪窗口

对于Objective-C解决方案也是如此

@implementation UIWindow (iOS13)


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


@end

由于许多开发人员要求将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类中创建并添加了这个方法作为类方法,并以下面的非常简单的方式使用它。

[AppDelegate keyWindow];

不要忘记像下面这样在AppDelegate.h类中添加这个方法。

+(UIWindow*)keyWindow;
- (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;
}

如果你想在任何ViewController中使用它,那么你可以简单地使用。

self.view.window

Berni的代码很好,但它不工作时,应用程序从后台回来。

这是我的代码:

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
}
}

.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
}
}

如果你的应用还没有更新到采用基于场景的应用生命周期,另一种获取活动窗口对象的简单方法是通过UIApplicationDelegate:

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

我有回答了一个重复的问题,因为我在这里找不到一个答案,提供了这么多的代码(注释),下面是我的贡献:

(在运行于Xcode 13.2.1的iOS 15.2上测试)

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 中找到所呈现的UIViewController,这里是另一个你可能会发现有用的extension:

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添加到UIViewController

这允许我添加更多有用的扩展,比如更容易表示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)
}
}
    

}

如果你使用SwiftLint的'first_where'规则,并想静音交战:

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

支持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
}

我已经解决了:

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

您可能知道,由于可能存在多个场景,因此不建议使用键窗口。最方便的解决方案是提供一个currentWindow作为扩展,然后进行搜索和替换。

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

对于iOS 16,我使用了以下方法:

let keyWindow = UIApplication.shared.currentUIWindow()?.windowScene?.keyWindow

Objective 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;
}