Search code examples
mvvmswiftuiobservablegrand-central-dispatchcombine

SwiftUI updating UI with high frequency data


I'm trying to update the main view with high frequency data coming from separate background thread. I've created two tabviews and in case of slow update rate I can change the view. But in another case the UI doesn't react. I've observed this behavior only on real device, in the simulator works everything fine.

The while loop is still representing an imu, just to keep it simple.

Did someone any idea how to fix this issue?

Many thanks!

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject var loop : Loop
    
    var body: some View {
        
        TabView{
        
            VStack {
                Text("Content View")
                LoopView()
            }.tabItem{
                  VStack{
                      Text("tab1")
                      Image(systemName: "car")
                }
                
            }
            
            Text("second view").tabItem{
                                    VStack{
                                        Text("tab2")
                                        Image(systemName: "star")
                }
            }
        }
    }
}


class Loop : ObservableObject {
    
    @Published var i : Int
    
    func startLoop() {
        while true {
            print("i = \(self.i)")
            DispatchQueue.main.async {
                self.i += 1
            }

            //sleep(1) // comment out to simulate worst case
        }
    }
    
    init() {
        DispatchQueue.global(qos: .background).async {
            self.startLoop()
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • You need to separate updating storage of frequency data from represented UI part, so storage receives/contains actual real data, but UI part update as soon as you only want (0.5 sec, 1 sec, 5 secs, etc.)

    Here is possible approach. Tested with Xcode 12 / iOS 14.

    import Combine
    class Loop : ObservableObject {
        private var storage: Int = 0
        private var counter = PassthroughSubject<Int, Never>()
    
        @Published var i : Int = 0   // only for UI
    
        func startLoop() {
            while true {
                storage += 1     // update storage 
                counter.send(storage) // publish event
            }
        }
    
        private var subscriber: AnyCancellable?
        init() {
            subscriber = counter
                .throttle(for: 0.5, scheduler: DispatchQueue.global(qos: .background), latest: true) // drop in background
                .receive(on: DispatchQueue.main)  // only latest result
                .sink { [weak self] (value) in    // on @pawello2222 comment
                   self?.i = value
                }
    
            DispatchQueue.global(qos: .background).async {
                self.startLoop()
            }
        }
    }