Наблюдайте за изменениями значения таймера одиночного разряда с помощью Publisher в Combine

Одно из требований моего приложения - возможность запускать несколько таймеров для отчетов.

Я попытался сохранить таймеры и секунды, переданные в @EnvironmentObject с @Published переменными, но каждый раз, когда объект обновляется, обновляется и любое представление, которое наблюдает @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 в это представление, и он не перестанет обновлять его, потому что таймер изменяет secondsPassed, который Published, вызывая лаги и бесполезные перерисовки.

Поэтому я кэшировал itemId активных таймеров в другом месте, в static struct, который я обновляю каждый раз, когда запускаю или останавливаю таймер.

Это показалось немного взломанным, поэтому в последнее время я думал перенести все это в синглтон, например, вот так

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

и разрешить только некоторым просмотрам наблюдать за изменениями в 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 обновлялись в режиме реального времени.


person riciloma    schedule 22.04.2020    source источник


Ответы (2)


Помещая результаты издателя таймера в Environment, вы распространяете уведомления об изменениях на все представления в дереве, которые определяют этот объект среды, что, я уверен, вызовет ненужные перерисовки и проблемы с производительностью (как вы уже видели).

Более совершенный механизм сильно ограничивает представления (или подпредставления), которые должны отображать постоянно обновляющееся время, и передавать ссылку на издателя таймера непосредственно им, а не накладывать ее в среду. Помещение самого таймера в синглтон - это один из вариантов, но не критичный для этого, и он не повлияет на каскадные перерисовки, которые вы видите.

Как использовать таймер с SwiftUI имеет функцию «вставлять таймер в само представление», которая может работать для того, что вы пытаетесь сделать, но немного лучше видео здесь: https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-второстепенноеиспользование-таймер

В примере Пола он вставляет таймер в само представление - это не было бы моим выбором, но для простого представления часов в реальном времени это неплохо. Вы можете так же легко передать издателя таймера из внешнего объекта - например, вашего синглтона.

person heckj    schedule 22.04.2020

В итоге я использовал следующее решение, объединяющее предложение @heckj и это от @Mykel.

Я отделил AnyCancellable от TimerPublishers, сохранив их в определенных словарях SingletonTimerManager.

Затем, каждый раз, когда объявляется ItemView, я создаю автоматически подключаемый @State TimerPublisher. Каждый экземпляр Timer теперь работает в .common RunLoop с допуском 0.5 для лучшего улучшения производительности, как предлагает здесь Пол: Многократный запуск событий с использованием таймера

Во время .onAppear() вызова ItemView, если издатель с таким же itemId уже существует в SingletonTimerManager, я просто назначаю этого издателя одному из моих представлений.

Затем я обрабатываю его, как в решении @Mykel, с запуском и остановкой как издателя ItemView, так и издателя SingletonTimerManager.

secondsPassed показаны в тексте, хранящемся внутри @State var seconds, который обновляется с помощью onReceive(), прикрепленного к издателю ItemView.

Я знаю, что, вероятно, создаю слишком много издателей с помощью этого решения, и я не могу точно определить, что происходит при копировании переменной издателя в другую, но общая производительность сейчас намного лучше.

Пример кода:

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
            }
    }
}
}
person riciloma    schedule 25.04.2020