Stop a timer on an if statement within a view - swift

I'am quite new to Swift and I despair of a problem.
I created a class with two functions to start and stop a timer.
There's a struct that starts the timer (start function) with onAppear.
Within the struct there's a button to stop the time.
import SwiftUI
class StopWatch: ObservableObject {
#Published var secondsElapsed = 0.0
var timer = Timer()
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1 }
}
func stopTimer() {
timer.invalidate()
}
}
struct Test: View {
#ObservedObject var stopWatch = StopWatch()
var body: some View {
VStack{
Button("Stop") {
stopWatch.stopTimer()
}
Text(String(format: "%.1f", self.stopWatch.secondsElapsed))
}.onAppear(perform: {stopWatch.startTimer()})
}
}
The timer starts if the view starts. So far so good :-).
But I want to get rid of the stop button and replace it with an if statement like:
if stopWatch.secondsElapsed > 5.0 {
stopWatch.stopTimer() }
I want to stop the timer without a user action by this if statement and the current timer value should show up.
I tried for hours, but I didn't get it. For that case I get the error message "Type '()' cannot conform to 'View'", but I tried a lot of other things and got a lot of other errors.
Can anyone help me?

If you want the timer to stop on 5 seconds you should probably do this in the closure from the timer. You can do this:
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
if self.secondselapsed == 5.0 {
timer.invalidate()
}
}
}
You get this error ("Type '()' cannot conform to 'View'") because you try to execute code within the body of a SwiftUI view without returning a view. If you really want to do this in the body for some reason you could do it like this: (I would not recommend doing this)
if stopWatch.secondsElapsed > 5.0 {
Text("Five seconds elapsed")
.onAppear {
stopWatch.stop()
}
}

Related

Invalidating a Timer in SwiftUI doesn't work

I have a Timer object within my view:
#State var timer: Timer?
Inside the view's body, I start the timer when the value of popup changes to true, and I invalidate the timer when viewModel.present changes to true.
var body: some View {
/* ... */
.onChange(of: viewModel.popup) { popup in guard popup else { return }; setupTimer() }
.onChange(of: viewModel.present) { present in guard present else { return }; resetTimer() }
}
In setupTimer(), I create the timer instance:
private func setupTimer() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false, block: { _ in
// This code won't stop running!
})
}
Next, in resetTimer(), I invalidate and remove the timer:
private func resetTimer() {
timer?.invalidate()
timer = nil
// ...
}
But the timer continues to run. I'm not sure why timer?.invaludate() fails to work; I've used breakpoints to assure that
setupTimer() is indeed only called once such that only a single timer is running (as per the answer here)
self.timer is non-nil when invalidating within resetTimer (as per the answer here)
Any ideas on what is going wrong here? I'm running this program in macOS 12.2.1.
Edit: Ok, so after some investigation, I've seemed to narrow down the problem. It seems as though the timer is successfully invalidated when setupTimer() is called within a tap gesture or action by the user. However, if setupTimer() is called "automatically" within a view (i.e. via onAppear, onChange, etc.) then it cannot be invalidated. So the new question I have is: why is this? Why can I not call setupTimer() within onAppear/onChange and cancel the timer?
There could be some other code (that you are not showing) affecting the timer, or maybe
it does not work well on macOS 12.2.1. However, here is a simple test that works well for me,
on macos 12.3 (the only one I have) using xcode 13.3, targets ios 15 and macCatalyst 12. Does this code work for you?
struct ContentView: View {
#State var timer: Timer?
var body: some View {
VStack (spacing: 55) {
Button(action: {setupTimer()}) {
Text("start")
}
Button(action: {resetTimer()}) {
Text("reset")
}
}.frame(width: 333, height: 333)
.onAppear {
setupTimer()
}
}
private func setupTimer() {
guard timer == nil else { return }
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
// This code stop running when timer invalidate
print("----> timer running")
})
}
private func resetTimer() {
timer?.invalidate()
timer = nil
}
}
Note that in your code, after resetTimer() is called any change in viewModel.popup may trigger
setupTimer() again, and so the timer starts again since it is nil.

Can a timer be scheduled to start at a real time?

Making a simple timer is easy. We know how to build something that fires at a certain interval. Functions and other tasks can be delayed with Dispatch Queue. Has anyone ever tried to start a function with timed precision?
I've found some documentation that shows that within the Dispatch Framework there are some structs that deal with microsecond(DispatchWallTime) and nanosecond(DispatchTime) precision. Given the assumption that a timer isn't "real-time", how can we go about firing a function precisely at a later date?
Here's a simple timer, the goal is to make the start function start at a later time but precisely at the start of a minute or a specific second.
struct ContentView: View {
#StateObject var timeManager = TimeManager()
#State var date = Date.now.formatted(.dateTime.hour().minute().second())
var body: some View {
VStack {
Text("Current Time: \(date)")
Text("Elapsed Time: \(timeManager.elapsedTime)").monospacedDigit()
HStack {
Button(action: {
timeManager.start()
}) {
Image(systemName: "play.fill")
.tint(.green)
}
Button(action: {
timeManager.stop()
}) {
Image(systemName: "stop.fill")
.tint(.red)
}
}
.font(.largeTitle)
}
}
}
class TimeManager: ObservableObject {
#Published var elapsedTime = 0
var timer = Timer()
// Make this function start at a later date/time with precision.
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.elapsedTime += 1
}
}
func stop() {
timer.invalidate()
}
}
I found an answer that gets to the point. It's from Paul Hudson, If anyone would like to answer this question with more depth to it or variations I'll mark it as the answer.
let date = Date().addingTimeInterval(5)
let timer = Timer(fireAt: date, interval: 0, target: self, selector: #selector(runCode), userInfo: nil, repeats: false)
RunLoop.main.add(timer, forMode: .common)
The reason I like this answer is twofold, one you can use a date. Then you can bind this to a date picker or any arbitrary date or time you choose. Two, I like it because it's built right into Swift. Please add your answer if you have a better solution. Thanks everyone!

How to make a LongPressGesture that runs repeatedly while the button is still being held down in SwiftUI?

I'd like to run the code in the longPressGesture every 0.5 seconds while the button is being held down. Any ideas on how to implement this?
import SwiftUI
struct ViewName: View {
var body: some View {
VStack {
Button(action: { } ) {
Image(systemName: "chevron.left")
.onTapGesture {
//Run code for tap gesture here
}
.onLongPressGesture (minimumDuration: 0.5) {
//Run this code every 0.5 seconds
}
}
}
}
You can do this by using timer. Make the timer starts when the user long pressed the image, and if the timer reaches 0, you can add two actions: 1. resetting the timer back to 0.5 seconds and 2.code you want to run every 0.5 seconds
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
}
)
}
}
}
Oh boy, I'm not really an expert but I've had a similar problem (detecting pressing and releasing) recently and the solution I've found is less than elegant. I'd love if someone show a more elegant solution but here's my monstrosity:
import SwiftUI
import Combine
struct ContentView: View {
#State private var ticker = Ticker()
#State private var isPressed: Bool = false
#State private var timePassed: TimeInterval?
var body: some View {
Button(action: {
// Action when tapped
NSLog("Tapped!")
}) {
Text(self.isPressed ? "Pressed for: \(String(format: "%0.1f", timePassed ?? 0))" : "Press and hold")
.padding()
.background(Capsule().fill(Color.yellow))
}
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { (value) in
self.isPressed = value
if value == true {
self.timePassed = 0
self.ticker.start(interval: 0.5)
}
}, perform: {})
.onReceive(ticker.objectWillChange) { (_) in
// Stop timer and reset the start date if the button in not pressed
guard self.isPressed else {
self.ticker.stop()
return
}
// Your code here:
self.timePassed = self.ticker.timeIntervalSinceStarted
}
}
}
/// Helper "ticker" that will publish regular "objectWillChange" messages
class Ticker: ObservableObject {
var startedAt: Date = Date()
var timeIntervalSinceStarted: TimeInterval {
return Date().timeIntervalSince(startedAt)
}
private var timer: Timer?
func start(interval: TimeInterval) {
stop()
startedAt = Date()
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
self.objectWillChange.send()
}
}
func stop() {
timer?.invalidate()
}
deinit {
timer?.invalidate()
}
}
This requires an explanation:
onTapGesture() is not necessary here because that's what Button does by default, so just putting the code you need to run in the action block should be sufficient;
There is a limited number of gestures available in SwiftUI and, as far as I know, the only way to make new gestures is to combine existing ones;
There is no gesture that would continuously execute some code as long as the button is pressed, but LongPressGesture might be the closest to it. However, this gesture is recognized (and ends) when the allotted time expires but you want to detect the touch as long as it lasts, hence the minimumDuration: .infinity parameter;
LongPressGesture would also end when the touch has moved away a long enough distance, however, that's not how the Button works – you can wander away and return back and, as long as you've lifted the touch on top of the button view, the gesture will be recognized as a button press. We should replicate this behavior in our long press as well, hence maximumDistance: .infinity;
With these parameters, the LongPressGesture will never be recognized, but there is a press parameter that now allows us to be notified when presses start and end;
Some sort of a timer could be used to execute a code block every so often; I've copied this "ticker" ObservableObject from somewhere. It has to be an ObservableObject because, that way, we can subscribe to it's updates within the View;
Now, when the button in pressed, we start the ticker;
When ticker ticks, we capture that with the onReceive() subscriber and that allows us to do something on every tick.
Something like that; again, I'd love someone to show me a better way :)
Good luck with your project!
–Baglan
I simply cleaned up #Baglan 's "monstrosity" a bit this morning.
import Foundation
import SwiftUI
struct LongPressButton: View {
#ObservedObject var timer = PressTimer()
enum PressState {
case inactive
case pressing
case finished
}
#State private var pressState = PressState.inactive
var duration: Double = 2.0
var body: some View {
button
.onLongPressGesture(minimumDuration: duration, maximumDistance: 50, pressing: { (value) in
if value == true {
/// Press has started
self.pressState = .pressing
print("start")
self.timer.start(duration)
} else {
/// Press has cancelled
self.pressState = .inactive
print("stop")
self.timer.stop()
}
}, perform: {
/// Press has completed successfully
self.pressState = .finished
print("done")
})
}
var button: some View {
pressState == .pressing ? Text("Pressing - \(String(format: "%.0f", timer.percent))%")
: Text("Start")
}
}
class PressTimer: ObservableObject {
#Published var percent: CGFloat = 0
private var count: CGFloat = 0
private let frameRateHz: CGFloat = 60
private var durationSeconds: CGFloat = 2
var timer: Timer?
func start(_ duration: Double = 2.0) {
self.durationSeconds = CGFloat(duration)
let timerInterval: CGFloat = 1 / frameRateHz
timer = Timer.scheduledTimer(withTimeInterval: Double(timerInterval), repeats: true, block: { _ in
self.count += timerInterval
self.percent = self.count / self.durationSeconds * 100
})
}
func stop() {
self.count = 0
self.percent = 0
self.timer?.invalidate()
self.timer = nil
}
}

Escaping closure captures mutating self in Swift Combine Subscriber [duplicate]

GOAL: I'm trying to make a general struct that can take an array of Ints and go through and set a timer for each one (and show a screen) in succession.
Problem: I get Escaping closure captures mutating 'self' parameter error as shown in the code.
import SwiftUI
struct ContentView: View {
#State private var timeLeft = 10
#State private var timers = Timers(timersIWant: [6, 8, 14])
// var timersIWantToShow: [Int] = [6, 8, 14]
var body: some View {
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
VStack {
Text("Hello, World! \(timeLeft)")
.foregroundColor(.white)
.background(Color.blue)
.font(.largeTitle)
}
}
}
struct Timers {
var countDownTimeStart: Int = 0
var currentTimer = 0
var timersIWant: [Int]
mutating func startTimer(with countDownTime: Int) {
var timeLeft = countDownTime
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in //Escaping closure captures mutating 'self' parameter
if timeLeft > 0 {
timeLeft -= 1
} else {
timer.invalidate()
self.currentTimer += 1
if self.currentTimer < self.timersIWant.count {
self.startTimer(with: self.timersIWant[self.currentTimer])
} else {
timer.invalidate()
}
}
})
}
}
}
I'm not sure if this has to do with my recursvie function (maybe this is bad form?) and I'm guessing the escaping closure is the func startTimer and the offending the 'self' parameter is the countDownTime parameter, but I'm not really sure what is happening or why it's wrong.
Escaping closure captures mutating 'self' parameter
The escaping closure is the Button's action parameter, and the mutating function is your startTimer function.
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
A simple solution is to change Times to be a class instead of a struct.
Also notice that timeLeft is defined in two places. I don't think this is what you want.
As Gil notes, this needs to be a class because you are treating it as a reference type. When you modify currentTimer, you don't expect that to create a completely new Timers instance, which is what happens with a value type (struct). You expect it to modify the existing Timers instance. That's a reference type (class). But to make this work, there's quite a bit more you need. You need to tie the Timers to the View, or the View won't update.
IMO, the best way to approach this is let Timers track the current timeLeft and have the view observe it. I've also added an isRunning published value so that the view can reconfigure itself based on that.
struct TimerView: View {
// Observe timers so that when it publishes changes, the view is re-rendered
#ObservedObject var timers = Timers(intervals: [10, 6, 8, 14])
var body: some View {
Button(action: { self.timers.startTimer()} ) {
Text("Hello, World! \(timers.timeLeft)")
.foregroundColor(.white)
.background(timers.isRunning ? Color.red : Color.blue) // Style based on isRunning
.font(.largeTitle)
}
.disabled(timers.isRunning) // Auto-disable while running
}
}
// Timers is observable
class Timers: ObservableObject {
// And it publishes timeLeft and isRunning; when these change, update the observer
#Published var timeLeft: Int = 0
#Published var isRunning: Bool = false
// This is `let` to get rid of any confusion around what to do if it were changed.
let intervals: [Int]
// And a bit of bookkeeping so we can invalidate the timer when needed
private var timer: Timer?
init(intervals: [Int]) {
// Initialize timeLeft so that it shows the upcoming time before starting
self.timeLeft = intervals.first ?? 0
self.intervals = intervals
}
func startTimer() {
// Invalidate the old timer and stop running, in case we return early
timer?.invalidate()
isRunning = false
// Turn intervals into a slice to make popFirst() easy
// This value is local to this function, and is captured by the timer callback
var timerLengths = intervals[...]
guard let firstInterval = timerLengths.popFirst() else { return }
// This might feel redundant with init, but remember we may have been restarted
timeLeft = firstInterval
isRunning = true
// Keep track of the timer to invalidate it elsewhere.
// Make self weak so that the Timers can be discarded and it'll clean itself up the next
// time it fires.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}
// Decrement the timer, or pull the nextInterval from the slice, or stop
if self.timeLeft > 0 {
self.timeLeft -= 1
} else if let nextInterval = timerLengths.popFirst() {
self.timeLeft = nextInterval
} else {
timer.invalidate()
self.isRunning = false
}
}
}
}

Why can't I mutate a variable initially set to a certain parameter when the func was called?

GOAL: I'm trying to make a general struct that can take an array of Ints and go through and set a timer for each one (and show a screen) in succession.
Problem: I get Escaping closure captures mutating 'self' parameter error as shown in the code.
import SwiftUI
struct ContentView: View {
#State private var timeLeft = 10
#State private var timers = Timers(timersIWant: [6, 8, 14])
// var timersIWantToShow: [Int] = [6, 8, 14]
var body: some View {
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
VStack {
Text("Hello, World! \(timeLeft)")
.foregroundColor(.white)
.background(Color.blue)
.font(.largeTitle)
}
}
}
struct Timers {
var countDownTimeStart: Int = 0
var currentTimer = 0
var timersIWant: [Int]
mutating func startTimer(with countDownTime: Int) {
var timeLeft = countDownTime
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in //Escaping closure captures mutating 'self' parameter
if timeLeft > 0 {
timeLeft -= 1
} else {
timer.invalidate()
self.currentTimer += 1
if self.currentTimer < self.timersIWant.count {
self.startTimer(with: self.timersIWant[self.currentTimer])
} else {
timer.invalidate()
}
}
})
}
}
}
I'm not sure if this has to do with my recursvie function (maybe this is bad form?) and I'm guessing the escaping closure is the func startTimer and the offending the 'self' parameter is the countDownTime parameter, but I'm not really sure what is happening or why it's wrong.
Escaping closure captures mutating 'self' parameter
The escaping closure is the Button's action parameter, and the mutating function is your startTimer function.
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
A simple solution is to change Times to be a class instead of a struct.
Also notice that timeLeft is defined in two places. I don't think this is what you want.
As Gil notes, this needs to be a class because you are treating it as a reference type. When you modify currentTimer, you don't expect that to create a completely new Timers instance, which is what happens with a value type (struct). You expect it to modify the existing Timers instance. That's a reference type (class). But to make this work, there's quite a bit more you need. You need to tie the Timers to the View, or the View won't update.
IMO, the best way to approach this is let Timers track the current timeLeft and have the view observe it. I've also added an isRunning published value so that the view can reconfigure itself based on that.
struct TimerView: View {
// Observe timers so that when it publishes changes, the view is re-rendered
#ObservedObject var timers = Timers(intervals: [10, 6, 8, 14])
var body: some View {
Button(action: { self.timers.startTimer()} ) {
Text("Hello, World! \(timers.timeLeft)")
.foregroundColor(.white)
.background(timers.isRunning ? Color.red : Color.blue) // Style based on isRunning
.font(.largeTitle)
}
.disabled(timers.isRunning) // Auto-disable while running
}
}
// Timers is observable
class Timers: ObservableObject {
// And it publishes timeLeft and isRunning; when these change, update the observer
#Published var timeLeft: Int = 0
#Published var isRunning: Bool = false
// This is `let` to get rid of any confusion around what to do if it were changed.
let intervals: [Int]
// And a bit of bookkeeping so we can invalidate the timer when needed
private var timer: Timer?
init(intervals: [Int]) {
// Initialize timeLeft so that it shows the upcoming time before starting
self.timeLeft = intervals.first ?? 0
self.intervals = intervals
}
func startTimer() {
// Invalidate the old timer and stop running, in case we return early
timer?.invalidate()
isRunning = false
// Turn intervals into a slice to make popFirst() easy
// This value is local to this function, and is captured by the timer callback
var timerLengths = intervals[...]
guard let firstInterval = timerLengths.popFirst() else { return }
// This might feel redundant with init, but remember we may have been restarted
timeLeft = firstInterval
isRunning = true
// Keep track of the timer to invalidate it elsewhere.
// Make self weak so that the Timers can be discarded and it'll clean itself up the next
// time it fires.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}
// Decrement the timer, or pull the nextInterval from the slice, or stop
if self.timeLeft > 0 {
self.timeLeft -= 1
} else if let nextInterval = timerLengths.popFirst() {
self.timeLeft = nextInterval
} else {
timer.invalidate()
self.isRunning = false
}
}
}
}