问题
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()
}
}
回答1:
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()
}
}
}
回答2:
Have you tried solving that problem by using combine? If your ui is updating too fast you can for example collect some data in combine in a kind of buffer and then after the buffer is full you can push it to your view. Another approach would be to debounce the input for a specific delay and then push only the last update to your view. For example lets say you have your published variable i that constantly updates it value. You could introduce the following viewmodel:
class Loop: ObservableObject {
@Published var i: Int = 0
@Published var updatedVariable: Int = 0
private var bag = Set<AnyCancellable>()
init() {
$i
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.sink { [weak self] value in
self?.updatedVariable = value
}
.store(in: &bag)
startLoop()
}
func startLoop() {
while true {
print("i = \(self.i)")
DispatchQueue.main.async {
self.i += 1
}
}
}
}
This way you update your ui only every 0.5 seconds if you use the updatedVariable instead the i variable.
来源:https://stackoverflow.com/questions/63678438/swiftui-updating-ui-with-high-frequency-data