НейроАгент

Как исправить ошибку keyWindow в React Native Expo после обновления

Узнайте, как исправить ошибку 'Cannot find the keyWindow' в React Native/Expo после обновления. Полное руководство с примерами кода и шагами по устранению неполадок.

Вопрос

Как исправить ошибку “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:

objc
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>

@interface AppDelegate : EXAppDelegateWrapper

@end

AppDelegate.m (реализация):

objc
#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:

objc
#import <UIKit/UIKit.h>

#import "AppDelegate.h"

int main(int argc, char * argv[]) {
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}

SceneDelegate.m:

objc
#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:

objc
#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+, где жизненный цикл окна значительно изменился.


Содержание


Причины возникновения проблемы

Ошибка возникает, потому что EXDevLauncher ожидает найти активное keyWindow во время инициализации, но в современной архитектуре iOS на основе Scene окно становится доступным не сразу. Согласно проблеме #23536 в репозитории Expo на GitHub, это происходит, когда в вашем Info.plist определен UIApplicationSceneManifest для поддержки нескольких экранов или совместимости с CarPlay.

Основная проблема — это состояние гонки между:

  1. Созданием окна SceneDelegate
  2. Попыткой EXDevLauncher получить доступ к этому окну
  3. Временными рамками вызова makeKeyAndVisible()

Ключевое замечание: Сообщение об ошибке указывает конкретно на EXDevLauncher/ExpoDevLauncherAppDelegateSubscriber.swift:8, что означает, что проблема происходит в коде лаунчера разработки Expo, а не в вашем пользовательском коде.


Мгновенное решение: исправление синхронизации SceneDelegate

Наиболее надежным решением является обеспечение правильной инициализации окна и его отображения перед тем, как EXDevLauncher попытается его использовать. Вот исправленный код SceneDelegate.m:

objc
#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");
}

Основные изменения:

  1. Вызов makeKeyAndVisible() перемещен перед инициализацией EXDevLauncher
  2. Устранено возможное состояние гонки путем обеспечения готовности окна
  3. Упрощен поток для избежания промежуточных состояний

Альтернативные обходные пути

Метод 1: Задержанная инициализация EXDevLauncher

Если вышеописанное не работает, добавьте небольшую задержку:

objc
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:

objc
- (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, убедитесь, что вы используете совместимые версии:

bash
npx expo install expo@latest expo-dev-client@latest

Предотвращение и будущая совместимость

1. Использование современной архитектуры iOS

Убедитесь, что ваше приложение полностью использует паттерн SceneDelegate:

Info.plist должен включать:

xml
<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. Правильная обработка жизненного цикла окна

Добавьте управление жизненным циклом окна:

objc
- (void)sceneDidBecomeActive:(UIScene *)scene {
  if ([scene isKindOfClass:[UIWindowScene class]]) {
    [self.window makeKeyAndVisible];
  }
}

3. Мониторинг обновлений Expo

Подпишитесь на проблемы в репозитории Expo на GitHub для решения подобных проблем. Сообщество часто предоставляет патчи до официальных релизов.


Расширенное устранение неполадок

Если проблема все еще сохраняется:

Проверка конфликтующих библиотек

Некоторые библиотеки могут мешать инициализации окна. Временно удалите сторонние библиотеки для изоляции проблемы.

Отладка с точками останова

Добавьте точки останова в SceneDelegate.m для отслеживания точной последовательности:

objc
// Добавьте эти строки в 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+
  • Манифест сцены приложения: Включен
  • Использование сцен: Да

Очистка и пересборка

bash
npx expo start --clear
rm -rf ios/build
cd ios && xcodebuild clean -scheme YourApp -configuration Debug

Заключение

Ошибка “Cannot find the keyWindow” после обновления Expo обычно вызвана проблемами синхронизации в архитектуре на основе Scene. Наиболее эффективным решением является обеспечение правильной инициализации окна и его отображения перед тем, как EXDevLauncher попытается его использовать.

Ключевые выводы:

  1. Переместите вызов makeKeyAndVisible() перед инициализацией EXDevLauncher в SceneDelegate
  2. Убедитесь в правильной конфигурации Info.plist с UIApplicationSceneManifest
  3. Держите зависимости Expo обновленными до последних совместимых версий
  4. Добавьте правильную обработку ошибок для событий жизненного цикла окна
  5. Следите за обсуждениями в сообществе для решения подобных проблем и обходных путей

Если проблема продолжает возникать, ознакомьтесь с проблемами в репозитории Expo на GitHub или обратитесь к сообществу на Reddit r/reactnative для устранения неполадок, специфичных для вашей версии.


Источники

  1. Проблема #23536 в репозитории Expo на GitHub - expo-dev-launcher не работает с UIApplicationSceneManifest
  2. Medium - Сага обновления модулей Expo (Expo 51 → 53)
  3. Stack Overflow - Как исправить keywindow после обновления expo и react
  4. Форумы разработчиков Apple - iOS 15: UIApplication.shared.keyWindow равен nil
  5. Stack Overflow - ‘keyWindow’ устарел в iOS 13.0