Executing Code Based on Elapsed Time in SwiftUI's TimelineView

136 views Asked by At

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?

1

There are 1 answers

0
Sweeper On

In the child screen's ZStack, you can add a view that only appears when remainingSeconds == 0. Then you can use onAppear (or task if you want to do something async) to run some code.

Here I've used a zero-sized Color.clear as an example.

ZStack{
    
    if remainingSeconds == 0 {
        Color.clear
            .frame(width: 0, height: 0)
            .onAppear {
                navController.screen = .main
            }
    }
    Rectangle()
        .fill(Color.cyan)
    Text("\(remainingSeconds)")
}