So... bit of a mind-bender, but you're losing your "self" reference, and possibly leaking timers into the runloop.
SwiftUI views are not actually views, but more like view-blueprints, that can be asked for a view. This means they can be inited as needed by the system and body can be called as needed.
This means your setupTimer method is at risk of getting called multiple times and timers could keep getting put into the run loop. It also means that the "self" reference inside the timer closure is super dubious.
Throwing it into an observable controller that's stored as a state object property will keep the state more constant and it will work.
(I'm leery of the .task method in this example and it's just to get the timer started. You probably want debounce protection if you're gonna store em in the runLoop. You should probably seek an alternative async/await solution for that)
@Observable
class Controller {
var referenceDate: Date = Date()
init() {
}
func setupTimer() {
let calendar = Calendar.current
guard let triggerDate = calendar.nextDate(
after: Date(),
matching: DateComponents(hour: 20, minute: 16, second: 0),
matchingPolicy: .nextTime
) else { return }
let timer = Timer(fire: triggerDate, interval: 0.2, repeats: false) { _ in
DispatchQueue.main.async {
self.referenceDate = Date()
}
print("runLoop!")
}
RunLoop.main.add(timer, forMode: .common)
}
}
struct ContentView: View {
@State var controller = Controller()
var body: some View {
VStack {
Text("Ref time: \(controller.referenceDate.formatted(date: .abbreviated, time: .standard))")
Button("Huh") {
controller.referenceDate = Date()
}
}
.task {
controller.setupTimer()
}
}
}