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:
@@ -233,18 +233,12 @@ actor API
|
||||
|
||||
private static func notifyError(_ error: any Swift.Error, continuation: AsyncStream<StreamEvent>.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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user