Я использую Core Data с Cloud Kit и поэтому должен проверять статус пользователя iCloud во время запуска приложения. В случае проблем я хочу выдать диалог пользователю, и я делаю это с помощью UIApplication.shared.keyWindow?.rootViewController?.present(...) до сих пор.
В Xcode 11 beta 4 появилось новое сообщение об устаревании, в котором говорится:
'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes
Как я должен представить диалог вместо этого?
В iOS больше нет концепции «keyWindow», так как одно приложение может иметь несколько окон. Вы можете сохранить созданное окно в своем SceneDelegate (если вы используете SceneDelegate)
@Sudara: Итак, если у меня еще нет контроллера представления, но я хочу представить предупреждение - как это сделать со сценой? Как получить сцену, чтобы можно было получить ее rootViewController? (Итак, короче: что такое сцена, эквивалентная «общему» для UIApplication?)





Это мое решение:
let keyWindow = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?.windows
.filter({$0.isKeyWindow}).first
Использование, например:
keyWindow?.endEditing(true)
Спасибо - не очень интуитивно понятно... 8-)
Тем временем я протестировал этот подход на примере нескольких сцен (developer.apple.com/documentation/uikit/app_and_environment/…), и все сработало, как и ожидалось.
Вам просто нужно получить isKeyWindow.
Здесь также может быть уместно проверить значение activationStateforegroundInactive, что в моем тестировании будет иметь место, если будет представлено предупреждение.
@Drew это нужно проверить, потому что при запуске приложения контроллер представления уже виден, но состояние foregroundInactive
Этот код выдает для меня keyWindow = nil. matt решение - это то, которое работает.
Это решение на самом деле не работает для меня в тех случаях, когда оно вызывается во время применения applicationWillEnterForeground. Решение, которое предлагает @matt, работает.
Я обновил этот ответ гораздо более производительной версией кода, которая должна дать тот же результат. Дайте мне знать, если я ошибаюсь!
.map({$0 as? UIWindowScene}).compactMap({$0}) можно заменить на .compactMap { $0 as? UIWindowScene }Нет, к сожалению, это неправильный ответ — он не работает, когда у вас есть две сцены рядом на iPad. У меня есть сообщение здесь: tengl.net/blog/2021/11/9/uiapplication-key-window-replacement
правда, иногда у меня нет попыток получить доступ таким образом
Редактировать Предложение, которое я здесь делаю, устарело в iOS 15. Что теперь? Что ж, если приложение не имеет нескольких собственных окон, я полагаю, что общепринятым современным способом было бы получить первое из connectedScenes приложения, принудить к UIWindowScene и взять его первое окно. Но это почти то, что делает принятый ответ! Так что мой обходной путь на данный момент кажется довольно слабым. Тем не менее, я оставлю это в силе по историческим причинам.
Принятый ответ, хотя и гениальный, может быть чрезмерно сложным. Точно такой же результат можно получить намного проще:
UIApplication.shared.windows.filter {$0.isKeyWindow}.first
Я также хотел бы предостеречь, что отказ от keyWindow не следует воспринимать слишком серьезно. Полное предупреждающее сообщение гласит:
'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes
Поэтому, если вы не поддерживаете несколько окон на iPad, не возражаете против продолжения использования keyWindow.
Как бы вы справились с таким переходом let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "homeVC") as! UITabBarController UIApplication.shared.keyWindow?.rootViewController = vc, потому что с iOS 13 и карточным представлением это становится проблемой, потому что пользователь после, скажем, выхода из системы будет перемещен на экран входа в главное приложение в иерархии представлений, где они могут провести пальцем вниз и вернуться что проблематично.
@LukasBimba Это кажется не связанным; не могли бы вы задать его как отдельный вопрос? Так будет проще помочь. Спасибо.
Вот вопрос: stackoverflow.com/questions/58151928/…
Привет @мат. У меня такой сценарий. У меня есть окно, открытое на весь экран, и другое окно в качестве наложения на сцену. Когда я нажимаю кнопку в полноэкранном режиме и представляю контроллер представления (путем доступа к keyWindow с использованием приведенного выше ответа), он отображается в контроллере представления окна наложения. Почему keyWindow не меняется на тот, с которым я взаимодействовал?
@RakeshaShastri Если вы нажимаете кнопку в полноэкранном режиме, вы не нужно ключевое окно. Вам не нужно окно Любые. Вы просто представляете self (контроллер представления, которому принадлежит кнопка).
@матовый да. я знаю об этом. Но я делаю пользовательский переход, где у меня есть контроллер представления внутри представления. Это представление добавляется в качестве подвида к полноэкранному представлению. Так что в конечном итоге мне нужен контроллер корневого представления окна. Прямо сейчас я вызываю makeKey() в окне представления перед доступом к keyWindow.
@RakeshaShastri Не могли бы вы задать об этом отдельный вопрос? Комментарии не место, чтобы сгладить это.
Как только у меня будет время, я сделаю простой, полный, проверяемый пример.
@RakeshaShastri :) Звучит здорово
Привет @мат. В новом проекте с раскадровкой UIApplication.shared.windows.filter {$0.isKeyWindow}.first всегда возвращает ноль, если я пытаюсь получить к нему доступ из начального контроллера представления. почему это?
Почему первое окно в массиве? Когда я прочитал документацию для -makeKeyAndVisible UIWindow и -windows UIApplication, мне показалось, что нам нужно последнее окно. Что мне не хватает?
@Mario Mario Это не первое окно в массиве окон. Это первое окно ключ в массиве окон.
Я думал, что в массиве окон будут окна из всех существующих сцен, и что каждая существующая сцена может иметь ключевое окно. Если это не так, то что я неправильно понимаю? Если это так, как мы узнаем, что хотим первого, а не последнего? Спасибо!
@Mario Но вопрос предполагает, что есть только одна сцена. Решаемая проблема - это просто устаревание определенного свойства. Очевидно, что жизнь намного сложнее, если у вас на самом деле несколько окон на iPad! Если вы действительно пытаетесь написать многооконное приложение для iPad, удачи вам.
рассмотрите возможность использования first(where:), так как это более эффективно, чем ваше решение (оно не создает промежуточный массив отфильтрованных элементов)
не хотел вас обидеть, просто заметил, что у вашего решения много плюсов, и многие люди будут его использовать, хотя оно могло бы быть и лучше
@ramzesenok Конечно, могло быть и лучше. Но это не так. Напротив, я был первым, кто предположил, что может быть достаточно запросить у приложения окно, которое является ключевым окном, тем самым избегая устаревания свойства keyWindow. Отсюда и плюсы. Если вам это не нравится, поставьте минус. Но не говорите мне изменить его, чтобы он соответствовал чужому ответу; это, как я уже сказал, было бы неправильно.
@Mario сказал: «Я думал [...], что каждая существующая сцена может иметь ключевое окно [...]». Документация указывает: «Только одно окно за раз может быть ключевым окном [для приложения, а не место действия]." Насколько я понимаю: даже если у вас есть несколько сцен вашего приложения, ключевое окно первый в массиве окон также является окном Только.
Теперь это также можно упростить как UIApplication.shared.windows.first(where: \.isKeyWindow)
@dadalar Да, мне очень нравится этот синтаксис (новое в Swift 5.2).
@LukasBimba, вы можете заменить свой код на этот UIApplication.shared.windows.first?.rootViewController = vc или просто на этот self.view.window?.rootViewController = vc
Принятый ответ на самом деле не работает для меня в тех случаях, когда он вызывается во время applicationWillEnterForeground. Решение, которое предлагает @matt, работает.
Права редактирования @BenLeggiero не должны использоваться ни для замены основного содержания ответа основным содержанием другого ответа, ни для введения собственного стиля письма.
@pommy Основной контент остался прежним (изменилась только производительность, а не поведение или подход), а мой стиль отличается. Как сказано на странице справки, каждый раз, когда вы видите сообщение, которое нуждается в улучшении, и склонны предложить редактирование, вы можете сделать это. [...] Ожидается, что правки будут существенными и оставят пост лучше, чем вы его нашли. Для нас с правами редактирования редактирование приветствуется!
@BenLeggiero Хорошо, но превращать мой ответ в ответ Помми не рекомендуется. Не используйте редактирование, чтобы лгать об истории. История такова, что я дал ответ, Помми дал другой ответ, возможно, лучший. Это нужно стоять.
Прости, что обидел тебя, @matt; это не было моим намерением. Я также не собирался лгать об истории. Я надеялся помочь людям, пришедшим сюда из Google, пытающимся найти быстрый ответ, которым может быть неинтересно читать все ответы, чтобы найти новые. Я считаю историю редактирования историей ответа, а не связанными ответами рядом с этим.
Честно говоря, я думал, что то, что я делаю, было НАСТОЛЬКО передовой практикой. Я задал вопрос в Meta, чтобы понять, что здесь произошло: meta.stackoverflow.com/q/401472/3939277
@BenLeggiero Я тоже так думал, пока не увидел, что ты делаешь мой ответ похожим на ответ Помми. Это мой ответ, поэтому мне нужно занять определенную позицию, и моя позиция такова, к лучшему или к худшему, это то, что я сказал. Я проголосовал за ответ Помми как за лучшее выражение моего. Ваше редактирование заставляет меня «украсть» его ответ, и я не хочу этого делать. Когда вы редактируете меня, вы вкладываете свои слова в рот мой, так что это нужно учитывать.
@matt большое спасибо, ваше простое решение решило мою большую проблему! :)
Даже если вы являются поддерживаете несколько окон, вы должны продолжать использовать его, так как нет хорошего обходного пути. У меня есть пост, в котором обсуждались ограничения: tengl.net/blog/2021/11/9/uiapplication-key-window-replacement Но в основном вы правы. Ваш ответ должен быть на первом месте.
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 за указание на это!
Оригинальный ответ:
Немного улучшив превосходный ответ Мэтта, он стал еще проще, короче и элегантнее:
UIApplication.shared.windows.first { $0.isKeyWindow }
Спасибо! Есть ли способ сделать это в задаче c?
@Allenktv К сожалению, у NSArray нет эквивалента first(where:). Вы можете попробовать составить однострочник с filteredArrayUsingPredicate: и firstObject:.
@Allenktv код был искажен в разделе комментариев, поэтому я разместил эквивалент Objective-C ниже.
Компилятор Xcode 11.2 сообщил об ошибке с этим ответом и предложил добавить скобки и их содержимое в first(where:): UIApplication.shared.windows.first(where: { $0.isKeyWindow })
@YassineElBadaoui Скобки не нужны в простом присваивании 89
Теперь это также можно упростить как UIApplication.shared.windows.first(where: \.isKeyWindow)
@dadalar Ваше решение с ключевым путем, возможно, более элегантно, но также немного длиннее, поскольку вы не можете отбросить метку аргумента where, как в случае закрытия замыкания.
@pommy Правда, я думаю, это вопрос личного вкуса. Xcode обычно не делает отступы для конечных замыканий, поэтому мне больше нравится другой подход.
Это должно было быть опубликовано как редактирование ответа Мэтта.
Права редактирования @BenLeggiero не должны использоваться для замены основного содержания ответа основным содержанием другого ответа.
@pommy Основной контент тот же. У этого та же концепция и подход к решению, но, как вы сказали в тексте, он «проще, короче и элегантнее». Как сказано на странице справки, каждый раз, когда вы видите сообщение, которое нуждается в улучшении, и склонны предложить редактирование, вы можете сделать это. [...] Ожидается, что правки будут существенными и оставят пост лучше, чем вы его нашли. Для нас с правами редактирования редактирование приветствуется!
К сожалению, windows также устарел.
@matt Спасибо, я обновил свой ответ. Я не могу избежать создания временных массивов, как в моем исходном ответе, но, по крайней мере, их все еще меньше, чем в принятом ответе. Я надеюсь, что оптимизация компилятора поймет, что создавать их не нужно. Kotlin’s asSequence() здесь бы пригодился.
Этот метод не работает в одной ситуации: на iPad есть две сцены на разделенном экране, и обе они распознаются в этом методе как «Ключевое окно». Но UIApplication.shared.keyWindow возвращает один (и это всегда тот, с которым вы в последний раз взаимодействовали, обозначенный тремя приподнятыми точками сверху, если вы подключаетесь к клавиатуре)
@TengL вы можете просто использовать filter(\.isKeyWindow) вместо 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;
}
Не забудьте добавить nullable к объявлению заголовка!
О, как я не скучаю по Objective-C :)
Я столкнулся с той же проблемой.
Я выделил 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 })
Я проверил этот код очень странным образом, и он работал лучше остальных. Там, где другие вылетали, он работал независимо от переднего плана/фона...
Поскольку connectScenes — это набор, необходимо ли здесь преобразование в массив?
Вот обратно совместимый способ обнаружения 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 extension. ?
Проверки доступности вряд ли нужны, так как windows и isKeyWindow существуют с iOS 2.0, а first(where:) с Xcode 9.0/Swift 4/2017.
UIApplication.keyWindow устарело в iOS 13.0: @ available (iOS, введено: 2.0, устарело: 13.0, сообщение: «Не следует использовать для приложений, поддерживающих несколько сцен, поскольку он возвращает ключевое окно для всех подключенных сцен»)@ВадимБулавин ты не понял комментария Помми предложила только использоватьstatic var key: UIWindow? { UIApplication.shared.windows.first(where: \.isKeyWindow) }
попробуйте с этим:
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 — текущее окно для сцен
Key Windows
- Track windows manually
Для решения Objective-C тоже
@implementation UIWindow (iOS13)
+ (UIWindow*) keyWindow {
NSPredicate *isKeyWindow = [NSPredicate predicateWithFormat:@"isKeyWindow == YES"];
return [[[UIApplication sharedApplication] windows] filteredArrayUsingPredicate:isKeyWindow].firstObject;
}
@end
Как и многие разработчики, запрашивающие код Цель С замены этого устаревания. Вы можете использовать этот код ниже, чтобы использовать 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
Код Берни хорош, но он не работает, когда приложение возвращается из фона.
Это мой код:
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
У меня был ответил на вопрос о дублирующем фиде, и, поскольку я не смог найти здесь ответ с таким количеством кода (с комментариями), вот мой вклад:
(Протестировано с 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)
}
}
Если вы хотите найти представленный UIViewController в ключе UIWindow , вот еще один 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.
Это позволяет мне добавлять больше полезных расширений, например, для более удобного представления UIViewController, например:
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 })
Вы делаете это в
SceneDelegateилиAppDelegate? И не могли бы вы опубликовать немного больше кода, чтобы мы могли дублировать?