Fix WebSocket reconnection after app backgrounding

- Add scenePhase monitoring to ContentView to detect app lifecycle changes
- Implement handleScenePhaseChange() to force WebSocket reconnection and full UI refresh when app returns to foreground from background
- Update error handling in watchWebsocket() to suppress UI errors for backgrounding (error code 53) while still triggering reconnection
- Simplify API.notifyError() to always report errors, letting UI layer decide what to display

This fixes the issue where WebSocket connections would permanently disconnect after extended backgrounding, as iOS terminates background network connections after ~30 seconds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-09 11:42:13 -07:00
parent 3a5c285511
commit 6110f712bd
2 changed files with 38 additions and 16 deletions

View File

@@ -234,17 +234,11 @@ actor API
private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream<StreamEvent>.Continuation) { private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream<StreamEvent>.Continuation) {
print("Websocket Error: \(error)") print("Websocket Error: \(error)")
var shouldNotifyObservers = true
let nsError = error as NSError let nsError = error as NSError
if nsError.code == 53 {
// This is a "connection abort", caused by backgrounding.
// Don't notify UI, just silently reconnect.
shouldNotifyObservers = false
}
if shouldNotifyObservers { // Always notify observers of WebSocket errors so reconnection can happen
continuation.yield(.error(.websocketError(error))) // The UI layer can decide whether to show the error to the user
} continuation.yield(.error(.websocketError(error)))
} }
private func request() -> RequestBuilder { private func request() -> RequestBuilder {

View File

@@ -11,12 +11,16 @@ struct ContentView: View
{ {
@State var model = MainViewModel() @State var model = MainViewModel()
@State private var websocketRestartTrigger = 0 @State private var websocketRestartTrigger = 0
@Environment(\.scenePhase) private var scenePhase
var body: some View { var body: some View {
MainView(model: $model) MainView(model: $model)
.task(id: websocketRestartTrigger) { await watchWebsocket() } .task(id: websocketRestartTrigger) { await watchWebsocket() }
.task { await refresh([.nowPlaying, .playlist, .favorites]) } .task { await refresh([.nowPlaying, .playlist, .favorites]) }
.task { await watchForSettingsChanges() } .task { await watchForSettingsChanges() }
.onChange(of: scenePhase) { oldPhase, newPhase in
handleScenePhaseChange(from: oldPhase, to: newPhase)
}
.sheet(isPresented: $model.isNowPlayingSheetPresented) { .sheet(isPresented: $model.isNowPlayingSheetPresented) {
NowPlayingView(model: model.nowPlayingViewModel) NowPlayingView(model: model.nowPlayingViewModel)
.presentationBackground(.regularMaterial) .presentationBackground(.regularMaterial)
@@ -50,6 +54,22 @@ struct ContentView: View
extension ContentView extension ContentView
{ {
private func handleScenePhaseChange(from oldPhase: ScenePhase, to newPhase: ScenePhase) {
// When app returns to active state from background, force reconnect and refresh
if oldPhase == .background && newPhase == .active {
Task {
// Force WebSocket reconnection
websocketRestartTrigger += 1
// Give the WebSocket a moment to reconnect
try? await Task.sleep(for: .milliseconds(100))
// Full UI refresh
await refresh([.nowPlaying, .playlist, .favorites])
}
}
}
private func refresh(_ what: RefreshType) async { private func refresh(_ what: RefreshType) async {
await model.withModificationsViaAPI { api in await model.withModificationsViaAPI { api in
if what.contains(.nowPlaying) { if what.contains(.nowPlaying) {
@@ -101,8 +121,16 @@ extension ContentView
await clearConnectionErrorIfNecessary() await clearConnectionErrorIfNecessary()
await handle(event: event) await handle(event: event)
case .error(let error): case .error(let error):
model.connectionError = error // Check if this is a backgrounding error (connection abort)
let nsError = error as NSError
let isBackgroundingError = nsError.code == 53
// Only show connection error to user if it's not a backgrounding error
if !isBackgroundingError {
model.connectionError = error
}
// Always attempt reconnection after a delay
Task { @MainActor in Task { @MainActor in
try await Task.sleep(for: .seconds(1.0)) try await Task.sleep(for: .seconds(1.0))
websocketRestartTrigger += 1 websocketRestartTrigger += 1