Here I have a simple SwiftUI project which has two timer publishers which are set to fire every 1 second and 2 seconds respectively.
The expected behavior is that the first label would update every second and the second label would update every 2 seconds. However what actually happens is only the first label updates every second and the second label remains at 0 indefinitely.
I know it's possible to make multiple timers using Timer.scheduledTimer(withTimeInterval:) by simply making new variables of those timer instances but it doesn't seem to work the same way with these publishers.
How can I make both timers work?
import SwiftUI
struct ContentView: View {
#State private var counter1 = 0
#State private var counter2 = 0
let timer1 = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timer2 = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 30) {
Text(String(counter1))
.frame(width: 50, height: 20)
Text(String(counter2))
.frame(width: 50, height: 20)
}
.padding()
.onReceive(timer1) {_ in
counter1 += 1
}
.onReceive(timer2) {_ in
counter2 += 2
}
}
}
In SwiftUI your view is a transitory struct. Ie the system remakes the views all the time and throws away the old ones. The View, therefore should not be holding the references to the timers (you will be making new timers and dropping old ones all the time). Make a viewModel and store the values there. In general don't store things on the view that are not in a property wrapper or passed in through the initializer.
final class ViewModel: ObservableObject {
#Published private(set) var first = 0
#Published private(set) var second = 0
private var subscriptions: Set<AnyCancellable> = []
func start() {
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.scan(0) { accumulated, _ in accumulated + 1 }
.assign(to: \.first, on: self)
.store(in: &subscriptions)
Timer.publish(every: 2, on: .main, in: .common)
.autoconnect()
.scan(0) { accumulated, _ in accumulated + 1 }
.assign(to: \.second, on: self)
.store(in: &subscriptions)
}
func stop() {
subscriptions.removeAll()
}
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text(String(describing: viewModel.first))
Text(String(describing: viewModel.second))
}
.onAppear { viewModel.start() }
.onDisappear { viewModel.stop() }
}
}
Related
In all tutorials and in official documentation I only see initialization of timer straight up when the view loads.
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
and later on
.onReceive(timer) {....}
but how should I init timer only on button click and assign it to the unassigned / not connected timer.
Later on I will need to cancel and reconnect, but that is not the issue here. Issue here is connecting only after button click.
I tried to init it like this
#State private var timer: Cancellable?
....
timer = Timer.publish(every: 1, on: .main, in: .common).connect()
But I can not call onReceive on timer inited like this, because first:
Protocol 'Cancellable' as a type cannot conform to 'Publisher'
and second
Argument type 'Cancellable?' does not conform to expected type 'Cancellable'
Just put the timer in a child view and control its visibility with a bool. When the TimerView is removed the state is destroyed and the timer stops.
struct ContentView: View {
#State var started = false
var body: some View {
VStack {
Button(started ? "Stop" : "Start") {
started.toggle()
}
if started {
TimerView()
}
}
}
}
struct TimerView: View {
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
...
Here is demo of possible approach - timer publisher is created with button (same autoconnected), but subscriber should be registered conditionally, because timer in such case is an optional.
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
#State private var timer: Publishers.Autoconnect<Timer.TimerPublisher>? // << here !!
#State private var value = 10 // just demo
var body: some View {
Button {
if timer != nil {
timer = nil // << reset !!
value = 10
} else {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // << create !!
}
} label: {
Text("Toggle")
}
if timer != nil { // << verify !!
Text("Counter: \(value)")
.font(.largeTitle)
.onReceive(timer!) { _ in // << observe !!
value = value == 0 ? 10 : value - 1
}
}
}
}
I am currently learning swiftui and have the following problem:
My code contains a timer that counts up when the app is opened. This works fine so far. Now I want the previous time to be saved when the app is closed and when it is reopened, the value is loaded and counted up from there.
Is there a simple way to implement this?
Here my code:
struct TimeView: View {
#State private var timeTracker = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { time in
self.timeTracker += 1
}
}
}
struct TimeView_Previews: PreviewProvider {
static var previews: some View {
TimeView()
}
}
struct TimeView: View {
#State private var timeTracker = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#AppStorage("TIME") var time: Int = 0
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { _ in timeTracker += 1 }
.onAppear(perform: { timeTracker = time })
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in time = timeTracker }
}
}
//OR
struct TimeView: View {
#AppStorage("TIME") var timeTracker: Int = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
HStack{
Text("\(timeTracker) s")
}
.onReceive(timer) { _ in timeTracker += 1 }
}
}
I'm making the app needs to control several timer at once, and now facing 2 problems.
1: N timers keep running N times faster than expected.
2: Stop button not working for each timer.
2nd problem is the core problem that I'm running into the real project, and would like to understand how to manage the multiple timer instances when they are generated with loop structure.
I pasted the test code, and this code does not use TimelineView for a reason, understanding how the Timer works.
import SwiftUI
import Combine
import Foundation
struct ContentView: View {
#StateObject var timerData = TimerDataViewModel()
var body: some View {
ScrollView{
ForEach(0..<10) { _ in
CurrentDateView(timerData: timerData)
Button(action:{
timerData.stop()
}, label:{
Text("STOP THIS TIMER")
})
}
}
}
}
struct CurrentDateView: View {
#State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
import Foundation
import SwiftUI
class TimerDataViewModel: ObservableObject{
#Published var timer = Timer()
#Published var startTime : Double = 0.0
#Published var secondsOriginal = 0.0
#Published var secondsElapsed = 0.0
#Published var secondsElapsed_ = 0.0
#Published var minutesElapsed = 0.0
#Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
func stop(){
self.timer.invalidate()
}
}
I want a timer to keep going while in the background.
Here is current code for the timer:
struct ContentView: View {
#State var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
} }
How can I make it keep going in the background?
there are several explanation sides about background task...like this e.g. : https://www.raywenderlich.com/5817-background-modes-tutorial-getting-started
I have text in view now i want to update that text using alert on every second.
Here is code i have done.
struct CountDownView : View {
var body: some View {
VStack{
Text("Update text with timer").lineLimit(nil).padding(20)
}.navigationBarTitle(Text("WWDC"), displayMode:.automatic)
}
}
Using Combine:
struct CurrentDateView : View {
#State var now = Date()
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text("\(now)")
.onReceive(timer) {
self.now = Date()
}
}
}
i have managed to update text using alert.
i have declared date as State so whenever date is changed using alert text will also get updated.
struct CurrentDateView : View {
#State var newDate = Date()
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
var body: some View {
Text("\(newDate)")
.onReceive(timer) {
self.newDate = Date()
}
}
}
the original sample was at:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-a-timer-with-swiftui
I do use this approach to update from one of my calls periodically...
see also at:
https://developer.apple.com/documentation/combine/replacing-foundation-timers-with-timer-publishers
struct ContentView: View {
#State var msg = ""
var body: some View {
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
Text(msg)
.onReceive(timer) { input in
msg = MyManager.shared.ifo ?? "not yet received"
}
}
}
I do I haver to call network in other ways, but here I simply call periodically some of my managers.
To stop timer (as in link from hackingSwift..) You can use:
self.timer.upstream.connect().cancel()