Timer publisher init timer after button click - swift

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
}
}
}
}

Related

Save a value at closure and restore the value at reopening

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 }
}
}

How to set up multiple combine timer publishers?

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() }
}
}

SwiftUI run code periodically while button is being held down; run different code when it is just tapped?

What I'm trying to do is implement a button that runs a certain line of code every 0.5 seconds while it is being held down (it can be held down indefinitely and thus run the print statement indefinitely). I'd like it to have a different behavior when it is tapped. Here is the code:
struct ContentView: View {
#State var timeRemaining = 0.5
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
#State var userIsPressing = false //detecting whether user is long pressing the screen
var body: some View {
VStack {
Image(systemName: "chevron.left").onReceive(self.timer) { _ in
if self.userIsPressing == true {
if self.timeRemaining > 0 {
self.timeRemaining -= 0.5
}
//resetting the timer every 0.5 secdonds and executing code whenever //timer reaches 0
if self.timeRemaining == 0 {
print("execute this code")
self.timeRemaining = 0.5
}
}
}.gesture(LongPressGesture(minimumDuration: 0.5)
.onChanged() { _ in
//when longpressGesture started
self.userIsPressing = true
}
.onEnded() { _ in
//when longpressGesture ended
self.userIsPressing = false
}
)
}
}
}
At the moment, it's sort of reverse of what I need it to do; the code above runs the print statement indefinitely when I click the button once but when I hold it down, it only does the execution once...how can I fix this?
Here is a solution - to get continuous pressing it needs to combine long press gesture with sequenced drag and add timer in handlers.
Updated: Tested with Xcode 11.4 / iOS 13.4 (in Preview & Simulator)
struct TimeEventGeneratorView: View {
var callback: () -> Void
private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
Color.clear
.onReceive(self.timer) { _ in
self.callback()
}
}
}
struct TestContinuousPress: View {
#GestureState var pressingState = false // will be true till tap hold
var pressingGesture: some Gesture {
LongPressGesture(minimumDuration: 0.5).sequenced(before:
DragGesture(minimumDistance: 0, coordinateSpace:
.local)).updating($pressingState) { value, state, transaction in
switch value {
case .second(true, nil):
state = true
default:
break
}
}.onEnded { _ in
}
}
var body: some View {
VStack {
Image(systemName: "chevron.left")
.background(Group { if self.pressingState { TimeEventGeneratorView {
print(">>>> pressing: \(Date())")
}}})
.gesture(TapGesture().onEnded {
print("> just tap ")
})
.gesture(pressingGesture)
}
}
}

Keep running the timer in the background at swiftUI

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

How to update text using timer in SwiftUI

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()