在 Combine 中用 Publisher 观察单例定时器值的变化

Observe singleton timer value changes with Publisher in Combine

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

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

例子

class TimerManager: ObservableObject {
   @Published var secondsPassed: [String: Int]
   var timers: [String:AnyCancellable]

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

   func isTimerValid(itemId: String) -> Bool {
       return self.timers[itemId].isTimerValid
   }

   // other code...
}

因此,例如,如果在任何其他视图中我需要通过调用函数 isTimerValid 来了解特定计时器是否处于活动状态,我需要在该视图中包含此 @EnvironmentObject,并且它不会停止刷新它,因为计时器更改 secondsPassedPublished,导致延迟和无用的重绘。

所以我做的一件事是将活动计时器的 itemId 缓存在其他地方,在我每次启动或停止计时器时更新的 static struct 中。

看起来有点hacky,所以最近我一直在考虑将所有这些都移动到一个Singleton中,例如这样

class SingletonTimerManager {

   static let singletonTimerManager = SingletonTimerManager()

   var secondsPassed: [String: Int]
   var timers: [String:AnyCancellable]

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

   // other code...
}

只让一些View观察secondsPassed的变化。从好的方面来说,我也许可以在后台线程上移动计时器。

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

这些是我的Views(虽然是非常简单的摘录)

struct ContentView: View {

    // set outside the ContentView
    var selectedItemId: String

    // timerValue: set by a publisher?

    var body: some View {
        VStack {  
            ItemView(seconds: Binding.constant(timerValue))
        }
    }
}

struct ItemView: View {
    @Binding var seconds: Int

    var body: some View {
        Text("\(self.seconds)")
    }
}

我需要以某种方式观察 SingletonChronoManager.secondsPassed[selectedItemId],以便 ItemView 实时更新。

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

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

How to use a timer with SwiftUI has a "shoving a timer into the view itself", which may work for what you're trying to do, but slightly better is the video here: https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer

在 Paul 的示例中,他将计时器塞入视图本身 - 这不是我的选择,但对于简单的实时时钟视图来说,这还不错。您可以轻松地从外部对象(例如您的单例)传入计时器发布者。

我最终使用了以下解决方案,结合了@heckj 的建议和 this one from @Mykel.

我所做的是将 AnyCancellableTimerPublishers 分开,方法是将它们保存在 SingletonTimerManager.

的特定词典中

然后,每次声明 ItemView 时,我都会实例化一个自动连接的 @State TimerPublisher。每个 Timer 实例现在都在 .common RunLoop 中运行,具有 0.5 容差以更好地帮助性能,正如 Paul 在这里所建议的:Triggering events repeatedly using a timer

ItemView.onAppear() 调用期间,如果 SingletonTimerManager 中已经存在具有相同 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
            }
    }
}
}