Отформатируйте секундомер в реальном времени до сотого с помощью Swift
У меня есть приложение, использующее NSTimer с интервалом обновления 0,01 секунды для отображения текущего секундомера в формате строки как 00:00,00 (мм: сс.СС). (По сути, клонирование встроенного секундомера iOS для интеграции в математические задачи спортивного времени в реальном времени, возможно, в будущем потребуется точность в миллисекунды)
Я использую (неправильно) NSTimer для принудительного обновления UILabel. Если пользователь нажимает кнопку "Пуск", это код NSTimer, используемый для начала повторения функции:
displayOnlyTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("display"), userInfo: nil, repeats: true)
А вот функция, которая выполняется вышеупомянутым NSTimer:
func display() {
let currentTime = CACurrentMediaTime() - timerStarted + elapsedTime
if currentTime < 60 {
timeDisplay.text = String(format: "%.2f", currentTime)
}else if currentTime < 3600 {
var minutes = String(format: "%00d", Int(currentTime/60))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = minutes + ":" + seconds
}else {
var hours = String(format: "%00d", Int(currentTime/3600))
var minutes = String(format: "%02d", (Int(currentTime/60)-(Int(currentTime/3600)*60)))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = hours + ":" + minutes + ":" + seconds
}
}
Там будет по крайней мере 2 ссылки показа одновременно. Будет ли этот метод слишком неэффективным, когда все остальные элементы будут в игре?
Затем дисплей обновляется без использования NSTimer, когда пользователь нажимает "Стоп / Пауза / Сброс". Я не нашел ничего, что прямо переведено на Swift. Я почти уверен, что использую неэффективный метод для быстрого обновления текста UILabel в UIView.
Больше деталей: я работаю над менее запутанным кодом для текущего формата таймера (mm: ss.SS). Я обновлю это еще раз, когда я закончу это.
ОБНОВЛЕНИЕ: Спасибо Робу и jtbandes за ответы на оба моих вопроса (метод форматирования и метод обновления дисплея). Было легко заменить NSTimer (см. Выше) на CADisplayLink():
displayLink = CADisplayLink(target: self, selector: Selector("display"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
А затем заменить все экземпляры в коде
displayOnlyTimer.invalidate()
с
displayLink.paused = true
(это приостановит обновление отображаемой ссылки)
2 ответа
Для быстрого обновления пользовательского интерфейса вы должны использовать CADisplayLink. Все, что быстрее, чем частота обновления дисплея, является пустой тратой вычислительной мощности, поскольку физически не может быть отображено. Это также обеспечивает timestamp
предыдущего кадра, так что вы можете попытаться предсказать, когда будет следующий кадр.
Вы рассчитываете CACurrentMediaTime() - timerStarted + elapsedTime
многократно. Я бы порекомендовал сделать это только один раз и сохранить его в локальной переменной.
Рассмотрите возможность использования NSDateComponentsFormatter. Попробуйте повторно использовать один экземпляр средства форматирования, а не каждый раз создавать новый (что обычно является наиболее дорогой частью). В целом, чем меньше манипуляций со строками вы можете сделать, тем лучше.
Вы можете проверить CACurrentMediaTime в начале и в конце вашего метода отображения, чтобы увидеть, сколько времени это займет. В идеале оно должно быть намного меньше 16,6 мс. Следите за использованием ЦП (и общим энергопотреблением) в навигаторе отладки Xcode.
Сегодня я решал ту же проблему и нашел этот ответ. Советы Роба и jtbandes очень помогли, и я смог собрать чистое и работающее решение со всего Интернета. Спасибо, ребята. И спасибо Моти за вопрос.
Я решил использовать CADisplayLink, потому что нет смысла использовать обратный вызов таймера чаще, чем обновления экрана:
class Stopwatch: NSObject {
private var displayLink: CADisplayLink!
//...
override init() {
super.init()
self.displayLink = CADisplayLink(target: self, selector: "tick:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
//...
}
//...
}
Я отслеживаю время, увеличивая переменную elapsedTime на displayLink.duration каждый тик:
var elapsedTime: CFTimeInterval!
override init() {
//...
self.elapsedTime = 0.0
//...
}
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
//...
}
Форматирование времени выполняется через NSDateFormatter:
private let formatter = NSDateFormatter()
override init() {
//...
formatter.dateFormat = "mm:ss,SS"
}
func elapsedTimeAsString() -> String {
return formatter.stringFromDate(NSDate(timeIntervalSinceReferenceDate: elapsedTime))
}
Пользовательский интерфейс может быть обновлен при закрытии обратного вызова, который секундомер вызывает на каждом тике:
var callback: (() -> Void)?
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
// Calling the callback function if available
callback?()
}
И это все, что вам нужно сделать в ViewController, чтобы использовать секундомер:
let stopwatch = Stopwatch()
stopwatch.callback = self.tick
func tick() {
elapsedTimeLabel.text = stopwatch.elapsedTimeAsString()
}
Вот суть с полным кодом секундомера и руководством по использованию: https://gist.github.com/Flar49/06b8c9894458a3ff1b14
Я надеюсь, что это объяснение и суть помогут другим, кто наткнется на эту тему в будущем с той же проблемой:)