Как исправить ошибку “Fatal error: Cannot find the keyWindow. Make sure to call window.makeKeyAndVisible()” в React Native/Expo после обновления?
Я столкнулся с этой ошибкой после обновления версий Expo и React Native:
EXDevLauncher/ExpoDevLauncherAppDelegateSubscriber.swift:8: Fatal error: Cannot find the keyWindow. Make sure to call window.makeKeyAndVisible().
Я пытался изменить файл AppDelegate.m безуспешно. Ниже приведены мои файлы AppDelegate и SceneDelegate:
AppDelegate.m:
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>
@interface AppDelegate : EXAppDelegateWrapper
@end
AppDelegate.m (реализация):
#import "AppDelegate.h"
// @generated begin react-native-maps-import - expo prebuild (DO NOT MODIFY) sync-f2f83125c99c0d74b42a2612947510c4e08c423a
#if __has_include(<GoogleMaps/GoogleMaps.h>)
#import <GoogleMaps/GoogleMaps.h>
#endif
// @generated end react-native-maps-import
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if __has_include(<GoogleMaps/GoogleMaps.h>)
[GMSServices provideAPIKey:@"AIzaSyCDnK85Y_BEl8g-tdrdSl8eC2VGotnEB5k"];
#endif
self.moduleName = @"main";
self.initialProps = @{};
NSLog(@"[AppDelegate] didFinishLaunching begin");
// Вызываем super, но БЕЗ создания окна (SceneDelegate сделает это)
BOOL result = [super application:application didFinishLaunchingWithOptions:launchOptions];
NSLog(@"[AppDelegate] didFinishLaunching end");
return result;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
// Явно определяем делегаты удаленных уведомлений для обеспечения совместимости с некоторыми сторонними библиотеками
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// Явно определяем делегаты удаленных уведомлений для обеспечения совместимости с некоторыми сторонними библиотеками
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
}
// Явно определяем делегаты удаленных уведомлений для обеспечения совместимости с некоторыми сторонними библиотеками
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
@end
main.m:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
SceneDelegate.m:
#import "SceneDelegate.h"
#import <EXDevLauncher/EXDevLauncherController.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import "AppDelegate.h"
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
UIWindowScene *windowScene = (UIWindowScene *)scene;
NSLog(@"[SceneDelegate] willConnectToSession");
// Создаем окно для этого сцены
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
NSLog(@"[SceneDelegate] window created");
// Получаем AppDelegate для делегата dev launcher
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
// Делаем окно видимым ПОСЛЕ инициализации dev launcher
[self.window makeKeyAndVisible];
NSLog(@"[SceneDelegate] window makeKeyAndVisible called");
// Теперь запускаем dev launcher с окном
NSLog(@"[SceneDelegate] EXDevLauncherController startWithWindow called");
EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
[controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
NSLog(@"[SceneDelegate] setup complete, window is key and visible");
}
- (void)sceneDidDisconnect:(UIScene *)scene {
NSLog(@"[SceneDelegate] sceneDidDisconnect");
}
- (void)sceneDidBecomeActive:(UIScene *)scene {
NSLog(@"[SceneDelegate] sceneDidBecomeActive");
}
- (void)sceneWillResignActive:(UIScene *)scene {
NSLog(@"[SceneDelegate] sceneWillResignActive");
}
- (void)sceneWillEnterForeground:(UIScene *)scene {
NSLog(@"[SceneDelegate] sceneWillEnterForeground");
}
- (void)sceneDidEnterBackground:(UIScene *)scene {
NSLog(@"[SceneDelegate] sceneDidEnterBackground");
}
@end
AppDelegate+SceneConfiguration.m:
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "SceneDelegate.h"
// Эта категория предоставляет UIScene конфигурацию во время выполнения, чтобы приложение
// использовало SceneDelegate, даже если Info.plist не объявляет UIApplicationSceneManifest.
@implementation AppDelegate (SceneConfiguration)
- (UISceneConfiguration *)application:(UIApplication *)application
configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
options:(UISceneConnectionOptions *)options
{
UISceneConfiguration *config = [UISceneConfiguration configurationWithName:@"Default Configuration"
sessionRole:connectingSceneSession.role];
config.delegateClass = [SceneDelegate class];
return config;
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions
{
// No-op. Реализовано для полного принятия жизненного цикла UIScene.
}
@end
Что вызывает эту ошибку и как можно её исправить в моем приложении React Native/Expo после обновления?
После обновления версий Expo и React Native ошибка “Fatal error: Cannot find the keyWindow” обычно возникает из-за проблем с синхронизацией между инициализацией SceneDelegate и попыткой EXDevLauncher получить доступ к окну. Это распространенная проблема при переходе на архитектуру на основе Scene в iOS 13+, где жизненный цикл окна значительно изменился.
Содержание
- Причины возникновения проблемы
- Мгновенное решение: исправление синхронизации SceneDelegate
- Альтернативные обходные пути
- Предотвращение и будущая совместимость
- Расширенное устранение неполадок
Причины возникновения проблемы
Ошибка возникает, потому что EXDevLauncher ожидает найти активное keyWindow во время инициализации, но в современной архитектуре iOS на основе Scene окно становится доступным не сразу. Согласно проблеме #23536 в репозитории Expo на GitHub, это происходит, когда в вашем Info.plist определен UIApplicationSceneManifest для поддержки нескольких экранов или совместимости с CarPlay.
Основная проблема — это состояние гонки между:
- Созданием окна SceneDelegate
- Попыткой EXDevLauncher получить доступ к этому окну
- Временными рамками вызова
makeKeyAndVisible()
Ключевое замечание: Сообщение об ошибке указывает конкретно на
EXDevLauncher/ExpoDevLauncherAppDelegateSubscriber.swift:8, что означает, что проблема происходит в коде лаунчера разработки Expo, а не в вашем пользовательском коде.
Мгновенное решение: исправление синхронизации SceneDelegate
Наиболее надежным решением является обеспечение правильной инициализации окна и его отображения перед тем, как EXDevLauncher попытается его использовать. Вот исправленный код SceneDelegate.m:
#import "SceneDelegate.h"
#import <EXDevLauncher/EXDevLauncherController.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import "AppDelegate.h"
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) { return; }
UIWindowScene *windowScene = (UIWindowScene *)scene;
NSLog(@"[SceneDelegate] willConnectToSession");
// Создаем окно для этого сцена
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
// КРИТИЧЕСКИ ВАЖНО: Делаем окно видимым ПЕРЕД инициализацией лаунчера разработки
[self.window makeKeyAndVisible];
NSLog(@"[SceneDelegate] window made key and visible");
// Получаем AppDelegate для делегата лаунчера разработки
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
// Теперь запускаем лаунчер разработки с уже видимым окном
NSLog(@"[SceneDelegate] EXDevLauncherController startWithWindow called");
EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
[controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
NSLog(@"[SceneDelegate] setup complete");
}
Основные изменения:
- Вызов
makeKeyAndVisible()перемещен перед инициализацией EXDevLauncher - Устранено возможное состояние гонки путем обеспечения готовности окна
- Упрощен поток для избежания промежуточных состояний
Альтернативные обходные пути
Метод 1: Задержанная инициализация EXDevLauncher
Если вышеописанное не работает, добавьте небольшую задержку:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
EXDevLauncherController *controller = [EXDevLauncherController sharedInstance];
[controller startWithWindow:self.window delegate:appDelegate launchOptions:nil];
});
Метод 2: Проверка существующих окон
Добавьте проверку безопасности в ваш AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Проверяем наличие валидного key window
UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow;
if (!keyWindow) {
// Создаем временное окно, если его нет
UIWindowScene *windowScene = [UIApplication.sharedApplication.connectedScenes anyObject];
if ([windowScene isKindOfClass:[UIWindowScene class]]) {
keyWindow = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)windowScene];
[keyWindow makeKeyAndVisible];
}
}
// Продолжаем нормальную инициализацию
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
Метод 3: Обновление зависимостей Expo
Согласно истории обновления Expo, убедитесь, что вы используете совместимые версии:
npx expo install expo@latest expo-dev-client@latest
Предотвращение и будущая совместимость
1. Использование современной архитектуры iOS
Убедитесь, что ваше приложение полностью использует паттерн SceneDelegate:
Info.plist должен включать:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
2. Правильная обработка жизненного цикла окна
Добавьте управление жизненным циклом окна:
- (void)sceneDidBecomeActive:(UIScene *)scene {
if ([scene isKindOfClass:[UIWindowScene class]]) {
[self.window makeKeyAndVisible];
}
}
3. Мониторинг обновлений Expo
Подпишитесь на проблемы в репозитории Expo на GitHub для решения подобных проблем. Сообщество часто предоставляет патчи до официальных релизов.
Расширенное устранение неполадок
Если проблема все еще сохраняется:
Проверка конфликтующих библиотек
Некоторые библиотеки могут мешать инициализации окна. Временно удалите сторонние библиотеки для изоляции проблемы.
Отладка с точками останова
Добавьте точки останова в SceneDelegate.m для отслеживания точной последовательности:
// Добавьте эти строки в willConnectToSession
NSLog(@"[DEBUG] About to create window");
// Точка останова здесь
NSLog(@"[DEBUG] Window created: %@", self.window);
// Точка останова здесь
NSLog(@"[DEBUG] About to make key and visible");
// Точка останова здесь
Проверка настроек проекта Xcode
Убедитесь, что ваш проект Xcode имеет:
- Цель развертывания: iOS 13.0+
- Манифест сцены приложения: Включен
- Использование сцен: Да
Очистка и пересборка
npx expo start --clear
rm -rf ios/build
cd ios && xcodebuild clean -scheme YourApp -configuration Debug
Заключение
Ошибка “Cannot find the keyWindow” после обновления Expo обычно вызвана проблемами синхронизации в архитектуре на основе Scene. Наиболее эффективным решением является обеспечение правильной инициализации окна и его отображения перед тем, как EXDevLauncher попытается его использовать.
Ключевые выводы:
- Переместите вызов
makeKeyAndVisible()перед инициализацией EXDevLauncher в SceneDelegate - Убедитесь в правильной конфигурации Info.plist с UIApplicationSceneManifest
- Держите зависимости Expo обновленными до последних совместимых версий
- Добавьте правильную обработку ошибок для событий жизненного цикла окна
- Следите за обсуждениями в сообществе для решения подобных проблем и обходных путей
Если проблема продолжает возникать, ознакомьтесь с проблемами в репозитории Expo на GitHub или обратитесь к сообществу на Reddit r/reactnative для устранения неполадок, специфичных для вашей версии.
Источники
- Проблема #23536 в репозитории Expo на GitHub - expo-dev-launcher не работает с UIApplicationSceneManifest
- Medium - Сага обновления модулей Expo (Expo 51 → 53)
- Stack Overflow - Как исправить keywindow после обновления expo и react
- Форумы разработчиков Apple - iOS 15: UIApplication.shared.keyWindow равен nil
- Stack Overflow - ‘keyWindow’ устарел в iOS 13.0