0

I have a countdown Timer that shows seconds and milliseconds. The user can start/stop recording multiple times until the timer hits zero. The user can also delete a previous recording at which point I have to re-add that deleted time back into the initial 20 secs. There are 2 issues.

The first issue is when the timer is stopped, the remaining time that shows on the timer label doesn't match the time culmination of the recordings. From my understanding this might be a RunLoop issue and I don't think there is anything that I can do about the inaccuracies.

let initialTime = 20.0

var cumulativeTimeForAllAssests = 0.0

for asset in arrOfAssets {
    let assetDuration = CMTimeGetSeconds(asset.duration)       
    print("assetDuration: ", assetDuration)

    cumulativeTimeForAllAssests += assetDuration
}

print("\ncumulativeTimeForAllAssests: ", cumulativeTimeForAllAssests)

After starting/stopping 5 times, the remaining time on the timer label says 16.5 but the culmination of the assets time is 4.196666.... The timer label should say 15.8, it's 0.7 milli off. The more I start/stop the recording, the more inaccurate/further off the culmination time - the initial time and the timer label time is.

assetDuration:  0.7666666666666667
assetDuration:  0.9666666666666667
assetDuration:  0.7983333333333333
assetDuration:  0.7333333333333333
assetDuration:  0.9316666666666666

cumulativeTimeForAllAssests:  4.196666666666667

The second issue is because I'm using seconds and milliseconds in my timerLabel, when I add re-add the subtracted time back in via deleteAssetAndUpdateTimer(...), I use the parts of modf() to update the seconds and milliseconds. I couldn't think of another way to update the timer. I know there has to be a more accurate way to do it.

Timer code:

weak var timer: Timer?
var seconds = 20
var milliseconds = 0
let initialTime = 20.0

func startTimer() {

    invalidateTimer()

    if seconds == Int(initalTime) && milliseconds == 0 {
        timerIsRunning()
    }

    timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
        self?.timerIsRunning()
    })
}

func timerIsRunning() {

    updateTimerLabel()

    if milliseconds == 0 {
        seconds -= 1
    }

    milliseconds -= 1

    if milliseconds < 0 {    
        milliseconds = 9
    }

    if seconds == 0 && milliseconds == 0 {

        invalidateTimer()
        updateTimerLabel()
    }
}

func invalidateTimer() {
    timer?.invalidate()
    timer = nil
}

func updateTimerLabel() {

    let milisecStr = "\(milliseconds)"
    let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"

    timerLabel.text = "\(secondsStr).\(milisecStr)"
}

Delete asset and update timer code:

// the timer is stopped when this is called
func deleteAssetAndUpdateTimer(_ assetToDelete: AVURLAsset) {

    var cumulativeTimeForAllAssests = 0.0
    
    for asset in arrOfAssets {

        let assetDuration = CMTimeGetSeconds(asset.duration)       

        cumulativeTimeForAllAssests += assetDuration
    }

    let timeFromAssetToDelete = CMTimeGetSeconds(assetToDelete.duration)
    
    let remainingTime = self.initialTime - cumulativeTimeForAllAssests
    
    let updatedTime = remainingTime + timeFromAssetToDelete

    let mod = modf(updatedTime)

    self.seconds = Int(mod.0)
        
    self.milliseconds = Int(mod.1 * 10)

    updateTimerLabel()

    // remove assetToDelete from array
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • You should never use a timer to measure elapsed time – Leo Dabus Mar 14 '22 at 01:45
  • @LeoDabus I was just looking at your answer https://stackoverflow.com/a/30653322/4833705 and Duncan's answer https://stackoverflow.com/a/30983444/4833705. I see that you count down from the current time using `Date()`. Those questions aren't for multiple stop and go. I'm trying to figure how to do that and still re-add the subtracted time back in – Lance Samaria Mar 14 '22 at 02:46
  • Just use multiple dates. There is no guarantee that the timer selector method will be executed at the desired interval. You should only use the timer method to update the UI. – Leo Dabus Mar 14 '22 at 03:21
  • @LeoDabus thanks, I'll try to figure it out. This is a new one. I use timers in the traditional manner, I've never used them in the way that you are suggesting. I'll see what I can do. Thanks and enjoy your day! – Lance Samaria Mar 14 '22 at 05:56

1 Answers1

0

The big issue here was I was using a Timer to countdown which was incorrect. Following @LeoDabus' comments, I instead used CACurrentMediaTime():

let timerLabel = UILabel()

let maxRecordingTime = 30.0
lazy var elapsedTime = maxRecordingTime
var startTime: CFTimeInterval?
var endTime: CFTimeInterval?
weak var timer: Timer?

override func viewDidLoad() {
    super.viewDidLoad()
    
    updateTimerLabel(with: Int(maxRecordingTime))
}

@IBAction func recordButtonPressed(_ sender: UIButton) {

    if startTime == nil {
        startTimer()
    } else {
        stopTimer(updateElapsed: true)
    }
}

func startTimer() {
    
    if elapsedTime == 0 { return }
    
    stopTimer()

    startTime = CACurrentMediaTime()
    endTime = startTime! + elapsedTime
    
    print("startTime: \(startTime!) | endTime: \(endTime!)")

    timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
        self?.timerIsRunning()
    }
}

func timerIsRunning() {
    
    guard let startTime = startTime, let endTime = endTime else { return }
    
    let currentTime = CACurrentMediaTime()

    let remainingTime = currentTime - startTime

    print("%2d %.3lf", elapsedTime, remainingTime)

    if currentTime >= endTime {
        
        print("stopped at - currentTime: \(currentTime) | endTime: \(endTime)")
        
        stopTimer(updateElapsed: true, currentTime: currentTime)
        return
    }
    
    let countDownTime: Double = elapsedTime - remainingTime
    
    let seconds = Int(countDownTime)
    
    updateTimerLabel(with: seconds)
}

func updateTimerLabel(with seconds: Int) {
    
    let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
    
    timerLabel.text = secondsStr
}

func stopTimer(updateElapsed: Bool = false, currentTime: Double? = nil) {

    timer?.invalidate()
    timer = nil
    
    if updateElapsed {
        updateElapsedTime(using: currentTime)
    }
    
    startTime = nil
    endTime = nil
}

func updateElapsedTime(using currentTime: Double? = nil) {
    
    guard let startTime = startTime else { return }
    
    var timeNow = CACurrentMediaTime()
    
    if let currentTime = currentTime {
        timeNow = currentTime
    }
    
    var updatedTime = elapsedTime - (timeNow - startTime)
    
    if updatedTime < 0 {
        updatedTime = 0
    }
    
    elapsedTime = updatedTime
}

func resetElapsedTime() { // This is for a resetButton not shown here
    
    elapsedTime = maxRecordingTime
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256