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 }
}
}
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
}
}
}
}
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() }
}
}
My goal is to create a view in SwiftUI that starts with 0. When you press the view, a timer should start counting upwards, and tapping again stops the timer. Finally, when you tap again to start the timer, the timer should begin at 0.
Here is my current code:
import SwiftUI
struct TimerView: View {
#State var isTimerRunning = false
#State private var endTime = Date()
#State private var startTime = Date()
let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
var tap: some Gesture {
TapGesture(count: 1)
.onEnded({
isTimerRunning.toggle()
})
}
var body: some View {
Text("\(endTime.timeIntervalSince1970 - startTime.timeIntervalSince1970)")
.font(.largeTitle)
.gesture(tap)
.onReceive(timer) { input in
startTime = isTimerRunning ? startTime : Date()
endTime = isTimerRunning ? input : endTime
}
}
}
This code causes the timer to start instantly and never stop, even when I tap on it. The timer also goes backward (into negative numbers) rather than forward.
Can someone please help me understand what I am doing wrong? Also, I would like to know if this is a good overall strategy for a timer (using Timer.publish).
Thank you!
Here is a fixed version. Take a look at the changes I made.
.onReceive now updates a timerString if the timer is running. The timeString is the interval between now (ie. Date()) and the startTime.
Tapping on the timer sets the startTime if it isn't running.
struct TimerView: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "0.00"
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = String(format: "%.2f", (Date().timeIntervalSince( self.startTime)))
}
}
.onTapGesture {
if !isTimerRunning {
timerString = "0.00"
startTime = Date()
}
isTimerRunning.toggle()
}
}
}
The above version, while simple, bugs me that the Timer is publishing all the time. We only need the Timer publishing when the timer is running.
Here is a version that starts and stops the Timer:
struct TimerView: View {
#State var isTimerRunning = false
#State private var startTime = Date()
#State private var timerString = "0.00"
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(self.timerString)
.font(Font.system(.largeTitle, design: .monospaced))
.onReceive(timer) { _ in
if self.isTimerRunning {
timerString = String(format: "%.2f", (Date().timeIntervalSince( self.startTime)))
}
}
.onTapGesture {
if isTimerRunning {
// stop UI updates
self.stopTimer()
} else {
timerString = "0.00"
startTime = Date()
// start UI updates
self.startTimer()
}
isTimerRunning.toggle()
}
.onAppear() {
// no need for UI updates at startup
self.stopTimer()
}
}
func stopTimer() {
self.timer.upstream.connect().cancel()
}
func startTimer() {
self.timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
}
}
Stop-watch Timer
The following approach allows you create a start/stop/reset SwiftUI Timer using #Published and #ObservedObject property wrappers, along with the ObservableObject protocol.
Here's the ContentView structure:
import SwiftUI
struct ContentView: View {
#ObservedObject var stopWatch = Stop_Watch()
var body: some View {
let minutes = String(format: "%02d", stopWatch.counter / 60)
let seconds = String(format: "%02d", stopWatch.counter % 60)
let union = minutes + " : " + seconds
ZStack {
Color.black.ignoresSafeArea()
VStack {
Spacer()
HStack {
Button("Start") { self.stopWatch.start() }
.foregroundColor(.purple)
Button("Stop") { self.stopWatch.stop() }
.foregroundColor(.orange)
Button("Reset") { self.stopWatch.reset() }
.foregroundColor(.yellow)
}
Spacer()
Text("\(union)")
.foregroundColor(.teal)
.font(.custom("", size: 90))
Spacer()
}
}
}
}
...and Stop_Watch class:
class Stop_Watch: ObservableObject {
#Published var counter: Int = 0
var timer = Timer()
func start() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0,
repeats: true) { _ in
self.counter += 1
}
}
func stop() {
self.timer.invalidate()
}
func reset() {
self.counter = 0
self.timer.invalidate()
}
}
Updated for Swift 5.7 and iOS 16 to display a timer that counts up seconds and minutes like a simple stopwatch. Using DateComponentsFormatter to format the minutes and seconds.
struct StopWatchView: View {
#State var isRunning = false
#State private var startTime = Date()
#State private var display = "00:00"
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text(display)
.font(.system(size: 20, weight: isRunning ? .bold : .light, design: .monospaced))
.foregroundColor(.accentColor)
.onReceive(timer) { _ in
if isRunning {
let duration = Date().timeIntervalSince(startTime)
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
display = formatter.string(from: duration) ?? ""
}
}
.onTapGesture {
if isRunning {
stop()
} else {
display = "00:00"
startTime = Date()
start()
}
isRunning.toggle()
}
.onAppear {
stop()
}
}
func stop() {
timer.upstream.connect().cancel()
}
func start() {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
}
struct StopWatchView_Previews: PreviewProvider {
static var previews: some View {
StopWatchView()
}
}
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()