NSTimer Too Slow

2019-01-29 13:50发布

I have seen multiple questions similar to this; however, none of them resolved my problem. My timer is simply going too slowly. I am only using an interval of 0.01 seconds. Here is my code:

@IBOutlet var timerLabel: UILabel!

var miliseconds = 0
var seconds = 0

func updateLabel() {
    if miliseconds == 0 {
        timerLabel.text = "\(seconds).00"
    } else if miliseconds < 10 {
        timerLabel.text = "\(seconds).0\(miliseconds)"
    } else {
        timerLabel.text = "\(seconds).\(miliseconds)"
    }
}

var timer = NSTimer()

func updateTime() {

    miliseconds++
    if miliseconds == 100 {
        miliseconds = 0
        seconds++
    }
    updateLabel()
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    if timerState == 1 {
        timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "updateTime", userInfo: nil, repeats: true)
        NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
        timerLabel.textColor = UIColor.blackColor()
        timerState = 2
    }
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    if timerState == 0 {
        miliseconds = 0
        seconds = 0
        updateLabel()
        timerLabel.textColor = UIColor.greenColor()
        timerState = 1
    } else if timerState == 2 {
        timerState = 0
        timer.invalidate()
    }
}

var timerState = 0
//timerState of 0 = Has not started
//timerState of 1 = About to start
//timerState of 2 = Timing

I have also tried using delays:

func delay(delay:Double, closure:()->()) {

    dispatch_after(
    dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)

}

I called updateTime in viewDidLoad, and at the end of updateTime, I added:

delay(0.01) { () -> () in
    self.updateTime()
}

However, it still went at the same speed as before.

How can I fix this issue? If I missed a question while researching, please let me know. Thanks!

2条回答
我想做一个坏孩纸
2楼-- · 2019-01-29 14:40

From my comment above ... (with encouragement)

NSTimer - from apple developer docs:

"A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. "

查看更多
爷、活的狠高调
3楼-- · 2019-01-29 14:50

There is a whole mess of problems here. Let's clean this up.

class ViewController: UIViewController {

    @IBOutlet var timerLabel: UILabel!

First of all, don't use a timer to update the label. Use a CADisplayLink. A display link synchronizes with the screen refresh interval, which is 1/60 of a second on most iOS devices (not 1/100), so you don't do extra work:

    private var displayLink: CADisplayLink?

Next, don't try to track the elapsed time by incrementing a counter when the timer (or link) fires, because the timer or link is not guaranteed to fire as often as you requested. Instead, store the time at which the timer was started, and the time at which it was stopped (if it was stopped):

    private var startTime: CFAbsoluteTime = 0
    private var endTime: CFAbsoluteTime = 0 {
        didSet {
            updateLabel()
        }
    }

Track the state using an enum instead of mysterious hard-coded numbers. And since the label color depends only the state, add a property to the state that gives the label color for that state:

    private enum State {
        case Stopped
        case Pending
        case Running

        var labelColor: UIColor {
            switch self {
            case .Pending: return UIColor.greenColor()
            case .Stopped, .Running: return UIColor.blackColor()
            }
        }
    }

The elapsed time depends on the state, so add a method to compute it:

    private var elapsedTime: NSTimeInterval {
        switch state {
        case .Stopped: return endTime - startTime
        case .Running: return CFAbsoluteTimeGetCurrent() - startTime
        case .Pending: return 0
        }
    }

Use a format string to convert the elapsed time to a string when updating the label:

    private func updateLabel() {
        timerLabel.text = String(format: "%.02f", elapsedTime)
    }

Changing the timer state can change both the label color and the elapsed time, so update the label color and the label text when the state changes:

    private var state = State.Stopped {
        didSet {
            timerLabel.textColor = state.labelColor
            updateLabel()
        }
    }

When a touch begins, create the display link if needed, then update the state. The state's didSet will handle updating the label as necessary:

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        createDisplayLinkIfNeeded()

        switch state {
        case .Stopped:
            state = .Pending
        case .Pending:
            break
        case .Running:
            state = .Stopped
            endTime = CFAbsoluteTimeGetCurrent()
            displayLink?.paused = true
        }
    }

When a touch ends, start the timer if necessary:

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if state == .Pending {
            startTime = CFAbsoluteTimeGetCurrent()
            displayLink?.paused = false
            state = .Running
        }
    }

Here's how you create the display link:

    private func createDisplayLinkIfNeeded() {
        guard self.displayLink == nil else { return }
        let displayLink = CADisplayLink(target: self, selector: "displayLinkDidFire:")
        displayLink.paused = true
        displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
        self.displayLink = displayLink
    }

And here's the method the display link will call:

    func displayLinkDidFire(_: CADisplayLink) {
        updateLabel()
    }

} // end of ViewController
查看更多
登录 后发表回答