Skip to content

Commit 9fef98f

Browse files
vanpeltClaude
andauthored
Fix SSE reconnection when app returns to foreground (#217)
## Problem When users navigated away from the app during an SSE connection to a codespace, iOS would suspend the URLSession connection. Upon returning to the app, the connection would remain stuck in a "connecting" state indefinitely, never completing or showing an error. ## Solution Added app lifecycle monitoring using SwiftUI's `scenePhase` environment value. When the app is backgrounded during an active SSE connection, the view now tracks this state. Upon returning to the foreground, it automatically disconnects the stale connection and initiates a fresh SSE connection to the same codespace. ## Implementation - Added `@Environment(\.scenePhase)` to monitor app state transitions - Tracks connection state with `wasConnectingBeforeBackground` flag - Stores `pendingCodespaceName` to preserve the target codespace across app backgrounding - Implements `handleScenePhaseChange()` to detect background→active transitions and trigger reconnection - Uses SwiftUI-native approach instead of NotificationCenter for better integration with view lifecycle --------- Co-authored-by: Claude <claude@anthropic.com>
1 parent 3304361 commit 9fef98f

File tree

5 files changed

+70
-23
lines changed

5 files changed

+70
-23
lines changed

xcode/Catnip.xcodeproj/project.pbxproj

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,10 @@
423423
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
424424
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
425425
CODE_SIGN_ENTITLEMENTS = CatnipWidgetsExtension.entitlements;
426+
CODE_SIGN_IDENTITY = "Apple Development";
426427
CODE_SIGN_STYLE = Automatic;
427428
CURRENT_PROJECT_VERSION = 1;
428-
DEVELOPMENT_TEAM = QCUUU7QVF7;
429+
DEVELOPMENT_TEAM = 5DTHBP38WM;
429430
GENERATE_INFOPLIST_FILE = YES;
430431
INFOPLIST_FILE = CatnipWidgets/Info.plist;
431432
INFOPLIST_KEY_CFBundleDisplayName = CatnipWidgets;
@@ -437,8 +438,9 @@
437438
"@executable_path/../../Frameworks",
438439
);
439440
MARKETING_VERSION = 1.0;
440-
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnip.CatnipWidgets;
441+
PRODUCT_BUNDLE_IDENTIFIER = com.wandb.catnip.CatnipWidgets;
441442
PRODUCT_NAME = "$(TARGET_NAME)";
443+
PROVISIONING_PROFILE_SPECIFIER = "";
442444
SKIP_INSTALL = YES;
443445
STRING_CATALOG_GENERATE_SYMBOLS = YES;
444446
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = WIDGET_EXTENSION;
@@ -456,9 +458,10 @@
456458
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
457459
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
458460
CODE_SIGN_ENTITLEMENTS = CatnipWidgetsExtension.entitlements;
461+
CODE_SIGN_IDENTITY = "Apple Development";
459462
CODE_SIGN_STYLE = Automatic;
460463
CURRENT_PROJECT_VERSION = 1;
461-
DEVELOPMENT_TEAM = QCUUU7QVF7;
464+
DEVELOPMENT_TEAM = 5DTHBP38WM;
462465
GENERATE_INFOPLIST_FILE = YES;
463466
INFOPLIST_FILE = CatnipWidgets/Info.plist;
464467
INFOPLIST_KEY_CFBundleDisplayName = CatnipWidgets;
@@ -470,8 +473,9 @@
470473
"@executable_path/../../Frameworks",
471474
);
472475
MARKETING_VERSION = 1.0;
473-
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnip.CatnipWidgets;
476+
PRODUCT_BUNDLE_IDENTIFIER = com.wandb.catnip.CatnipWidgets;
474477
PRODUCT_NAME = "$(TARGET_NAME)";
478+
PROVISIONING_PROFILE_SPECIFIER = "";
475479
SKIP_INSTALL = YES;
476480
STRING_CATALOG_GENERATE_SYMBOLS = YES;
477481
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = WIDGET_EXTENSION;
@@ -490,12 +494,15 @@
490494
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
491495
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
492496
CODE_SIGN_ENTITLEMENTS = catnip/catnip.entitlements;
497+
CODE_SIGN_IDENTITY = "Apple Development";
493498
CODE_SIGN_STYLE = Automatic;
494499
CURRENT_PROJECT_VERSION = 2;
495-
DEVELOPMENT_TEAM = QCUUU7QVF7;
500+
DEVELOPMENT_TEAM = 5DTHBP38WM;
496501
ENABLE_PREVIEWS = YES;
497502
GENERATE_INFOPLIST_FILE = YES;
498503
INFOPLIST_FILE = catnip/Info.plist;
504+
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
505+
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
499506
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
500507
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
501508
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -506,8 +513,9 @@
506513
"@executable_path/Frameworks",
507514
);
508515
MARKETING_VERSION = 1.0;
509-
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnip;
516+
PRODUCT_BUNDLE_IDENTIFIER = com.wandb.catnip;
510517
PRODUCT_NAME = "$(TARGET_NAME)";
518+
PROVISIONING_PROFILE_SPECIFIER = "";
511519
STRING_CATALOG_GENERATE_SYMBOLS = YES;
512520
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
513521
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -524,12 +532,15 @@
524532
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
525533
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
526534
CODE_SIGN_ENTITLEMENTS = catnip/catnip.entitlements;
535+
CODE_SIGN_IDENTITY = "Apple Development";
527536
CODE_SIGN_STYLE = Automatic;
528537
CURRENT_PROJECT_VERSION = 2;
529-
DEVELOPMENT_TEAM = QCUUU7QVF7;
538+
DEVELOPMENT_TEAM = 5DTHBP38WM;
530539
ENABLE_PREVIEWS = YES;
531540
GENERATE_INFOPLIST_FILE = YES;
532541
INFOPLIST_FILE = catnip/Info.plist;
542+
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
543+
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
533544
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
534545
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
535546
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -540,8 +551,9 @@
540551
"@executable_path/Frameworks",
541552
);
542553
MARKETING_VERSION = 1.0;
543-
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnip;
554+
PRODUCT_BUNDLE_IDENTIFIER = com.wandb.catnip;
544555
PRODUCT_NAME = "$(TARGET_NAME)";
556+
PROVISIONING_PROFILE_SPECIFIER = "";
545557
STRING_CATALOG_GENERATE_SYMBOLS = YES;
546558
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
547559
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -678,7 +690,7 @@
678690
BUNDLE_LOADER = "$(TEST_HOST)";
679691
CODE_SIGN_STYLE = Automatic;
680692
CURRENT_PROJECT_VERSION = 1;
681-
DEVELOPMENT_TEAM = QCUUU7QVF7;
693+
DEVELOPMENT_TEAM = 5DTHBP38WM;
682694
GENERATE_INFOPLIST_FILE = YES;
683695
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
684696
MARKETING_VERSION = 1.0;
@@ -699,7 +711,7 @@
699711
BUNDLE_LOADER = "$(TEST_HOST)";
700712
CODE_SIGN_STYLE = Automatic;
701713
CURRENT_PROJECT_VERSION = 1;
702-
DEVELOPMENT_TEAM = QCUUU7QVF7;
714+
DEVELOPMENT_TEAM = 5DTHBP38WM;
703715
GENERATE_INFOPLIST_FILE = YES;
704716
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
705717
MARKETING_VERSION = 1.0;
@@ -719,7 +731,7 @@
719731
buildSettings = {
720732
CODE_SIGN_STYLE = Automatic;
721733
CURRENT_PROJECT_VERSION = 1;
722-
DEVELOPMENT_TEAM = QCUUU7QVF7;
734+
DEVELOPMENT_TEAM = 5DTHBP38WM;
723735
GENERATE_INFOPLIST_FILE = YES;
724736
MARKETING_VERSION = 1.0;
725737
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnipUITests;
@@ -738,7 +750,7 @@
738750
buildSettings = {
739751
CODE_SIGN_STYLE = Automatic;
740752
CURRENT_PROJECT_VERSION = 1;
741-
DEVELOPMENT_TEAM = QCUUU7QVF7;
753+
DEVELOPMENT_TEAM = 5DTHBP38WM;
742754
GENERATE_INFOPLIST_FILE = YES;
743755
MARKETING_VERSION = 1.0;
744756
PRODUCT_BUNDLE_IDENTIFIER = wandb.catnipUITests;

xcode/CatnipWidgetsExtension.entitlements

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<dict>
55
<key>com.apple.security.application-groups</key>
66
<array>
7-
<string>group.com.wandb.catnip</string>
7+
<string>group.com.wandb.catnip.widgets</string>
88
</array>
99
</dict>
1010
</plist>

xcode/catnip/Info.plist

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,6 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>ITSAppUsesNonExemptEncryption</key>
6-
<false/>
7-
<key>NSSupportsLiveActivities</key>
8-
<true/>
9-
<key>UIBackgroundModes</key>
10-
<array>
11-
<string>remote-notification</string>
12-
<string>fetch</string>
13-
</array>
145
<key>CFBundleURLTypes</key>
156
<array>
167
<dict>
@@ -24,5 +15,14 @@
2415
</array>
2516
<key>NSUserNotificationsUsageDescription</key>
2617
<string>Catnip sends notifications when your codespace is ready. Creating a codespace can take up to 10 minutes.</string>
18+
<key>ITSAppUsesNonExemptEncryption</key>
19+
<false/>
20+
<key>NSSupportsLiveActivities</key>
21+
<true/>
22+
<key>UIBackgroundModes</key>
23+
<array>
24+
<string>remote-notification</string>
25+
<string>fetch</string>
26+
</array>
2727
</dict>
2828
</plist>

xcode/catnip/Views/CodespaceView.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ enum RepositoryListMode {
2626
}
2727

2828
struct CodespaceView: View {
29+
@Environment(\.scenePhase) private var scenePhase
2930
@EnvironmentObject var authManager: AuthManager
3031
@StateObject private var installer = CatnipInstaller.shared
3132
@StateObject private var tracker = CodespaceCreationTracker.shared
@@ -40,6 +41,8 @@ struct CodespaceView: View {
4041
@State private var createdCodespace: CodespaceCreationResult.CodespaceInfo?
4142
@State private var repositoryListMode: RepositoryListMode = .installation
4243
@State private var pendingRepository: String?
44+
@State private var pendingCodespaceName: String?
45+
@State private var wasConnectingBeforeBackground = false
4346

4447
private let catFacts = [
4548
"Cats can rotate their ears 180 degrees.",
@@ -227,6 +230,10 @@ struct CodespaceView: View {
227230
NSLog("🐱 [CodespaceView] Failed to preload repositories: \(error)")
228231
}
229232
}
233+
234+
}
235+
.onChange(of: scenePhase) { oldPhase, newPhase in
236+
handleScenePhaseChange(oldPhase: oldPhase, newPhase: newPhase)
230237
}
231238
}
232239

@@ -510,6 +517,9 @@ struct CodespaceView: View {
510517
statusMessage = ""
511518
statusMessage = "Finding your codespace..."
512519

520+
// Store codespace name for potential reconnection after backgrounding
521+
pendingCodespaceName = codespaceName
522+
513523
// Mock connection for UI tests
514524
if UITestingHelper.isUITesting {
515525
UserDefaults.standard.set("mock-codespace", forKey: "codespace_name")
@@ -1115,6 +1125,31 @@ struct CodespaceView: View {
11151125
.background(Color(uiColor: .systemGroupedBackground))
11161126
}
11171127

1128+
// MARK: - App Lifecycle Handling
1129+
1130+
private func handleScenePhaseChange(oldPhase: ScenePhase, newPhase: ScenePhase) {
1131+
// Track when app goes to background during SSE connection
1132+
if newPhase == .background && phase == .connecting {
1133+
wasConnectingBeforeBackground = true
1134+
NSLog("🐱 [CodespaceView] App backgrounded during SSE connection, will reconnect on foreground")
1135+
}
1136+
1137+
// Reconnect when app returns to foreground if we were connecting
1138+
if newPhase == .active && oldPhase == .background && wasConnectingBeforeBackground && phase == .connecting {
1139+
NSLog("🐱 [CodespaceView] App foregrounded, reconnecting SSE...")
1140+
1141+
// Disconnect the old stale connection
1142+
sseService?.disconnect()
1143+
sseService = nil
1144+
1145+
// Restart the connection with the same codespace name
1146+
handleConnect(codespaceName: pendingCodespaceName)
1147+
1148+
// Reset the flag
1149+
wasConnectingBeforeBackground = false
1150+
}
1151+
}
1152+
11181153
private var createRepositoryView: some View {
11191154
ScrollView {
11201155
VStack(spacing: 24) {

xcode/catnip/catnip.entitlements

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<string>development</string>
77
<key>com.apple.security.application-groups</key>
88
<array>
9-
<string>group.com.wandb.catnip</string>
9+
<string>group.com.wandb.catnip.widgets</string>
1010
</array>
1111
</dict>
1212
</plist>

0 commit comments

Comments
 (0)