Swift 观察联合收割机中Publisher的单例计时器值变化

Swift 观察联合收割机中Publisher的单例计时器值变化,swift,timer,swiftui,combine,Swift,Timer,Swiftui,Combine,我的应用程序的一个要求是能够启动多个计时器,用于报告目的 我试图用@Published变量存储@EnvironmentObject中传递的计时器和秒数,但每次对象刷新时,观察@EnvironmentObject的任何视图也会刷新 范例 class TimerManager:ObservableObject{ @已发布的var secondsPassed:[字符串:Int] 变量计时器:[字符串:anycancelable] func startTimer(itemId:String){ self

我的应用程序的一个要求是能够启动多个计时器,用于报告目的

我试图用
@Published
变量存储
@EnvironmentObject
中传递的计时器和秒数,但每次对象刷新时,观察
@EnvironmentObject
的任何视图也会刷新

范例

class TimerManager:ObservableObject{
@已发布的var secondsPassed:[字符串:Int]
变量计时器:[字符串:anycancelable]
func startTimer(itemId:String){
self.secondsPassed[itemId]=0
self.timers[itemId]=计时器
.publish(每隔:1,在:.main上,在:.default中)
.自动连接()
.sink(接收值:{uu}in
self.secondsPassed[itemId]!+=1
})
}
func isTimerValid(itemId:String)->Bool{
返回self.timers[itemId].isTimerValid
}
//其他代码。。。
}
因此,例如,如果在任何其他视图中,我需要通过调用函数
isTimerValid
来知道特定计时器是否处于活动状态,我需要在该视图中包含此
@EnvironmentObject
,并且它不会停止刷新它,因为计时器会更改
secondsPassed
,这是
发布的
,造成延迟和无用的重画

因此,我做的一件事是将活动计时器的
itemId
缓存到其他地方,在
静态
结构中,我每次启动或停止计时器时都会更新该结构

这看起来有点不太对劲,所以最近我一直在考虑把这一切都转移到一个单身家庭中,比如这样

class SingletonTimerManager{
静态let singletontimermager=singletontimermager()
var secondsPassed:[字符串:Int]
变量计时器:[字符串:anycancelable]
func startTimer(itemId:String){
self.secondsPassed[itemId]=0
self.timers[itemId]=计时器
.publish(每隔:1,在:.main上,在:.default中)
.自动连接()
.sink(接收值:{uu}in
self.secondsPassed[itemId]!+=1
})
}
//其他代码。。。
}
只允许一些视图观察对
secondsPassed
的更改。好的一面是,我可以移动背景线程上的计时器

我一直在努力如何正确地做到这一点

这些是我的
视图
(尽管是一个非常简单的摘录)


我需要以某种方式观察SingletonChronoManager.secondsPassed[selectedItemId]
以便
项目视图能够实时更新

通过将计时器发布器结果放入环境中,您将向树中定义该环境对象的所有视图传播更改通知,我确信这将导致不必要的重画和性能问题(如您所见)

更好的机制是强烈限制需要显示不断更新时间的视图(或子视图),并将对计时器发布器的引用直接传递给它们,而不是将其分层到环境中。将计时器本身放入单例中是一个选项,但不是关键,并且不会影响您看到的级联重画

有一个“将计时器推入视图本身”,这可能适用于您尝试执行的操作,但这里的视频稍微好一些:


在Paul的例子中,他将计时器塞进视图本身——这不是我的选择,但对于一个简单的实时时钟视图来说,这并不坏。您可以很容易地从外部对象传入计时器发布器,例如您的singleton

我最终使用了以下解决方案,将@heckj建议和

我所做的是将
anycancelable
TimerPublishers
分开,将它们保存在
singletontimermager
的特定词典中

然后,每次声明
ItemView
时,我都会实例化一个自动连接的
@State TimerPublisher
。现在,每个计时器实例都在
.common
RunLoop
中运行,公差为
0.5
,以更好地帮助实现Paul在此处建议的性能:

在调用
ItemView
.onAppear()
过程中,如果
singletontimermager
中已经存在具有相同
itemId
的发布服务器,我只需将该发布服务器分配给我的一个视图

然后我像在@Mykel解决方案中一样处理它,启动和停止ItemView的发布服务器和SingletonTimerManager发布服务器

secondsPassed
显示在存储在
@State var seconds
中的文本中,该文本通过附加到
ItemView
发布服务器的
onReceive()
进行更新

我知道我可能用这个解决方案创建了太多的发布者,我无法准确地指出将一个发布者变量复制到另一个发布者变量时会发生什么,但现在总体性能要好得多

示例代码:

SingletonTimerManager

class SingletonTimerManager {
   static let singletonTimerManager = SingletonTimerManager()

   var secondsPassed: [String: Int]
   var cancellables: [String:AnyCancellable]
   var publishers: [String: TimerPublisher]

   func startTimer(itemId: String) {
      self.secondsPassed[itemId] = 0
      self.publisher[itemId] = Timer
          .publish(every: 1, tolerance: 0.5, on: .main, in: .common)
      self.cancellables[itemId] = self.publisher[itemId]!.autoconnect().sink(receiveValue: {_ in self.secondsPassed[itemId] += 1})
   }

   func isTimerValid(_ itemId: String) -> Bool {
       if(self.cancellables[itemId] != nil && self.publishers[itemId] != nil) {
           return true
       }
       return false
   }
}
ContentView

struct ContentView: View {

    var itemIds: [String]

    var body: some View {
        VStack {  
            ForEach(self.itemIds, id: \.self) { itemId in
                ItemView(itemId: itemId)
            }
        }
    }
}

struct ItemView: View {
    var itemId: String
    @State var seconds: Int
    @State var timerPublisher = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack {
        Button("StartTimer") {
            // Call startTimer in SingletonTimerManager....
            self.timerPublisher = SingletonTimerManager.publishers[itemId]! 
            self.timerPublisher.connect()
        }
        Button("StopTimer") {
            self.timerPublisher.connect().cancel()
            // Call stopTimer in SingletonTimerManager....
        }
        Text("\(self.seconds)")
            .onAppear {
                // function that checks if the timer with this itemId is running
                if(SingletonTimerManager.isTimerValid(itemId)) {
                     self.timerPublisher = SingletonTimerManager.publishers[itemId]!
                     self.timerPublisher.connect()
                }
            }.onReceive($timerPublisher) { _ in
                self.seconds = SingletonTimerManager.secondsPassed[itemId] ?? 0
            }
    }
}
}
struct ContentView: View {

    var itemIds: [String]

    var body: some View {
        VStack {  
            ForEach(self.itemIds, id: \.self) { itemId in
                ItemView(itemId: itemId)
            }
        }
    }
}

struct ItemView: View {
    var itemId: String
    @State var seconds: Int
    @State var timerPublisher = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack {
        Button("StartTimer") {
            // Call startTimer in SingletonTimerManager....
            self.timerPublisher = SingletonTimerManager.publishers[itemId]! 
            self.timerPublisher.connect()
        }
        Button("StopTimer") {
            self.timerPublisher.connect().cancel()
            // Call stopTimer in SingletonTimerManager....
        }
        Text("\(self.seconds)")
            .onAppear {
                // function that checks if the timer with this itemId is running
                if(SingletonTimerManager.isTimerValid(itemId)) {
                     self.timerPublisher = SingletonTimerManager.publishers[itemId]!
                     self.timerPublisher.connect()
                }
            }.onReceive($timerPublisher) { _ in
                self.seconds = SingletonTimerManager.secondsPassed[itemId] ?? 0
            }
    }
}
}