Search code examples
iosswiftswiftuitimercombine

How can I pass Binding<Timer> in SwiftUI?


I would like to pass a timer from ContentView to SecondView, but I don't know how to manage it because I never used it before.

Can someone figure this out for me?

ContentView

struct ContentView: View {
    @State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
    @State private var timeRemaining = 10
    
    
    var body: some View {
        NavigationView {
            VStack {
                Text("\(timeRemaining)")
                    .onReceive(timer) { _ in
                        if timeRemaining > 0 {
                            timeRemaining -= 1
                        }
                    }
                
                NavigationLink {
                    SecondView(timer: ???) // <-- What should i pass here?
                } label: {
                    Text("Change View")
                }
            }
        }
    }
}

SecondView

struct SecondView: View {
    @Binding var timer: ??? // <-- What type?
    @State private var timeRemaining = 5
    
    var body: some View {
        Text("Hello")
            .onReceive(timer) { _ in
                if timeRemaining > 0 {
                    timeRemaining -= 1
                }
            }
    }
}

struct SecondView_Previews: PreviewProvider {
    static var previews: some View {
        SecondView(timer: ???) // <-- Same thing here in SecondView preview
    }
}

Solution

  • With this timer declaration you are in the Combine world. Combine is the reactive framework from Apple.

    First you would need to import it:

    import Combine
    

    I have commented the code but Combine is a far field and it probably would be best to read the documentation about it, read some tutorials and try some things out.

    documentation

    struct ContentView: View {
        // The typ here is Publishers.Autoconnect<Timer.TimerPublisher>
        // But we can erase it and the result will be a Publisher that emits a date and never throws an error: AnyPublisher<Date,Never>
        @State private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
            .autoconnect()
            .eraseToAnyPublisher()
        @State private var timeRemaining = 10
        
        
        var body: some View {
            NavigationView {
                VStack {
                    Text("\(timeRemaining)")
                        .onReceive(timer) { _ in
                            if timeRemaining > 0 {
                                timeRemaining -= 1
                            }
                        }
                    
                    NavigationLink {
                        // pass the publisher on
                        SecondView(timer: timer)
                    } label: {
                        Text("Change View")
                    }
                }
            }
        }
    }
    
    struct SecondView: View {
        //You don´t need binding here as this view never manipulates this publisher
        var timer: AnyPublisher<Date,Never>
        @State private var timeRemaining = 5
        
        var body: some View {
            Text("Hello")
                .onReceive(timer) { _ in
                    if timeRemaining > 0 {
                        timeRemaining -= 1
                        print(timeRemaining)
                    }
                }
        }
    }
    
    struct SecondView_Previews: PreviewProvider {
        // Creating a static private var should work here !not tested!
        @State static private var timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
            .autoconnect()
            .eraseToAnyPublisher()
        static var previews: some View {
            SecondView(timer: timer)
        }
    }