SwiftUI | why does the wheelPicker not work as expected if a Timer is scheduled? - swift

I have problem with making a selection with a wheelPicker if timer is running simultaneously.
A) default as runloop mode of timer:
the wheelPicker works fine and updates $selection But the counter in my ContenView won't keep counting while the wheelPicker is spinning
B) common as runloop mode of timer
the counter keeps counting while I interact with the wheelPicker, but the wheel pickers selection can't be changed if the timer fires before the picker is done spinning.
Here is my class holding the timer:
import Foundation
class MyTimer: ObservableObject {
private var timer : Timer?
#Published var counter : Int = 0
public func launchTimer(){
self.timer = Timer.scheduledTimer( withTimeInterval: 0.2, repeats: true, block:{_ in
self.counter += 1
})
if self.timer != nil {
RunLoop.current.add(timer!, forMode: .common)
}
}
}
Here is my contentView
import SwiftUI
struct ContentView: View {
#StateObject var myTimer : MyTimer = MyTimer()
#State var selecion : Int = 0
var body: some View {
VStack {
Text("counter: \(myTimer.counter)")
.padding()
Text("selection: \(self.selecion)")
.padding()
Picker(selection: $selecion, label: Text("picker") ) {
ForEach(0..<50, id: \.self) { id in
Text(String(id))
}
}
.pickerStyle(WheelPickerStyle())
Button(action: {
myTimer.launchTimer()
}, label: {
Text("launch timer")
})
}
}
}
I am fully aware, the code above has other issues, since the Timer could be scheduled multiple times. But I hope it is sufficient for illustrational purposes.

Related

How to allow timer to count down in background on Watch OS

Made a pretty simple timer app, but I've found that Apple Watch documentation is no where near as good as the phone. It's been difficult to find any answers on this.
Here is my content view. The seconds for the timer is kept in '''timeVal'''
struct ContentView: View {
#State var timeVal = 0.0
#State var timerScreenShow:Bool = false
var body: some View {
VStack {
HStack{
Spacer()
Text("\(self.timeVal.hourMinute)")
Spacer()
}
//various buttons to set timer
NavigationLink(
destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: Double(Int(self.timeVal)), initialTime: Int(self.timeVal)),
isActive: $timerScreenShow,
label: {
Text("Start")
}).background(Color.green).cornerRadius(22)
}
}
}
Then, here is the bit that keeps time:
struct TimerView: View {
#Binding var timerScreenShow:Bool
#State var timeVal:Double
let initialTime:Int
var body: some View {
if timeVal > -1 {
VStack {
ZStack {
Text("\(self.timeVal.hourMinuteSecond)").font(.system(size: 20))
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if self.timeVal > -1 {
self.timeVal -= 1
}
}
}
//Text("\(self.timeVal.hourMinuteSecond)")
ProgressBar(progress: Int(self.timeVal), initial: self.initialTime).frame(width: 90.0, height: 90.0)
}
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Cancel")
.foregroundColor(Color.red)
})
.padding(.top)
}
} else {
Button(action: {
self.timerScreenShow = false
}, label: {
Text("Done!")
.foregroundColor(Color.green)
}).onAppear() {
WKInterfaceDevice.current().play(.notification)
}
}
}
}
What do I need to do to keep the timer running when the watch app is closed?
Don't keep track of how much time is left on the counter. Instead, store the startTime = Date(). Use the timer only as a signal to update the UI. When the timer ticks, compute the time left as startAmount - (Date().timeInterval(since: startTime)). Store startAmount and startTime and Bool isTimerRunning in persistent storage such as #AppStorage. When the app restarts, pick up where you left off.

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

SwiftUI Multiple Timer Management

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

SwiftUI instanced #State variable

I am quite new to SwiftUI. I have a following "Counter" view that counts up every second. I want to "reset" the counter when the colour is changed:
struct MyCounter : View {
let color: Color
#State private var count = 0
init(color:Color) {
self.color = color
_count = State(initialValue: 0)
}
var body: some View {
Text("\(count)").foregroundColor(color)
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
}
}
Here is my main view that uses counter:
struct ContentView: View {
#State var black = true
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow)
Button(action:{self.black.toggle()}) { Text("Toggle") }
}
}
}
When i click "Toggle" button, i see MyCounter constructor being called, but #State counter persists and never resets. So my question is how do I reset this #State value? Please note that I do not wish to use counter as #Binding and manage that in the parent view, but rather MyCounter be a self-contained widget. (this is a simplified example. the real widget I am creating is a sprite animator that performs sprite animations, and when I swap the image, i want the animator to start from frame 0). Thanks!
There are two way you can solve this issue. One is to use a binding, like E.Coms explained, which is the easiest way to solve your problem.
Alternatively, you could try using an ObservableObject as a view model for your timer. This is the more flexible solution. The timer can be passed around and it could also be injected as an environment object if you so desire.
class TimerModel: ObservableObject {
// The #Published property wrapper ensures that objectWillChange signals are automatically emitted.
#Published var count: Int = 0
init() {}
func start() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
func reset() {
count = 0
}
}
Your timer view then becomes
struct MyCounter : View {
let color: Color
#ObservedObject var timer: TimerModel
init(color: Color, timer: TimerModel) {
self.color = color
self.timer = timer
}
var body: some View {
Text("\(timer.count)").foregroundColor(color)
.onAppear(){
self.timer.start()
}
}
}
Your content view becomes
struct ContentView: View {
#State var black = true
#ObservedObject var timer = TimerModel()
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow, timer: self.timer)
Button(action: {
self.black.toggle()
self.timer.reset()
}) {
Text("Toggle")
}
}
}
}
The advantage of using an observable object is that you can then keep track of your timer better. You could add a stop() method to your model, which invalidates the timer and you can call it in a onDisappear block of your view.
One thing that you have to be careful about this approach is that when you're using the timer in a standalone fashion, where you create it in a view builder closure with MyCounter(color: ..., timer: TimerModel()), every time the view is rerendered, the timer model is replaced, so you have to make sure to keep the model around somehow.
You need a binding var:
struct MyCounter : View {
let color: Color
#Binding var count: Int
var body: some View {
Text("\(count)").foregroundColor(color)
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}
}
}
struct ContentView: View {
#State var black = true
#State var count : Int = 0
var body: some View {
VStack {
MyCounter(color: black ? Color.black : Color.yellow , count: $count)
Button(action:{self.black.toggle()
self.count = 0
}) { Text("Toggle") }
}
}
}
Also you can just add one State Value innerColor to help you if you don't like binding.
struct MyCounter : View {
let color: Color
#State private var count: Int = 0
#State private var innerColor: Color?
init(color: Color) {
self.color = color
}
var body: some View {
return Text("\(self.count)")
.onAppear(){
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.count = self.count + 1 }
}.foregroundColor(color).onReceive(Just(color), perform: { color in
if self.innerColor != self.color {
self.count = 0
self.innerColor = color}
})
}
}

getting unresolved identifier 'self' in SwiftUI code trying to use Timer.scheduledTimer

In SwiftUI I'm noting to use a Timer that:
Try 1 - This doesn't work as get "Use of unresolved identifier 'self'"
var timer2: Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
self.angle = self.angle + .degrees(1)
}
Try 2 - Works, but have to put in an "_ = self.timer" to start it later
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {_ in
self.angle = self.angle + .degrees(1)
}
}
// then after need to use " .onAppear(perform: {_ = self.timer}) "
Is there a way to get my Try1 working? That is where in a SwiftUI file I can create the timer up front? Or actually where in SwiftUI would one normally start and stop the timer? i.e. where are lifecycle methods
Whole file:
import SwiftUI
struct ContentView : View {
#State var angle: Angle = .degrees(55)
// var timer2: Timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
// self.angle = self.angle + .degrees(1)
// }
var timer: Timer {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {_ in
self.angle = self.angle + .degrees(1)
}
}
private func buttonAction() {
print("test")
self.angle = self.angle + .degrees(5)
}
var body: some View {
VStack{
Text("Start")
ZStack {
Circle()
.fill(Color.blue)
.frame(
width: .init(integerLiteral: 100),
height: .init(integerLiteral: 100)
)
Rectangle()
.fill(Color.green)
.frame(width: 20, height: 100)
// .rotationEffect(Angle(degrees: 25.0))
.rotationEffect(self.angle)
}
Button(action: self.buttonAction) {
Text("CLICK HERE")
}
Text("End")
}
.onAppear(perform: {_ = self.timer})
}
}
It isn't clear to me that you need a timer for your example, but since there is a great deal of misinformation out there about how to include a Timer in a SwiftUI app, I'll demonstrate.
The key is to put the timer elsewhere and publish each time it fires. We can easily do this by adding a class that holds the timer as a bindable object to our environment (note that you will need to import Combine):
class TimerHolder : BindableObject {
var timer : Timer!
let didChange = PassthroughSubject<TimerHolder,Never>()
var count = 0 {
didSet {
self.didChange.send(self)
}
}
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[weak self] _ in
guard let self = self else { return }
self.count += 1
}
}
}
We will need to pass an instance of this class down through the environment, so we modify the scene delegate:
window.rootViewController =
UIHostingController(rootView: ContentView())
becomes
window.rootViewController =
UIHostingController(rootView: ContentView()
.environmentObject(TimerHolder()))
Finally, lets put up some UI that starts the timer and displays the count, to prove that it is working:
struct ContentView : View {
#EnvironmentObject var timerHolder : TimerHolder
var body: some View {
VStack {
Button("Start Timer") { self.timerHolder.start() }
Text(String(self.timerHolder.count))
}
}
}
EDIT Update for those who have not been following the plot: BindableObject has migrated into ObservableObject, and there is no need any more for manual signalling. So:
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
// ... and the rest is as before ...
Based on matt's answer above, I think there is a less complicated version possible.
class TimerHolder : ObservableObject {
var timer : Timer!
#Published var count = 0
func start() {
self.timer?.invalidate()
self.count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
self.count += 1
print(self.count)
}
}
}
With : ObservableObjectand #Publish var count
and in the View
#ObservedObject var durationTimer = TimerHolder()
...
Text("\(durationTimer.count) Seconds").onAppear {
self.durationTimer.start()
}
you can observe any change to the count variable and update the Text in the view accordingly.
As mentioned in other comments, make sure though, that the timer is not restarted unexpectedly due to an action recreating the view.