Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift UI overwhelmed by high-frequency @StateObject updates?

Scenario

A simple SwiftUI App that consists of a TabView with two tabs. The App struct has a @StateObject property, which is being repeatedly and very quickly (30 times per second) updated by simulateFastStateUpdate.

In this example, simulateFastStateUpdate is not doing any useful work, but it closely resembles a real function that quickly updates the app's state. The function does some work on a background queue for a short interval of time and then schedules a state update on the main queue. For example, when using the camera API, the app might update the preview image as frequently as 30 times per second.

Question

When the app is running, the TabView does not respond to taps. It's permanently stuck on the first tab. Removing liveController.message = "Nice" line fixes the issue.

  1. Why is TabView stuck?
  2. Why is updating @StateObject causing this issue?
  3. How to adapt this simple example, so that the TabView is not stuck?
import SwiftUI

class LiveController: ObservableObject {
    @Published var message = "Hello"
}

@main
struct LiveApp: App {
    @StateObject var liveController = LiveController()

    var body: some Scene {
        WindowGroup {
            TabView() {
                Text(liveController.message)
                    .tabItem {
                        Image(systemName: "1.circle")
                    }
                Text("Tab 2")
                    .tabItem {
                        Image(systemName: "2.circle")
                    }
            }
            .onAppear {
                DispatchQueue.global(qos: .userInitiated).async {
                    simulateFastStateUpdate()
                }
            }
        }
    }
    
    func simulateFastStateUpdate() {
        DispatchQueue.main.async {
            liveController.message = "Nice"
        }
        
        // waits 33 ms ~ 30 updates per second
        usleep(33 * 1000)
        
        DispatchQueue.global(qos: .userInitiated).async {
            simulateFastStateUpdate()
        }
    }
}
like image 745
hellodanylo Avatar asked Sep 12 '25 05:09

hellodanylo


1 Answers

You are blocking the main thread with these constant updates and the app is busy processing your UI updates and can't handle touch inputs (also received on the main thread).

Whatever creates this rapid event stream needs to be throttled. You can use Combine's throttle or debounce functionality to reduce the frequency of your UI updates.

Look at this sample, I added the class UpdateEmittingComponent producing updates with a Timer. This could be your background component updating rapidly.

In your LiveController I'm observing the result with Combine. There I added a throttle into the pipeline which will cause the message publisher to fiere once per second by dropping all in-between values.

Removing the throttle will end up in an unresponsive TabView.

import SwiftUI
import Combine

/// class simulating a component emitting constant events
class UpdateEmittingComponent: ObservableObject {
    @Published var result: String = ""
    
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = Timer
            .publish(every: 0.00001, on: .main, in: .default)
            .autoconnect()
            .sink {
                [weak self] _ in
                self?.result = "\(Date().timeIntervalSince1970)"
            }
    }
}

class LiveController: ObservableObject {
    @Published var message = "Hello"
    
    @ObservedObject var updateEmitter = UpdateEmittingComponent()
    private var cancellable: AnyCancellable?
    
    init() {
        updateEmitter
            .$result
            .throttle(for: .seconds(1),
                scheduler: RunLoop.main,
                latest: true
            )
            .assign(to: &$message)
    }
}

@main
struct LiveApp: App {
    @StateObject var liveController = LiveController()

    var body: some Scene {
        WindowGroup {
            TabView() {
                Text(liveController.message)
                    .tabItem {
                        Image(systemName: "1.circle")
                    }
                Text("Tab 2")
                    .tabItem {
                        Image(systemName: "2.circle")
                    }
            }
        }
    }
}
like image 59
mgratzer Avatar answered Sep 14 '25 18:09

mgratzer