2

I'd like to use @AppStorage("interval") to initialize timer. I cannot do this in the struct initialization because I'll get an error. But if I try to do declare timer first and initialize it in startTimer(), I get all sorts of errors as well. What's the best way to make sure interval is available to initialize timer? Here's the relevant code:

struct ContentView: View {
    @AppStorage("interval") var interval = 5.0
    @State private var timer = Timer.publish(every: interval, on: .main, in: .common).autoconnect()
    //or try this:
    @State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>?
    //or try this:
    @State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>? = nil

    init() {
        startTimer()
    }

    func startTimer() {
        timer = Timer.publish(every: interval, on: .main, in: .common).autoconnect()
    }

    func stopTimer() {
        timer.upstream.connect().cancel()
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
Daan
  • 1,417
  • 5
  • 25
  • 40
  • Don't hold it in view, move it instead in view model class to manage by reference properly. – Asperi Jan 16 '21 at 10:18

1 Answers1

1

If you want to keep Timer inside the View, you can declare it as AnyCancellable and then use assign or sink to assign the value.

Here is a possible example:

import Combine
import SwiftUI

struct ContentView: View {
    @AppStorage("interval") var interval = 5.0
    @State private var timer: AnyCancellable?
    
    @State private var currentDate = Date()
    let endDate = Calendar.current.date(byAdding: .second, value: 30, to: Date())!
    
    var body: some View {
        Text(currentDate, style: .relative)
            .onAppear(perform: startTimer)
    }

    func startTimer() {
        timer = Timer.publish(every: interval, on: .main, in: .default)
            .autoconnect()
            .assign(to: \.currentDate, on: self)
            // .sink { self.currentDate = $0 } // alternatively, if you need more then just assigning the value
    }

    func stopTimer() {
        timer?.cancel()
    }
}

However, it might be better to keep the timer logic out of the View like in this example:

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Thank you, but how do I make this work with `.onReceive(timer)`? It seems the type is not right. – Daan Jan 16 '21 at 17:35
  • @Daan Why do you need it? The usual way is to use sink or assign. – pawello2222 Jan 16 '21 at 17:43
  • It's a SwiftUI feature. I can then use the timer to update the view every interval. `onReceive` is a subscriber and works similarly to `sink` I believe. – Daan Jan 16 '21 at 17:57
  • @Daan But is there a reason why do you want to do it with `onReceive`? (Internally it also uses Combine publishers.) If you want to go this path, you need to store both the timer and its subscription (cancellable) as properties. See this post: https://stackoverflow.com/a/62679430/8697793 – pawello2222 Jan 16 '21 at 18:18
  • I guess because I'd like to use as much as the SwiftUI stuff as possible. All I want, really, is to initialize `Timer.TimerPublisher` with the `@AppStorage("interval")` value, but in every example I've found it gets initialized with a normal number. – Daan Jan 16 '21 at 21:16
  • @Daan *All I want, really, is to initialize Timer.TimerPublisher with the @AppStorage("interval")* - this is simple, you can't do it. `@AppStorage` is not available in `init`. In `init` you can only read data from `UserDefaults` in a *standard* way (`UserDefaults.standard.integer(forKey: "interval")`). See https://stackoverflow.com/q/43550813/8697793 – pawello2222 Jan 16 '21 at 22:05
  • Alright, I’ll just do that then. That was my previous solution, which worked fine. – Daan Jan 16 '21 at 22:16
  • @pawello2222: Huge respect for this guy, I'm really annoyed with Timer.TimerPublisher with the optional declaration. Thanks alot for this answer. Really helpful – Abhishek Thapliyal Oct 01 '22 at 13:58