SwiftUI NavigationView with PresentationMode creates bug in multilevel navigation hierarchy

408 Views Asked by At

I have an iOS 13.5 SwiftUI (macOS 10.15.6) app that requires the user to navigate two levels deep in a NavigationView hierarchy to play a game. The game is timed. I'd like to use custom back buttons in both levels, but if I do, the timer in the second level breaks in a strange way. If I give up on custom back buttons in the first level and use the system back button everything works. Here is a minimum app that replicates the problem:

class SimpleTimerManager: ObservableObject {
  @Published var elapsedSeconds: Double = 0.0
  private(set) var timer = Timer()
  
  func start() {
    print("timer started")
    timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) {_ in
      if (Int(self.elapsedSeconds * 100) % 100 == 0) { print ("\(self.elapsedSeconds)") }
      self.elapsedSeconds += 0.01
    }
  }
  
  func stop() {
    timer.invalidate()
    elapsedSeconds = 0.0
    print("timer stopped")
  }
}

struct ContentView: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: CountDownIntervalPassThroughView()) {
        Text("Start the timer!")
      }
    }
    .navigationViewStyle(StackNavigationViewStyle())
  }
}

struct CountDownIntervalPassThroughView: View {
  @Environment(\.presentationMode) var mode: Binding<PresentationMode>

  var body: some View {
    VStack {
      NavigationLink(destination: CountDownIntervalView()) {
        Text("One more click...")
      }
      Button(action: {
        self.mode.wrappedValue.dismiss()
        print("Going back from CountDownIntervalPassThroughView")
      }) {
        Text("Go back!")
      }
    }
    .navigationBarBackButtonHidden(true)
  }
}

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

Actually, with this example code, there is some strange behavior even if I use the system back, although my full app doesn't have that problem and I can't see any differences that would explain why. But - for the moment I'd like to focus on why this code breaks with multi-level custom back buttons.

Thanks in advance for any thoughts on this...

0

There are 0 best solutions below