Lets say you have a SwiftUI app where you are implementing navigation like this:
import SwiftUI
enum ScreenToShow {
case main, child
}
@Observable
class NavigationController {
var screen: ScreenToShow = .main
}
struct ContentView: View {
@Environment(NavigationController.self) var navController
var body: some View {
switch navController.screen {
case .main:
MainScreen()
case .child:
ChildScreen2(totalDurationSeconds: 5.0)
}
}
}
So whenever navController.screen is set to something new in the app, the "main router" reacts and re-renders.
But what if we have a child screen that is/contains a TimeLineView we use to display a countdown, and when the countdown finishes, the navigation should go back to the main screen automatically.
Attempt:
import SwiftUI
struct ChildScreen2: View {
@Environment(NavigationController.self) var navController
@State private var startTime: Date = Date()
let totalDurationSeconds:Double
var body: some View {
TimelineView(.periodic(from: Date(), by: 1)) {
context in
let elapsedSeconds = Date().timeIntervalSince(startTime)
let remainingSeconds = Int(totalDurationSeconds) - Int(elapsedSeconds)
if remainingSeconds == 0 {
// uncommenting the following line results in "Type '()' cannot conform to 'View'"
// (we are not allowed to have "normal" code in a View) :(
// navController.screen = .main
}
Text(context.date.description)
ZStack{
Rectangle()
.fill(Color.cyan)
Text("\(remainingSeconds)")
}
}
}
}
The whole reason I am doing this is that I had an app that does this successfully, but was getting overwhelmingly complicated (and having other view transition and lifecycle complications) due to using Timers and imperative techniques.
Then I discovered TimelineView and was very optimistic... but now I'm stuck again.
The basic question is: How do you execute code (like as simple as setting variable or calling a function) based on the elapsed time inside a TimelineView?
I've thought of a workaround hack like creating a custom view and conditionally showing it only to execute the code I want in its initializer... but that's awful!
Am I missing some obvious non-hacky, elegant, declarative, and scalable solution?
In the child screen's
ZStack
, you can add a view that only appears whenremainingSeconds == 0
. Then you can useonAppear
(ortask
if you want to do something async) to run some code.Here I've used a zero-sized
Color.clear
as an example.