From 1c742fe8233d1a07a5b16ad8d35f63b9794d91a6 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 16 Oct 2026 03:11:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20iOS=2027=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=95=B1=EC=9D=B4=20=EC=8B=A4=ED=96=89=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20UIScene=20?= =?UTF-8?q?=EC=83=9D=EB=AA=85=EC=A3=BC=EA=B8=B0=EB=A1=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS 27 SDK로 링크된 앱은 실행 시점에 UIKit이 Scene 매니페스트를 검사한다. 우리 앱은 매니페스트 없이 AppDelegate가 UIWindow(frame:)로 창을 직접 띄우는 예전 방식이라 이 검사(UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption) 에서 EXC_BREAKPOINT로 멈추고, iOS 27 베타 기기에서 앱이 열리지 않았다. iOS 26.6.1 기기에서는 같은 TestFlight 빌드(1.7.2/22)가 정상 실행됐다. - SceneDelegate를 얇은 어댑터로 추가한다. RN은 지금처럼 didFinishLaunching에서 시작하고, 그 window를 연결된 UIWindowScene에 붙인다. window를 새로 만들지 않는 건 RNFB messaging·expo-system-ui가 delegate.window를 읽기 때문이다 - Scene 생명주기에서는 iOS가 AppDelegate의 open url / continue userActivity를 부르지 않는다. cold(connectionOptions)·warm(scene 콜백) URL을 모두 AppDelegate의 기존 메서드로 위임해 firebaseauth 필터 → Expo 구독자 → RCTLinkingManager 순서를 그대로 탄다. RCTLinkingManager를 직접 부르면 expo-linking 구독자를 건너뛰어 expo-router가 읽는 ExpoLinkingRegistry.initialURL이 비고, cold start 딥링크가 홈으로 떨어진다 - withIosPrebuildFixes가 매니페스트와 SceneDelegate를 생성하게 해서 expo prebuild --clean 뒤에도 유지된다 Expo SDK 54의 ExpoAppDelegate에는 Scene 지원이 없어 SDK 업그레이드 대신 이 방식을 택했다. Co-Authored-By: Claude Opus 5.5 (1M context) --- ios/app.xcodeproj/project.pbxproj | 4 ++ ios/app/Info.plist | 17 ++++++ ios/app/SceneDelegate.swift | 53 +++++++++++++++++++ .../ios-prebuild-support/SceneDelegate.swift | 53 +++++++++++++++++++ plugins/withIosPrebuildFixes.js | 35 ++++++++++-- 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 ios/app/SceneDelegate.swift create mode 100644 plugins/ios-prebuild-support/SceneDelegate.swift diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj index eb67042..5bb25a4 100644 --- a/ios/app.xcodeproj/project.pbxproj +++ b/ios/app.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 82AA7221D96D78101B265EA1 /* Pods_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2FE34E4E96E79AB979AE79F /* Pods_app.framework */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; + F1A5CE0D2EA0000000000002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A5CE0D2EA0000000000001 /* SceneDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -31,6 +32,7 @@ ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = app/AppDelegate.swift; sourceTree = ""; }; F11748442D0722820044C1D9 /* app-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "app-Bridging-Header.h"; path = "app/app-Bridging-Header.h"; sourceTree = ""; }; + F1A5CE0D2EA0000000000001 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = app/SceneDelegate.swift; sourceTree = ""; }; FFC3E90B83FE5881E7191A28 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = app/PrivacyInfo.xcprivacy; sourceTree = ""; }; /* End PBXFileReference section */ @@ -50,6 +52,7 @@ isa = PBXGroup; children = ( F11748412D0307B40044C1D9 /* AppDelegate.swift */, + F1A5CE0D2EA0000000000001 /* SceneDelegate.swift */, F11748442D0722820044C1D9 /* app-Bridging-Header.h */, BB2F792B24A3F905000567C9 /* Supporting */, 13B07FB51A68108700A75B9A /* Images.xcassets */, @@ -403,6 +406,7 @@ buildActionMask = 2147483647; files = ( F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */, + F1A5CE0D2EA0000000000002 /* SceneDelegate.swift in Sources */, 4EB67CDF4FA2FA6DB261C2AF /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/app/Info.plist b/ios/app/Info.plist index 2d7ec17..923fdb6 100644 --- a/ios/app/Info.plist +++ b/ios/app/Info.plist @@ -59,6 +59,23 @@ 동아리 지원 시 입력한 이름과 전화번호를 동아리 관리자에게 전달하는 용도로만 사용됩니다. RCTNewArchEnabled + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UILaunchStoryboardName SplashScreen UIRequiredDeviceCapabilities diff --git a/ios/app/SceneDelegate.swift b/ios/app/SceneDelegate.swift new file mode 100644 index 0000000..2bd3582 --- /dev/null +++ b/ios/app/SceneDelegate.swift @@ -0,0 +1,53 @@ +import UIKit + +// iOS 27 SDK부터 UIScene 생명주기가 필수라 추가한 얇은 어댑터. +// React Native는 AppDelegate가 띄운 window를 그대로 쓰고, URL 진입은 AppDelegate로 위임해 +// firebaseauth 필터 → Expo 구독자(expo-linking, expo-router) → RCTLinkingManager 순서를 유지한다. +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard + let windowScene = scene as? UIWindowScene, + let appWindow = (UIApplication.shared.delegate as? AppDelegate)?.window + else { + return + } + + appWindow.windowScene = windowScene + appWindow.makeKeyAndVisible() + window = appWindow + + // Scene 생명주기에서는 cold start URL이 launchOptions가 아니라 connectionOptions로 들어온다. + self.scene(scene, openURLContexts: connectionOptions.urlContexts) + for userActivity in connectionOptions.userActivities { + self.scene(scene, continue: userActivity) + } + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + for context in URLContexts { + var options: [UIApplication.OpenURLOptionsKey: Any] = [:] + if let sourceApplication = context.options.sourceApplication { + options[.sourceApplication] = sourceApplication + } + _ = appDelegate.application(UIApplication.shared, open: context.url, options: options) + } + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + _ = appDelegate.application(UIApplication.shared, continue: userActivity, restorationHandler: { _ in }) + } +} diff --git a/plugins/ios-prebuild-support/SceneDelegate.swift b/plugins/ios-prebuild-support/SceneDelegate.swift new file mode 100644 index 0000000..2bd3582 --- /dev/null +++ b/plugins/ios-prebuild-support/SceneDelegate.swift @@ -0,0 +1,53 @@ +import UIKit + +// iOS 27 SDK부터 UIScene 생명주기가 필수라 추가한 얇은 어댑터. +// React Native는 AppDelegate가 띄운 window를 그대로 쓰고, URL 진입은 AppDelegate로 위임해 +// firebaseauth 필터 → Expo 구독자(expo-linking, expo-router) → RCTLinkingManager 순서를 유지한다. +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard + let windowScene = scene as? UIWindowScene, + let appWindow = (UIApplication.shared.delegate as? AppDelegate)?.window + else { + return + } + + appWindow.windowScene = windowScene + appWindow.makeKeyAndVisible() + window = appWindow + + // Scene 생명주기에서는 cold start URL이 launchOptions가 아니라 connectionOptions로 들어온다. + self.scene(scene, openURLContexts: connectionOptions.urlContexts) + for userActivity in connectionOptions.userActivities { + self.scene(scene, continue: userActivity) + } + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + for context in URLContexts { + var options: [UIApplication.OpenURLOptionsKey: Any] = [:] + if let sourceApplication = context.options.sourceApplication { + options[.sourceApplication] = sourceApplication + } + _ = appDelegate.application(UIApplication.shared, open: context.url, options: options) + } + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + _ = appDelegate.application(UIApplication.shared, continue: userActivity, restorationHandler: { _ in }) + } +} diff --git a/plugins/withIosPrebuildFixes.js b/plugins/withIosPrebuildFixes.js index fd4fa57..2d1a073 100644 --- a/plugins/withIosPrebuildFixes.js +++ b/plugins/withIosPrebuildFixes.js @@ -1,9 +1,23 @@ const fs = require('fs'); const path = require('path'); -const { withFinalizedMod, withPodfile, withXcodeProject } = require('@expo/config-plugins'); +const { IOSConfig, withFinalizedMod, withInfoPlist, withPodfile, withXcodeProject } = require('@expo/config-plugins'); const SUPPORT_DIR = path.join(__dirname, 'ios-prebuild-support'); +// iOS 27 SDK부터 UIScene 생명주기가 필수다. SceneDelegate는 AppDelegate의 window와 URL 처리를 그대로 재사용한다. +const SCENE_DELEGATE_FILE = 'SceneDelegate.swift'; +const SCENE_MANIFEST = { + UIApplicationSupportsMultipleScenes: false, + UISceneConfigurations: { + UIWindowSceneSessionRoleApplication: [ + { + UISceneConfigurationName: 'Default Configuration', + UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate', + }, + ], + }, +}; + const RNFB_SCRIPT_PHASE_NAMES = new Set([ '[CP-User] [RNFB] Core Configuration', '[CP-User] [RNFB] Crashlytics Configuration', @@ -143,7 +157,12 @@ function normalizeXcodeString(value) { .replace(/\\"/g, '"'); } -function applyXcodeProjectFixes(project) { +function applyXcodeProjectFixes(project, projectName) { + const sceneDelegatePath = `${projectName}/${SCENE_DELEGATE_FILE}`; + if (!project.hasFile(sceneDelegatePath)) { + IOSConfig.XcodeUtils.addBuildSourceFileToGroup({ filepath: sceneDelegatePath, groupName: projectName, project }); + } + const shellScriptBuildPhases = project.hash?.project?.objects?.PBXShellScriptBuildPhase; for (const phase of Object.values(shellScriptBuildPhases || {})) { @@ -179,7 +198,8 @@ function removeAppTestsFromScheme(schemeContents) { }); } -function applyFinalizedFileFixes(platformProjectRoot) { +function applyFinalizedFileFixes(platformProjectRoot, projectName) { + copySupportFile(SCENE_DELEGATE_FILE, path.join(platformProjectRoot, projectName, SCENE_DELEGATE_FILE)); copySupportFile('xcode.env', path.join(platformProjectRoot, '.xcode.env')); copySupportFile('ci_post_clone.sh', path.join(platformProjectRoot, 'ci_scripts', 'ci_post_clone.sh'), 0o755); copySupportFile( @@ -200,15 +220,20 @@ module.exports = function withIosPrebuildFixes(config) { return config; }); + config = withInfoPlist(config, (config) => { + config.modResults.UIApplicationSceneManifest = SCENE_MANIFEST; + return config; + }); + config = withXcodeProject(config, (config) => { - config.modResults = applyXcodeProjectFixes(config.modResults); + config.modResults = applyXcodeProjectFixes(config.modResults, config.modRequest.projectName); return config; }); return withFinalizedMod(config, [ 'ios', (config) => { - applyFinalizedFileFixes(config.modRequest.platformProjectRoot); + applyFinalizedFileFixes(config.modRequest.platformProjectRoot, config.modRequest.projectName); return config; }, ]); From 851febdb143a14ef720c33fc596d18a9585f3d67 Mon Sep 17 00:00:00 2001 From: seongwon seo Date: Fri, 16 Oct 2026 15:43:34 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20bump=20app=20version=20to=201.7.3?= =?UTF-8?q?=20=EC=95=B1=20=EB=B2=84=EC=A0=84=201.7.3,=20iOS=20buildNumber?= =?UTF-8?q?=2020,=20Android=20versionCode=2020.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.7.2가 이미 App Store에 출시돼 같은 버전으로는 새 빌드를 올릴 수 없다. iOS 27에서 앱이 열리지 않는 문제(#47) 수정 릴리즈라 patch 를 올린다. 지난 bump 커밋(865deb3)과 같은 3개 파일을 고친다. Co-Authored-By: Claude Opus 5.5 (1M context) --- app.json | 6 +++--- ios/app.xcodeproj/project.pbxproj | 8 ++++---- ios/app/Info.plist | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app.json b/app.json index 9ac14b9..94dcf18 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "모아동", "slug": "moadong-app", - "version": "1.7.2", + "version": "1.7.3", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "moadongapp", @@ -10,7 +10,7 @@ "newArchEnabled": true, "ios": { "supportsTablet": false, - "buildNumber": "19", + "buildNumber": "20", "googleServicesFile": "./GoogleService-Info.plist", "bundleIdentifier": "com.moadong.moadong", "associatedDomains": [ @@ -26,7 +26,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 19, + "versionCode": 20, "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/images/android-icon-foreground.png", diff --git a/ios/app.xcodeproj/project.pbxproj b/ios/app.xcodeproj/project.pbxproj index eb67042..c302f26 100644 --- a/ios/app.xcodeproj/project.pbxproj +++ b/ios/app.xcodeproj/project.pbxproj @@ -417,7 +417,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = app/app.entitlements; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = 2QMK9GBWN6; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -430,7 +430,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.7.2; + MARKETING_VERSION = 1.7.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -454,7 +454,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = app/app.entitlements; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = 2QMK9GBWN6; INFOPLIST_FILE = app/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -462,7 +462,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.7.2; + MARKETING_VERSION = 1.7.3; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/ios/app/Info.plist b/ios/app/Info.plist index 2d7ec17..6ce3787 100644 --- a/ios/app/Info.plist +++ b/ios/app/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.7.2 + 1.7.3 CFBundleSignature ???? CFBundleURLTypes @@ -39,7 +39,7 @@ CFBundleVersion - 19 + 20 LSMinimumSystemVersion 12.0 LSRequiresIPhoneOS