Stopping timer at defined amount of time in Swift
Start by decoupling your expectations.
A "clock" is a container for the period of time from which it was started to now. Additionally, it could be "restarted", so it may need to know how long each previous run cycle was, this would then be added into the overall duration of the "clock"
The Timer
is simply a way to run some code on periodical bases. Because a Timer
only guarantees "at least" period, it should avoid been used for simple counter addition, as it can cause drift in your calculations (for a simple clock, it's probably not a big deal, but if you need any kind of precision, it's best to avoid it)
SimpleClock
import Foundation
public class SimpleClock {
internal var startedAt: Date? = nil
internal var totalRunningTime: TimeInterval = 0 // Used for pause/resume
var isRunning: Bool = false {
didSet {
if isRunning {
startedAt = Date()
} else {
totalRunningTime += currentCycleDuration
self.startedAt = nil
}
}
}
// This is the amount of time that this cycle has been running,
// that is, the amount of time since the clock was started to now.
// It does not include other cycles
internal var currentCycleDuration: TimeInterval {
guard let startedAt = startedAt else {
return 0
}
return Date().timeIntervalSince(startedAt)
}
func reset() {
isRunning = false
totalRunningTime = 0
}
// This is the "total" amount of time the clock has been allowed
// to run for, excluding periods when the clock was paused
var duration: TimeInterval {
return totalRunningTime + currentCycleDuration
}
}
Okay, this is pretty basic concept. It's just a container for recording when a "cycle" starts and stops and managing the "overall" duration (start/pause/resume cycles)
That's all fine and good, but what we really want is some way to determine if the period has "timeout" or not.
AlarmClock
import Foundation
class AlarmClock: SimpleClock {
var timeout: TimeInterval = 0
var hasExpired: Bool {
return duration >= timeout
}
var timeRemaining: TimeInterval {
return max(timeout - duration, 0)
}
}
All this does is add a concept of a "timeout" period and provides some additional functionality that allows use to easily determine if the clock has expired and the amount of time remaining
Example
Okay, that's all nice a good, but how does this work (and help us)
Okay, this is a really simple example. It has a label and two buttons. One button starts/pauses the clock and the other resets it.
The label displays both the running time and the remaining time of the alarm clock. If he clock expires, it will automatically be reset.
The class contains a Timer
which periodically "ticks" and allows the code to inspect that current state of the alarm clock.
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var cycleButton: UIButton!
@IBOutlet weak var resetButton: UIButton!
let alarmClock: AlarmClock = {
let clock = AlarmClock()
clock.timeout = 10.0
return clock
}()
var timer: Timer? = nil
var durationFormatter: DateComponentsFormatter {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .abbreviated
return formatter
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func cycleClock(_ sender: Any) {
alarmClock.isRunning = !alarmClock.isRunning
if alarmClock.isRunning {
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
} else {
timer?.invalidate()
timer = nil
}
updateDurationLabel()
updateCycleButtonState()
}
@IBAction func restartClock(_ sender: Any) {
timer?.invalidate()
timer = nil
alarmClock.reset()
updateDurationLabel()
updateCycleButtonState()
}
func updateCycleButtonState() {
if alarmClock.isRunning {
cycleButton.setTitle("Pause", for: [])
} else {
cycleButton.setTitle("Start", for: [])
}
}
func updateDurationLabel() {
durationLabel.text = "\(durationFormatter.string(from: alarmClock.duration)!)/\(durationFormatter.string(from: alarmClock.timeRemaining)!)"
}
@objc func tick() {
print("click")
updateDurationLabel()
if alarmClock.hasExpired {
restartClock(self)
}
}
}
Now, you could also add some kind of "internal" thread to periodically check the state of the clock and call a delegate which could then bee used to update the UI, but the intention here is the decoupling of the concerns, and this means you're not adding yet another thread to the system unnecessarily (not saying you couldn't do, but it's just one more level of complexity I didn't want to add ;))
Swift question, how to stop my timer at 00:00 not negative number
Update your condition ... Currently when second goes below 0 .. then timer is invalidating ...
if seconds < 10 && seconds >= 0 {
secondsString = "0\(seconds)"
if seconds == 0 {
timer.invalidate()
}
}
Using Swift 3 Stopping a scheduledTimer, Timer continue firing even if timer is nil
Try to make the following changes to your code:
First, you have to change the way you declare timerTest
var timerTest : Timer?
then in startTimer
before instantiating check if timerTest
is nil
func startTimer () {
guard timerTest == nil else { return }
timerTest = Timer.scheduledTimer(
timeInterval: TimeInterval(0.3),
target : self,
selector : #selector(ViewController.timerActionTest),
userInfo : nil,
repeats : true)
}
Finally in your stopTimerTest
you invalidate timerTest
if it isn't nil
func stopTimerTest() {
timerTest?.invalidate()
timerTest = nil
}
How to stop timer in Text view?
Here is a demo of possible approach - as .timer
run from now for ever (by design), the idea is to replace it with regular text once specified period is over.
Tested with Xcode 12b3 / iOS 14.
struct DemoView: View {
@State private var run = false
var body: some View {
VStack {
if run {
Text(nextRollTime(in: 10), style: .timer)
} else {
Text("0:00")
}
}
.font(Font.system(.title, design: .monospaced))
.onAppear {
self.run = true
}
}
func nextRollTime(in seconds: Int) -> Date {
let date = Calendar.current.date(byAdding: .second, value: seconds, to: Date())
DispatchQueue.main.asyncAfter(deadline: .now() + Double(seconds)) {
self.run = false
}
return date ?? Date()
}
}
Swift - Timer doesn't stop
After research I found a solution,
The only thing that worked for me is to create functions in AppDelegate file and call them when needed,
Here is the code, the timerSwitch function:
func timerSwitch()
{
if (timerStatus) {
checkStateTimer = Timer.scheduledTimer(
timeInterval: 60,
target:self,
selector: #selector(self.yourFunction),
userInfo: nil, repeats: true)
} else {
checkStateTimer?.invalidate()
}
}
func stopTimer()
{
timerStatus = false
timerSwitch()
}
func startTimer()
{
timerStatus = true
timerSwitch()
}
While 'yourFunction' is what you want to execute when the timer starts,
In my case is sending heartbeat.
Then I called the timerSwitch is the following functions in AppDelegate:
func applicationWillResignActive(_ application: UIApplication) {
stopTimer()
}
func applicationDidEnterBackground(_ application: UIApplication) {
stopTimer()
}
func applicationDidBecomeActive(_ application: UIApplication) {
startTimer()
}
swift: stop countdown timer at zero
Remember, your timer doesn't count down to zero - you implement that in your code. The timer just fires every second.
In your updateTime function, you need to invalidate the timer, and call your sound function, when the timer runs out
How to capture the elapsed time when a timer is paused
Get the time when the user pauses the app as a new date(), and also the time when the user resumes the app. Get the difference between those two dates/times and subtract that from the total time to get the correct total time for the practice session.
For example, you could have a variable that keeps track of total pause time:
var totalPauseTime: TimeInterval = 0
var pauseStartTime: Date?
It gets recalculated every time a user taps the pause button to resume (after previously having tapped it to pause):
@IBAction func pauseButtonTapped(_ sender: DesignableButton)
{
if resumeTapped == false
{
pauseStartTime = Date()
timer.invalidate()
resumeTapped = true
sender.setImage(UIImage(named: "Resume"), for: .normal)
} else {
if let pauseStartTime = pauseStartTime {
totalPauseTime += Date().timeIntervalSinceReferenceDate - pauseStartTime.timeIntervalSinceReferenceDate
}
runTimer()
resumeTapped = false
sender.setImage(UIImage(named: "Pause"), for: .normal)
}
}
Haven't actually tested the above code but it should give an idea. Then you subtract totalPauseTime at the point in your code where you calculate the total session time.
Swift | The timer wont stop on invalidate call, rather speeds up?
As far as I can recognize, you have 2 mistakes.
First one is mentioned by the other answer. It is a true suggestion that you shouldn't always add a new UIButton and should use just hide/unhide property for each button.
Second mistake is on how you add a target. You are using .allTouchEvents, however you might intent to use .touchUpInside as your control state.
Kindly see the below corrected code for your reference:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
var startButton: UIButton!
var stopButton: UIButton!
var timer: Timer!
var counter: Double = 0.0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
startButton = UIButton(frame: CGRect(x: 0, y: UIScreen.main.bounds.height * 0.9 , width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 0.1))
startButton.setTitle("Start Timer", for: .normal)
startButton.setTitleColor(.white , for: .normal)
startButton.backgroundColor = .red
startButton.addTarget(self, action: #selector(playButton(_:)), for: .touchUpInside)
self.view.addSubview(startButton)
self.startButton.isHidden = false
stopButton = UIButton(frame: CGRect(x: 0, y: UIScreen.main.bounds.height * 0.9 , width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 0.1))
stopButton.setTitle("Stop Timer", for: .normal)
stopButton.setTitleColor(.white , for: .normal)
stopButton.backgroundColor = .red
stopButton.addTarget(self, action: #selector(pauseButton(_:)), for: .touchUpInside)
self.view.addSubview(stopButton)
self.stopButton.isHidden = true
}
@objc func playButton(_ sender : Any) {
timer = Timer.scheduledTimer(timeInterval: 1, target: self , selector: #selector(updateTimer), userInfo: nil, repeats: true)
startButton.isEnabled = false
stopButton.isEnabled = true
startButton.isHidden = true
stopButton.isHidden = false
}
@objc func pauseButton(_ sender: Any) {
timer.invalidate()
stopButton.isHidden = true
startButton.isHidden = false
startButton.isEnabled = true
stopButton.isEnabled = false
}
@objc func updateTimer(_ sender : Any)
{
counter += 0.1
titleLabel.text = String(format: "%.1f", counter)
}
}
Stopping AVAudioPlayer at a certain time in Swift
You could use a Timer
to check the currentTime
property of your AVAudioPlayer
object.
For Example
Start the timer when you start playing the audio. Adjusting the check interval 0.1
based on the level of accuracy required.
var timer:Timer!
timer = Timer.scheduledTimerWithTimeInterval(
0.1,
target: self,
selector: "checkTime",
userInfo: nil,
repeats: true
)
Then create the function that the timer runs every 0.1
second to check the audio players current time against your own value where END_TIME
could be updated or replaced by your songEnd
logic.
let END_TIME = 10.0
func checkTime() {
if self.audioPlayer.currentTime >= END_TIME {
self.audioPlayer.stop()
timer.invalidate()
}
}
Related Topics
Accessing Struct from One Class to Another
Can't Use 'Shape' as Type in Swiftui
Exc_Bad_Instruction Only in iPhone 5 Simulator
Setting Observer for Swift Objects/Properties
Value of Optional Type 'Nsurl' Not Unwrapped; Did You Mean to Use '!' or ''
Launch Safari (Not Default Browser) at Url in Swift
How to Reset a Subview in Swiftui
Swift How to Format a Large Number with Thousands Separators
How to Inject .Environmentobject() in Watchos6
Generic Class with Class-Bound Constraint Cannot Be Parametrized by Class-Bound Protocol
Swift: Download Image from Internet and Cache Them Doesn't Work Properly. Need Suggestions