Timer fires twice then doesn't reset - swift

I've got a countdown timer that counts down and executes the code twice but then it doesn't reset, instead it continues counting in negative numbers. Can someone tell me why?
var didCount = 4
func startDelayTimer() {
delayTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { _ in
self.startDelayCount()
})
}
func startDelayCount() {
delayTime -= 1
timeLbl.text = String(delayTime)
if delayTime <= 3 {
soundPLayer.play()
}
if delayTime == 0 {
delayTimer.invalidate()
doSomething()
}
}
func doSomething() {
doCount += 1
if doCount < didCount {
startDelayTimer()
}
else {
print("done")
}
}

The direct issue is that you reset the timer without remebering to reset delayTime.
But I think there's also an architectural issue, in that you have a murky mix of responsibilities (managing a timer, updating a label, and playing sounds). I'd suggest you extract the timer responsibilities elsewhere.
Perhaps something along these lines:
/// A timer which counts from `initialCount` down to 0, firing the didFire callback on every count
/// After each full countdown, it repeats itself until the repeatLimit is reached.
class RepeatingCountDownTimer {
typealias FiredCallback: () -> Void
typealias FinishedCallback: () -> Void
private var initialCount: Int
private var currentCount: Int // Renamed from old "delayTime"
private var repeatCount = 0 // Renamed from old "doCount"
private let repeatLimit: Int // Renamed from old "didCount"
private var timer: Timer?
private let didFire: FiredCallback
private let didFinish: FinishedCallback
init(
countDownFrom initialCount: Int,
repeatLimit: Int,
didFire: #escaping FiredCallback,
didFinish: #escaping FinishedCallback
) {
self.initialCount = initialCount
self.currentCount = initialCount
self.repeatLimit = repeatLimit
self.didFire = didFire
self.didFinish = didFinish
}
public func start() {
self.currentCount = self.initialCount
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] in
self?.fire()
})
}
private func fire() {
currentCount -= 1
self.didFire(currentCount)
if currentCount == 0 {
repeat()
}
}
private func repeat() {
repeatCount += 1
if repeatCount < repeatLimit {
self.timer?.invalidate()
start()
} else {
finished()
}
}
private func finished() {
self.timer?.invalidate()
self.timer = nil
self.didFinish()
}
}
That's just rough psuedo-code, which will certainly need tweaking. But the idea is to separate timer and state management from the other things you need to do. This should make it easier to debug/develop/test this code, replacing useless names like doSomething with more concretely named events.
The usage might look something like:
let countDownTimer = RepeatingCountDownTimer(
countDownFrom: 4,
repeatLimit: 4,
didFire: { count in
timeLbl.text = String(count)
soundPlayer.play()
},
didFinish: { print("done") }
)
countDownTimer.start()

Related

How to Test Timer with #Published?

I created a view which uses a ObservableObject which used an Timer to update seconds which are an #Published property.
class TimerService: ObservableObject {
#Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
...
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1 }
self.timer?.fire()
}
func stop() {...}
func reset() {...}
}
To test this logic I tried to subscribe to the seconds var. The problem is that the .sink method only trigger once and never again, even when it should.
class WorkTrackerTests: XCTestCase {
var timerService: TimerService!
override func setUpWithError() throws {
super.setUp()
timerService = TimerService()
}
override func tearDownWithError() throws {
super.tearDown()
timerService = nil
}
func test_start_timer() throws {
var countingArray: [Int] = []
timerService.start()
timerService.$seconds.sink(receiveValue: { value -> Void in
print(value) // 1 (called once with this value)
countingArray.append(value)
})
timerService.stop()
for index in 0...countingArray.count-1 {
if(index>0) {
XCTAssertTrue(countingArray[index] - 1 == countingArray[index-1])
}
}
}
}
Is there something I did wrong or is the SwiftUI #Published Wrapper not capable of being subscribed by something else than SwiftUI itself?
I'll start by repeating what I said already in comments. There is no need to test Apple's code. Don't test Timer. You know what it does. Test your code.
As for your actual example test harness, it is flawed from top to bottom. A sink without a store will indeed get only one value, if it gets any at all. But the issue runs even deeper, as you are acting like your code will magically stop and wait for the timer to finish. It won't. You are saying stop immediately after saying start, so the timer never even runs. Asynchronous input requires asynchronous testing. You would need an expectation and a waiter.
But it is very unclear why you are subscribing to the publisher at all. What are you trying to find out? The only question of interest, it seems, is whether you are incrementing your variable each time the timer fires. And you can test that without subscribing to a publisher — and, as I said, without a Timer.
So much for the repetition. Now let's demonstrate. Let's start with the code you've actually shown, the only code that has content:
class TimerService: ObservableObject {
#Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
self.timer?.fire()
}
}
Now look at all the commands you send to the Timer and encapsulate them in a Timer subclass:
class TimerMock : Timer {
var block : ((Timer) -> Void)?
convenience init(block: (#escaping (Timer) -> Void)) {
self.init()
self.block = block
}
override class func scheduledTimer(withTimeInterval interval: TimeInterval,
repeats: Bool,
block: #escaping (Timer) -> Void) -> Timer {
return TimerMock(block:block)
}
override func fire() {
self.block?(self)
}
}
Now make your TimerService a generic so that we can inject TimerMock when testing:
class TimerService<T:Timer>: ObservableObject {
#Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
self.timer = T.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
self.timer?.fire()
}
}
So now we can test your logic without bothering to run a timer:
import XCTest
#testable import TestingTimer // whatever the name of your module is
class TestingTimerTests: XCTestCase {
func testExample() throws {
let timerService = TimerService<TimerMock>()
timerService.start()
if let timer = timerService.timer {
timer.fire()
timer.fire()
timer.fire()
}
XCTAssertEqual(timerService.seconds,4)
}
}
None of your other code needs to change; you can go on using TimerService as before. I can think of other ways to do this, but they would all involve dependency injection where in "real life" you use a Timer but when testing you use a TimerMock.

Timer.scheduledTimer not firing

I am trying to calculate the time it takes a vehicle to go between two checkpoints. I have setup the following timer to achieve this.
func startTimer() {
if hasStarted == true && timerStarted == false {
print("Timing started!")
gameTimer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: (#selector(activeTiming)), userInfo: nil, repeats: true)
timerStarted = true
}
}
#objc func activeTiming() {
print("Active timing block")
if(hasFinished == false) {
gameTime = gameTime + 0.001
print("Add time succeeded")
} else {
gameTimer?.invalidate()
}
}
The expected output would be the following:
Timing started!
Active timing block
Add time succeeded
Add time succeeded ... etc
The actual output:
Timing started!
So it would appear that the startTimer is properly being called but the timer is not firing the activeTiming block of code. Any suggestions would be extremely helpful. Thank you for your help in advance.
Posting this code as it is what I'm using, but I’m no expert on Swift, so your mileage may vary!
class PerformanceTest {
var name: String = ""
var tolerance: Int64 = 0
var lastTime: Int64 = 0
var thisTime: Int64 = 0
var delta: Int64 = 0
var percent: Float = 0
func setTolerance(vName: String, vTolerance: Int64) {
name = vName
tolerance = vTolerance
}
func reset() {
delta = 0
percent = Float((Float(delta) / Float(tolerance))) * 100
//high = 0
}
func start() {
lastTime = Date().toMillis()
}
func finish() {
thisTime = Date().toMillis()
let vDelta = thisTime - lastTime
if(vDelta > delta) {
delta = vDelta
percent = Float((Float(delta) / Float(tolerance))) * 100
if(delta > tolerance) {
print("Performance Indicator: \(name) Above Tolerance" + String(format: "%3.0f", percent) + "%")
}
}
}
func display() -> String {
//high = delta
//print("\(vString) Tolerance: \(tolerance) Max: \(high)")
return String(format: "%3.0f", percent) + "% |"
}
}
extension Date {
func toMillis() -> Int64! {
return Int64(self.timeIntervalSince1970 * 1000)
}
Usage:
var performanceDefenseLoop = PerformanceTest()
performanceDefenseLoop.setTolerance(vName: "DefenseLoop", vTolerance: 150)
func timeToUpdateDefenses()
{
performanceDefenseLoop.start()
defesensesLoop()
performanceDefenseLoop.finish()
print("\(performanceDefenseLoop.Display())"
}
// To reset
performanceDefenseLoop.reset()
Be sure to start timers in the main thread!
I also faced a similar issue where the timer body was not getting called. I found out that the timer was getting scheduled from a background thread instead of the main thread. Wrapping it in DispatchQueue.main.async fixed it.
DispatchQueue.main.async {
self.pingTimer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true, block: { [weak self] timer in
//
Logger.shared.log(.anyCable, .debug, "timer run")
self?.sendPing(sender: self)
})
}
Your code works for me, but that isn't how you want to time an event because the Timer isn't that accurate, and you are wasting a lot of computing time (read battery).
Instead, I suggest the following approach:
When the event starts, record the start time:
let startTime = Date()
When the event ends, compute the elapsed time:
let elapsedTime = Date().timeIntervalSince(startTime)
elapsedTime will be in seconds (including fractional seconds).

I need a reliable StopWatch in Swift 3

I need a reliable StopWatch in Swift 3. I came up with this based on another example found on SO ...
import Foundation
class StopWatch {
public private(set) var seconds:Double = 0
public var milliseconds: Double { return seconds * 1000 }
private weak var timer:Timer?
private var startTime:Double = 0
public private(set) var started = false
public func start() {
if started { return }
started = true
startTime = Date().timeIntervalSinceReferenceDate
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) {
timer in
if !self.started { return }
self.seconds = Date().timeIntervalSinceReferenceDate - self.startTime
print("Seconds: \(self.seconds)")
}
print("Started")
}
public func stop() {
if !started { return }
timer?.invalidate()
started = false
print("Stopped")
}
}
But this doesn't seem to work reliably. Fetching seconds are often staying at 0 or in other cases when accessing seconds they don't start from 0. What is wrong with this code?
It's most likely a threading issue. If UI is involved you have to make sure that the UI related code runs on the main thread.
The variable started is redundant. Checking the timer for nil does the same.
This code adds a DispatchQueue.main block. Put your code to update the UI into this block
class StopWatch {
public private(set) var seconds:Double = 0
public var milliseconds: Double { return seconds * 1000 }
private weak var timer : Timer?
private var startTime : Double = 0
private var timerIsRunning : Bool { return timer != nil }
public func start() {
if timerIsRunning { return }
startTime = Date().timeIntervalSinceReferenceDate
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
self.seconds = Date().timeIntervalSinceReferenceDate - self.startTime
print("Seconds: \(self.seconds)")
DispatchQueue.main.async {
// do something on the main thread
}
}
print("Started")
}
public func stop() {
if !timerIsRunning { return }
timer!.invalidate()
timer = nil
print("Stopped")
}
}

How Can I Unit Test Swift Timer Controller?

I am working a project that will utilize Swift's Timer class. My TimerController class will control a Timer instance by starting, pausing, resuming, and resetting it.
TimerController consists of the following code:
internal final class TimerController {
// MARK: - Properties
private var timer = Timer()
private let timerIntervalInSeconds = TimeInterval(1)
internal private(set) var durationInSeconds: TimeInterval
// MARK: - Initialization
internal init(seconds: Double) {
durationInSeconds = TimeInterval(seconds)
}
// MARK: - Timer Control
// Starts and resumes the timer
internal func startTimer() {
timer = Timer.scheduledTimer(timeInterval: timerIntervalInSeconds, target: self, selector: #selector(handleTimerFire), userInfo: nil, repeats: true)
}
internal func pauseTimer() {
invalidateTimer()
}
internal func resetTimer() {
invalidateTimer()
durationInSeconds = 0
}
// MARK: - Helpers
#objc private func handleTimerFire() {
durationInSeconds += 1
}
private func invalidateTimer() {
timer.invalidate()
}
}
Currently, my TimerControllerTests contains the following code:
class TimerControllerTests: XCTestCase {
func test_TimerController_DurationInSeconds_IsSet() {
let expected: TimeInterval = 60
let controller = TimerController(seconds: 60)
XCTAssertEqual(controller.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
}
}
I am able to test that the timer's expected duration is set correctly when initializing an instance of TimerController. However, I don't know where to start testing the rest of TimerController.
I want to ensure that the class successfully handles startTimer(), pauseTimer(), and resetTimer(). I want my unit tests to run as quickly as possible, but I think that I need to actually start, pause, and stop the timer to test that the durationInSeconds property is updated after the appropriate methods are called.
Is it appropriate to actually create the timer in TimerController and call the methods in my unit tests to verify that durationInSeconds has been updated correctly?
I realize that it will slow my unit tests down, but I don't know of another way to appropriately test this class and it's intended actions.
Update
I have been doing some research, and I have found, what I think to be, a solution that seems to get the job done as far as my testing goes. However, I am unsure whether this implementation is sufficient.
I have reimplemented my TimerController as follows:
internal final class TimerController {
// MARK: - Properties
private var timer = Timer()
private let timerIntervalInSeconds = TimeInterval(1)
internal private(set) var durationInSeconds: TimeInterval
internal var isTimerValid: Bool {
return timer.isValid
}
// MARK: - Initialization
internal init(seconds: Double) {
durationInSeconds = TimeInterval(seconds)
}
// MARK: - Timer Control
internal func startTimer(fireCompletion: (() -> Void)?) {
timer = Timer.scheduledTimer(withTimeInterval: timerIntervalInSeconds, repeats: true, block: { [unowned self] _ in
self.durationInSeconds -= 1
fireCompletion?()
})
}
internal func pauseTimer() {
invalidateTimer()
}
internal func resetTimer() {
invalidateTimer()
durationInSeconds = 0
}
// MARK: - Helpers
private func invalidateTimer() {
timer.invalidate()
}
}
Also, my test file has passing tests:
class TimerControllerTests: XCTestCase {
// MARK: - Properties
var timerController: TimerController!
// MARK: - Setup
override func setUp() {
timerController = TimerController(seconds: 1)
}
// MARK: - Teardown
override func tearDown() {
timerController.resetTimer()
super.tearDown()
}
// MARK: - Time
func test_TimerController_DurationInSeconds_IsSet() {
let expected: TimeInterval = 60
let timerController = TimerController(seconds: 60)
XCTAssertEqual(timerController.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
}
func test_TimerController_DurationInSeconds_IsZeroAfterTimerIsFinished() {
let numberOfSeconds: TimeInterval = 1
let durationExpectation = expectation(description: "durationExpectation")
timerController = TimerController(seconds: numberOfSeconds)
timerController.startTimer(fireCompletion: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + numberOfSeconds, execute: {
durationExpectation.fulfill()
XCTAssertEqual(0, self.timerController.durationInSeconds, "'durationInSeconds' is not set to correct value.")
})
waitForExpectations(timeout: numberOfSeconds + 1, handler: nil)
}
// MARK: - Timer State
func test_TimerController_TimerIsValidAfterTimerStarts() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
timerValidityExpectation.fulfill()
XCTAssertTrue(self.timerController.isTimerValid, "Timer is invalid.")
}
waitForExpectations(timeout: 5, handler: nil)
}
func test_TimerController_TimerIsInvalidAfterTimerIsPaused() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
self.timerController.pauseTimer()
timerValidityExpectation.fulfill()
XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
}
waitForExpectations(timeout: 5, handler: nil)
}
func test_TimerController_TimerIsInvalidAfterTimerIsReset() {
let timerValidityExpectation = expectation(description: "timerValidity")
timerController.startTimer {
self.timerController.resetTimer()
timerValidityExpectation.fulfill()
XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
}
waitForExpectations(timeout: 5, handler: nil)
}
}
The only thing that I can think of to make the tests faster is for me to mock the class and change let timerIntervalInSeconds = TimeInterval(1) to private let timerIntervalInSeconds = TimeInterval(0.1).
Is it overkill to mock the class so that I can use a smaller time interval for testing?
Rather than use a real timer (which would be slow), we can verify calls to a test double.
The challenge is that the code calls a factory method, Timer.scheduledTimer(…). This locks down a dependency. Testing would be easier if the test could provide a mock timer instead.
Usually, a good way to inject a factory is by supplying a closure. We can do this in the initializer, and provide a default value. Then the closure, by default, will make the actual call to the factory method.
In this case, it's a little complicated because the call to Timer.scheduledTimer(…) itself takes a closure:
internal init(seconds: Double,
makeRepeatingTimer: #escaping (TimeInterval, #escaping (TimerProtocol) -> Void) -> TimerProtocol = {
return Timer.scheduledTimer(withTimeInterval: $0, repeats: true, block: $1)
}) {
durationInSeconds = TimeInterval(seconds)
self.makeRepeatingTimer = makeRepeatingTimer
}
Note that I removed all references to Timer except inside the block. Everywhere else uses a newly-defined TimerProtocol.
self.makeRepeatingTimer is a closure property. Call it from startTimer(…).
Now test code can supply a different closure:
class TimerControllerTests: XCTestCase {
var makeRepeatingTimerCallCount = 0
var lastMockTimer: MockTimer?
func testSomething() {
let sut = TimerController(seconds: 12, makeRepeatingTimer: { [unowned self] interval, closure in
self.makeRepeatingTimerCallCount += 1
self.lastMockTimer = MockTimer(interval: interval, closure: closure)
return self.lastMockTimer!
})
// call something on sut
// verify against makeRepeatingTimerCallCount and lastMockTimer
}
}

Swift : Pause and Resume NSTimer

I know this has been posted before, but I'm new to swift and i would like to find help as per my situation.
So i need to run a timer, and when the stopMusic() is called(somewhere in my app), i need to pause the timer, then when the playMusic() is called, i need to resume the timer.
Here is what i have:
override func viewDidLoad() {
runFirstDropTimer = NSTimer.scheduledTimerWithTimeInterval(38.2, target: self, selector: "runAnimation", userInfo: nil, repeats: false)
}
func stopMusic() {
//Pause Timer
}
func playMusic() {
//Resume Timer
}
Thank you for any help you can provide!!!
Swift 5.
Resumable timer:
class ResumableTimer: NSObject {
private var timer: Timer? = Timer()
private var callback: () -> Void
private var startTime: TimeInterval?
private var elapsedTime: TimeInterval?
// MARK: Init
init(interval: Double, callback: #escaping () -> Void) {
self.callback = callback
self.interval = interval
}
// MARK: API
var isRepeatable: Bool = false
var interval: Double = 0.0
func isPaused() -> Bool {
guard let timer = timer else { return false }
return !timer.isValid
}
func start() {
runTimer(interval: interval)
}
func pause() {
elapsedTime = Date.timeIntervalSinceReferenceDate - (startTime ?? 0.0)
timer?.invalidate()
}
func resume() {
interval -= elapsedTime ?? 0.0
runTimer(interval: interval)
}
func invalidate() {
timer?.invalidate()
}
func reset() {
startTime = Date.timeIntervalSinceReferenceDate
runTimer(interval: interval)
}
// MARK: Private
private func runTimer(interval: Double) {
startTime = Date.timeIntervalSinceReferenceDate
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: isRepeatable) { [weak self] _ in
self?.callback()
}
}
}
Usage:
private var playTimer: ResumableTimer?
playTimer = ResumableTimer(interval: 5.0) { [weak self] in
self?.doSomethingOnTimerFire()
}
playTimer?.start()
Short answer: You can't. Timers can't be paused.
Instead you need to record the current time when you start the timer:
let startTime = NSDate.timeIntervalSinceReferenceDate()
let interval = 38.2
//Start your timer
runFirstDropTimer = NSTimer.scheduledTimerWithTimeInterval(interval,
target: self,
selector: "runAnimation",
userInfo: nil,
repeats: false)
Then, when you want to pause your timer, invalidate it and figure out how much time is left:
runFirstDropTimer.invalidate()
let elapsed = NSDate.timeIntervalSinceReferenceDate() - startTime
let remaining = interval - elapsed
And finally when you want to resume your timer create a new timer with the remaining value.