SwiftUI使用高频数据更新UI

3
我正在尝试使用来自独立后台线程的高频数据更新主视图。我创建了两个选项卡视图,在更新速率较慢的情况下,我可以更改视图。但在另一种情况下,用户界面没有反应。我只观察到这种行为在真实设备上出现,模拟器上一切正常。while循环仍表示imu,只是为了保持简单。有人有解决此问题的想法吗?非常感谢!
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()
    }
}

你想经常更新UI的原因是什么?它将涉及哪种类型的数据? - pawello2222
我曾经遇到过高频UI更新的类似问题,解决关键在于使用子视图,这样不必每次都刷新完整视图。也许这对你也有帮助。https://dev59.com/VlIG5IYBdhLWcg3wozHA - Volker88
无延迟的无限循环只是一个例子。传感器(IMU)以60 Hz更新,UI可能更新较慢。 - Murokill
2个回答

7

您需要将更新频率数据的存储部分和UI部分分开处理,这样存储将接收/包含实际的真实数据,但只要您想要(0.5秒、1秒、5秒等),UI部分就会更新。

下面是可能的方法。已在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
建议在类中使用 sink(receiveValue:) 以避免保留循环。请参见 https://forums.swift.org/t/does-assign-to-produce-memory-leaks/29546 - pawello2222
我已经快速测试了你的代码,它在我的电脑上可以运行。现在我将尝试理解组合类的确切含义。 - Murokill

2

您尝试过使用Combine解决这个问题吗?如果您的UI更新过快,您可以在Combine中收集一些数据并将其缓存,然后在缓存满时将其推送到视图中。另一种方法是将输入延迟特定时间,并仅将最后一次更新推送到您的视图。例如,假设您有一个不断更新其值的已发布变量i。您可以引入以下视图模型:

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
            }
        }
    }
}

如果您使用 updatedVariable 而不是 i 变量,每 0.5 秒更新一次 UI。


1
建议在类中使用 sink(receiveValue:) 以避免保留循环。请参见 https://forums.swift.org/t/does-assign-to-produce-memory-leaks/29546 - pawello2222

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接