I asked this question on StackOverflow also, but my interactions there have lead me to think this might be a SwiftUI bug, so I thought I'd ask here too.
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:
Poking at this some more, I see two strange behaviors. If I don't use a pass through, everything works for manual clicking. But if I let the timer expire and create a pop back, when I try to restart it, the view immediately pops, but the timer keeps running. If I do use a pass through, the timer starts when I navigate two levels down, but the view doesn't update. I wonder if this is a bug in how SwiftUI is handling the onAppear and mode.dismiss methods.
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:
Code Block 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) } }
Poking at this some more, I see two strange behaviors. If I don't use a pass through, everything works for manual clicking. But if I let the timer expire and create a pop back, when I try to restart it, the view immediately pops, but the timer keeps running. If I do use a pass through, the timer starts when I navigate two levels down, but the view doesn't update. I wonder if this is a bug in how SwiftUI is handling the onAppear and mode.dismiss methods.