diff --git a/QueueCube/Backend/API.swift b/QueueCube/Backend/API.swift index 5e42b21..a4bdaf4 100644 --- a/QueueCube/Backend/API.swift +++ b/QueueCube/Backend/API.swift @@ -233,18 +233,12 @@ actor API private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream.Continuation) { print("Websocket Error: \(error)") - - var shouldNotifyObservers = true + 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 { - continuation.yield(.error(.websocketError(error))) - } + + // Always notify observers of WebSocket errors so reconnection can happen + // The UI layer can decide whether to show the error to the user + continuation.yield(.error(.websocketError(error))) } private func request() -> RequestBuilder { diff --git a/QueueCube/Views/ContentView.swift b/QueueCube/Views/ContentView.swift index 218cace..49c510c 100644 --- a/QueueCube/Views/ContentView.swift +++ b/QueueCube/Views/ContentView.swift @@ -11,12 +11,16 @@ struct ContentView: View { @State var model = MainViewModel() @State private var websocketRestartTrigger = 0 - + @Environment(\.scenePhase) private var scenePhase + var body: some View { MainView(model: $model) .task(id: websocketRestartTrigger) { await watchWebsocket() } .task { await refresh([.nowPlaying, .playlist, .favorites]) } .task { await watchForSettingsChanges() } + .onChange(of: scenePhase) { oldPhase, newPhase in + handleScenePhaseChange(from: oldPhase, to: newPhase) + } .sheet(isPresented: $model.isNowPlayingSheetPresented) { NowPlayingView(model: model.nowPlayingViewModel) .presentationBackground(.regularMaterial) @@ -50,6 +54,22 @@ struct ContentView: View 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 { await model.withModificationsViaAPI { api in if what.contains(.nowPlaying) { @@ -93,7 +113,7 @@ extension ContentView private func watchWebsocket() async { guard let api = model.selectedServer?.api else { return } - + do { for await streamEvent in try await api.events() { switch streamEvent { @@ -101,13 +121,21 @@ extension ContentView await clearConnectionErrorIfNecessary() await handle(event: event) 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 try await Task.sleep(for: .seconds(1.0)) websocketRestartTrigger += 1 } - + break } }