Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mobile-app/ios/syncup.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
G0S1234567890ABC12345678 /* GoServerBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = G0S1234567890ABC12345679 /* GoServerBridge.mm */; };
KA01234567890ABC12345002 /* KeepAliveManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = KA01234567890ABC12345001 /* KeepAliveManager.swift */; };
QL01234567890ABC12345002 /* QuickLookPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = QL01234567890ABC12345001 /* QuickLookPresenter.swift */; };
SD01234567890ABC12345002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = SD01234567890ABC12345001 /* SceneDelegate.swift */; };
SF01234567890ABC12345002 /* ScopedFolderStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = SF01234567890ABC12345001 /* ScopedFolderStore.swift */; };
/* End PBXBuildFile section */

Expand All @@ -48,6 +49,7 @@
G0S1234567890ABC12345680 /* GoServerBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = GoServerBridge.h; path = syncup/GoServerBridge.h; sourceTree = "<group>"; };
KA01234567890ABC12345001 /* KeepAliveManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = KeepAliveManager.swift; path = syncup/KeepAliveManager.swift; sourceTree = "<group>"; };
QL01234567890ABC12345001 /* QuickLookPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = QuickLookPresenter.swift; path = syncup/QuickLookPresenter.swift; sourceTree = "<group>"; };
SD01234567890ABC12345001 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = syncup/SceneDelegate.swift; sourceTree = "<group>"; };
SF01234567890ABC12345001 /* ScopedFolderStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScopedFolderStore.swift; path = syncup/ScopedFolderStore.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand All @@ -67,6 +69,7 @@
isa = PBXGroup;
children = (
F11748412D0307B40044C1D9 /* AppDelegate.swift */,
SD01234567890ABC12345001 /* SceneDelegate.swift */,
BM01234567890ABC12345001 /* BackgroundManager.swift */,
BM01234567890ABC12345003 /* BackgroundErrorNotifier.swift */,
BM01234567890ABC12345005 /* AppShortcuts.swift */,
Expand Down Expand Up @@ -367,6 +370,7 @@
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
SD01234567890ABC12345002 /* SceneDelegate.swift in Sources */,
BM01234567890ABC12345002 /* BackgroundManager.swift in Sources */,
BM01234567890ABC12345004 /* BackgroundErrorNotifier.swift in Sources */,
BM01234567890ABC12345006 /* AppShortcuts.swift in Sources */,
Expand Down
19 changes: 12 additions & 7 deletions mobile-app/ios/syncup/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import UserNotifications

@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
// Owned by SceneDelegate under the UIScene lifecycle; kept here because
// UIKit and some libraries still read `delegate.window`.
var window: UIWindow?

var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
/// Retained so SceneDelegate can hand them to startReactNative when the scene connects.
var launchOptions: [UIApplication.LaunchOptionsKey: Any]?

public override func application(
_ application: UIApplication,
Expand Down Expand Up @@ -44,18 +48,19 @@ public class AppDelegate: ExpoAppDelegate {

reactNativeDelegate = delegate
reactNativeFactory = factory
self.launchOptions = launchOptions

#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
// Window creation + startReactNative live in SceneDelegate.scene(_:willConnectTo:options:).
// iOS 27 refuses to launch SDK-27-linked apps that build the window here without
// adopting UIScene (see UIApplicationSceneManifest in Info.plist).

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// NOTE: with a scene manifest present UIKit no longer calls the app-level
// background/foreground/active callbacks; SceneDelegate forwards its scene
// callbacks here so the logic (and Expo subscriber fan-out via super) is unchanged.

public override func applicationDidEnterBackground(_ application: UIApplication) {
super.applicationDidEnterBackground(application)
// re-submit request on every backgrounding; iOS drops stale ones.
Expand Down
23 changes: 23 additions & 0 deletions mobile-app/ios/syncup/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@
<string>SyncUp discovers peer devices on your local network to sync files directly without a server.</string>
<key>RCTNewArchEnabled</key>
<true/>
<!--
UIScene lifecycle. Required for apps linked against the iOS 27 SDK:
without it iOS 27 traps at launch in
_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption.
The window + React Native root are created in SceneDelegate.swift.
-->
<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>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
Expand Down
93 changes: 93 additions & 0 deletions mobile-app/ios/syncup/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Expo
import React
import UIKit

/// UIScene lifecycle adoption.
///
/// Apps linked against the iOS 27 SDK must adopt the UIScene lifecycle. On iOS 27
/// the legacy UIApplicationDelegate-owned window path traps at launch inside
/// `_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption` (EXC_BREAKPOINT),
/// so the app never gets past scene creation.
///
/// The scene owns the UIWindow and starts React Native into it. App-level
/// concerns (Go daemon, BGTaskScheduler registration, notifications, App
/// Shortcuts) stay in AppDelegate.didFinishLaunching, which still runs before
/// the first scene connects. Foreground/background transitions are forwarded to
/// the AppDelegate handlers so ExpoAppDelegate subscribers keep receiving them;
/// UIKit stops calling those app-level methods once a scene manifest exists.
public class SceneDelegate: UIResponder, UIWindowSceneDelegate {
public var window: UIWindow?

private var appDelegate: AppDelegate? {
UIApplication.shared.delegate as? AppDelegate
}

public func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
guard let appDelegate = appDelegate, let factory = appDelegate.reactNativeFactory else {
NSLog("SceneDelegate: AppDelegate or ReactNativeFactory missing; cannot start React Native")
return
}

let window = UIWindow(windowScene: windowScene)
self.window = window
appDelegate.window = window

factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: appDelegate.launchOptions)

// Deep links / user activities that arrived with the cold launch are no
// longer delivered via application(_:open:options:) under the scene lifecycle.
if !connectionOptions.urlContexts.isEmpty {
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
}
for activity in connectionOptions.userActivities {
self.scene(scene, continue: activity)
}
}

public func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let appDelegate = appDelegate else { return }
for context in URLContexts {
var options: [UIApplication.OpenURLOptionsKey: Any] = [
.openInPlace: context.options.openInPlace
]
if let source = context.options.sourceApplication {
options[.sourceApplication] = source
}
if let annotation = context.options.annotation {
options[.annotation] = annotation
}
_ = appDelegate.application(UIApplication.shared, open: context.url, options: options)
}
}

public func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let appDelegate = appDelegate else { return }
_ = appDelegate.application(UIApplication.shared, continue: userActivity) { _ in }
}

// MARK: - Lifecycle forwarding

public func sceneDidBecomeActive(_ scene: UIScene) {
appDelegate?.applicationDidBecomeActive(UIApplication.shared)
}

public func sceneWillResignActive(_ scene: UIScene) {
appDelegate?.applicationWillResignActive(UIApplication.shared)
}

public func sceneDidEnterBackground(_ scene: UIScene) {
appDelegate?.applicationDidEnterBackground(UIApplication.shared)
}

public func sceneWillEnterForeground(_ scene: UIScene) {
appDelegate?.applicationWillEnterForeground(UIApplication.shared)
}
}
Loading