Правильный способ использования таймера для запуска изменения NavigationView в SwiftUI

У меня есть приложение, в котором я хотел бы, чтобы пользователь мог запускать задачу по времени, и когда время истечет, иерархия навигации должна появиться и вернуть пользователя. У меня есть код, который ~ работает, но мне не нравится запах кода. Это правильный подход к чему-то вроде этого?

class SimpleTimerManager: ObservableObject {
  @Published var elapsedSeconds: Double = 0.0
  private(set) var timer = Timer()
  
  func start() {
    timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
      self.elapsedSeconds += 0.01
    }
  }
  
  func stop() {
    timer.invalidate()
    elapsedSeconds = 0.0
  }
}


struct ContentView: View {
  @ObservedObject var timerManager = SimpleTimerManager()
  
  var body: some View {
    NavigationView {
      NavigationLink(destination: CountDownIntervalView(
        timerManager: timerManager, length: 5.0
      )) {
        Text("Start the timer!")
      }
    }
    .navigationViewStyle(StackNavigationViewStyle())
  }
}


struct CountDownIntervalView: View {
  @ObservedObject var timerManager: SimpleTimerManager
  var length: Double
  @Environment(\.presentationMode) var mode: Binding<PresentationMode>
  var interval: Double {
    let interval = length - self.timerManager.elapsedSeconds
    if interval <= 0 {
      self.mode.wrappedValue.dismiss()
      self.timerManager.stop()
    }
    return interval
  }
    
  var body: some View {
    VStack {
      Text("Time remaining: \(String(format: "%.2f", interval))")
      Button(action: {
        self.mode.wrappedValue.dismiss()
        self.timerManager.stop()
      }) {
        Text("Quit early!")
      }
    }
    .navigationBarBackButtonHidden(true)
    .onAppear(perform: {
      self.timerManager.start()
    })
  }
}

У меня есть настраиваемая кнопка возврата, которую я бы хотел сохранить. Меня больше всего беспокоит то, что мне кажется неправильным иметь код, который останавливает таймер и выводит навигацию внутри вычисляемого свойства. Я бы предпочел что-то вроде

Text("\(interval)").onReceive(timerManager.timer, perform: { _ in
  if self.interval <= 0 {
    self.mode.wrappedValue.dismiss()
    self.timerManager.stop()
  }
})

внутри CountDownIntervalView, но это приводит к ошибке компилятора - Unable to infer complex closure return type; add explicit type to disambiguate - и, честно говоря, я не уверен, что этот подход также имеет смысл (добавление кода, который условно выводит навигацию на часть пользовательского интерфейса) . Как лучше всего подойти к этой проблеме?

Спасибо за любые мысли.


person Gabriel Perdue    schedule 20.07.2020    source источник


Ответы (1)


Вот решение. Протестировано с Xcode 11.4 / iOS 13.4

struct CountDownIntervalView: View {
  @ObservedObject var timerManager: SimpleTimerManager
  var length: Double
  @Environment(\.presentationMode) var mode: Binding<PresentationMode>

  var interval: Double {
    length - self.timerManager.elapsedSeconds
  }

  var body: some View {
    VStack {
      Text("Time remaining: \(String(format: "%.2f", interval))")
        .onReceive(timerManager.$elapsedSeconds) { _ in
            if self.interval <= 0 {
              self.mode.wrappedValue.dismiss()
              self.timerManager.stop()
            }
        }

    // ... other your code
person Asperi    schedule 20.07.2020
comment
Очаровательно - я не понимал, что должен относиться к elapsedSeconds как к привязке! Я принял это как решение, потому что предполагаю, что вы поддерживаете идею прикрепления всплывающего кода на Text, а не где-нибудь еще в представлении. Это все еще кажется мне немного подозрительным, но я готов поверить, что это лучшие практики SwiftUI. - person Gabriel Perdue; 20.07.2020