Keep running the timer in the background at swiftUI - swift

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

Related

Timer publisher init timer after button click

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

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

ScrollView containing #State property causes glitchy scrolling - SwiftUI

This simple ScrollView has very glitchy scrolling every time #State property is updated.
import SwiftUI
struct ContentView: View {
#State var currentTime: Double = 0
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollView {
Text("currentTime: \(currentTime)")
}
.onReceive(timer) { input in
currentTime = input.timeIntervalSince1970
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Glitchy scrolling video. ScrollView not working.
What I've tried
Putting the .onReceive inside the ScrollView
Updating the #State currentTime with other methods besides a timer. They all glitch like this.
How can I get the ScrollView to smoothly scroll, even during #State updates?
I think it is a bug! I was surprised how even is possible because i am updating my ScrollView all the time while scrolling! you need to add one line of code to solve the issue!
import SwiftUI
struct ContentView: View {
#State var currentTime: Double = 0
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollView {
Color.clear.frame(height: 0) // <<: Here!
Text("currentTime: \(currentTime)")
}
.onReceive(timer) { input in
currentTime = input.timeIntervalSince1970
}
}
}

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