问题
Normally, I would use an optional variable to hold my Timer
reference, as it's nice to be able to invalidate and set it to nil
before recreating.
I'm trying to use SwiftUI
and want to make sure I'm correctly doing so...
I declare as:
@State var timer:Publishers.Autoconnect<Timer.TimerPublisher>? = nil
Later I:
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
To drive a UI text control I use:
.onReceive(timer) { time in
print("The time is now \(time)")
}
What is the right way with this Combine
typed Timer
to invalidate and recreate?
I've read one should call:
self.timer.upstream.connect().cancel()
However, do I also need to invalidate or simply then nil
out?
回答1:
There is no need to throw away the TimerPublisher
itself. Timer.publish
creates a Timer.TimerPublisher
instance, which like all other publishers, only starts emitting values when you create a subscription to it - and it stops emitting as soon as the subscription is closed.
So instead of recreating the TimerPublisher
, you just need to recreate the subscription to it - when the need arises.
So assign the Timer.publish
on declaration, but don't autoconnect()
it. Whenever you want to start the timer, call connect
on it and save the Cancellable
in an instance property. Then whenever you want to stop the timer, call cancel
on the Cancellable
and set it to nil
.
You can find below a fully working view with a preview that starts the timer after 5 seconds, updates the view every second and stops streaming after 30 seconds.
This can be improved further by storing the publisher and the subscription on a view model and just injecting that into the view.
struct TimerView: View {
@State private var text: String = "Not started"
private var timerSubscription: Cancellable?
private let timer = Timer.publish(every: 1, on: .main, in: .common)
var body: some View {
Text(text)
.onReceive(timer) {
self.text = "The time is now \($0)"
}
}
mutating func startTimer() {
timerSubscription = timer.connect()
}
mutating func stopTimer() {
timerSubscription?.cancel()
timerSubscription = nil
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
var timerView = TimerView()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
timerView.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
timerView.stopTimer()
}
return timerView
}
}
With a view model, you don't even need to expose a TimerPublisher
(or any Publisher
) to the view, but can simply update an @Published
property and display that in the body
of your view. This enables you to declare timer
as autoconnect
, which means you don't manually need to call cancel
on it, you can simply nil
out the subscription reference to stop the timer.
class TimerViewModel: ObservableObject {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private var timerSubscription: Cancellable?
@Published var time: Date = Date()
func startTimer() {
timerSubscription = timer.assign(to: \.time, on: self)
}
func stopTimer() {
timerSubscription = nil
}
}
struct TimerView: View {
@ObservedObject var viewModel: TimerViewModel
var body: some View {
Text(viewModel.time.description)
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = TimerViewModel()
let timerView = TimerView(viewModel: viewModel)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
viewModel.stopTimer()
}
return timerView
}
}
来源:https://stackoverflow.com/questions/62678977/swiftui-optional-timer-reset-and-recreate