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

Я все еще относительно новичок в Combine и SwiftUI, может быть, я слишком усложняю вещи, но, пожалуйста, держите меня в покое минуту или две.

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

Я пытался сохранить таймеры и секунды, переданные в @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 обновлялся в реальном времени.

Может кто-нибудь предложить мне способ сделать это?

Ответы [ 2 ]

1 голос
/ 22 апреля 2020

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

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

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

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

0 голосов
/ 25 апреля 2020

Я закончил тем, что использовал следующее решение, сочетающее предложение @heckj и это от @ Mykel.

Я отделил AnyCancellable от TimerPublishers сохраняя их в указанных c словарях 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
            }
    }
}
}
...