2015-07-13 30 views
5

Mam app za pomocą NSTimer w centisecond (0,01 sekundy) interwały aktualizacji, aby wyświetlić uruchomiony stoper w formacie ciągu jako 00: 00.00 (mm: ss.SS). (Zasadniczo klonowanie wbudowanego stopera iOS w celu zintegrowania z matematyką czasu rzeczywistego w czasie rzeczywistym, prawdopodobnie wymagające milisekundowej dokładności w przyszłości)Formatowanie czasu rzeczywistego stopera na setne przy użyciu Swift

Używam (niewłaściwe użycie?) NSTimer'a do wymuszania aktualizacji UILabel. Jeśli użytkownik naciśnie przycisk Start, jest to kod NSTimer używany do uruchamiania powtórzenie funkcji:

displayOnlyTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("display"), userInfo: nil, repeats: true) 

i tutaj jest funkcja, która jest wykonywana przez wyżej 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 
    } 
} 

będzie co najmniej 2 łącza wyświetlacza działające w tym samym czasie. Czy ta metoda będzie zbyt nieefektywna, gdy wszystkie inne elementy będą w grze?

Wyświetlacz jest następnie aktualizowany bez użycia NSTimer, gdy użytkownik naciśnie przycisk stop/pause/reset. Nie znalazłem niczego, co bezpośrednio przełożyłoby się na Swift. Jestem dość pewny, że używam nieefektywnej metody, aby wymusić aktualizację tekstu UILabel szybko w UIView.

Więcej szczegółów: Pracuję nad mniej niechlujnym kodem dla formatu zegarka (mm: ss.SS). Zaktualizuję to jeszcze raz, kiedy to skończę.

AKTUALIZACJA: Dzięki Robowi i aplikacjom odpowiadającym na oba moje pytania (metoda formatowania i sposób aktualizacji ekranu). Łatwo było wymienić NSTimer (patrz wyżej) z CADisplayLink():

displayLink = CADisplayLink(target: self, selector: Selector("display")) 
     displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) 

a następnie zastąpić wszystkie wystąpienia w kodzie

displayOnlyTimer.invalidate() 

z

displayLink.paused = true 

(będzie wstrzymać aktualizację łącza wyświetlacza)

+0

Tylko dla własnego spokoju ducha, nie tworzyłeś nowego NSDateFormattera za każdym razem, prawda? W tej sytuacji nie tworzyłeś instancji NSDateFormatter wewnątrz metody 'display()', prawda? – justinpawela

+0

jtbandes ma rację i powinieneś zaakceptować tę odpowiedź (użyj linku wyświetlania, a nie licznika czasu, zdecydowanie nie wywołuj 'CACurrentMediaTime' wiele razy w funkcji ... mógł on przejść do następnej minuty między jednym a drugim połączeniem, w wyniku czego wynik jest całkowicie błędny, jeśli używasz formatera, stwórz go raz i użyj ponownie, itd.). Przy okazji, jeśli masz zamiar zrobić to tak, jak zrobiłeś powyżej, zamiast zastanawiać się, czy masz więcej niż 10, czy nie, po prostu użyj ciągu formatów takiego jak '% 05.2f' (który sformatuje go jako xx.xx, z wiodącym zerem). – Rob

+0

Na szczęście nie. Oszczędzam czas rozpoczęcia do zmiennej, gdy użytkownik naciśnie liczbę. Kiedy wstrzymują, po prostu odejmuję czas rozpoczęcia od CACurrentMediaTime() i dodaje go do sumy całkowitej. – mothy

Odpowiedz

6

Aby zaktualizować aktualizacje interfejsu użytkownika, należy użyć a CADisplayLink. Wszystko, co jest szybsze niż częstotliwość odświeżania wyświetlacza, jest marnowaniem mocy obliczeniowej, ponieważ fizycznie nie można go wyświetlić. Zapewnia także timestamp z poprzedniej klatki, dzięki czemu można spróbować przewidzieć, kiedy będzie następna klatka.

Wielokrotne obliczanie wartości CACurrentMediaTime() - timerStarted + elapsedTime. Polecam zrobić to tylko raz i zapisać go w zmiennej lokalnej.

Rozważ użycie NSDateComponentsFormatter. Spróbuj użyć jednej instancji programu formatującego zamiast tworzyć nową za każdym razem (co jest zazwyczaj najdroższą częścią). Ogólnie rzecz biorąc, mniej manipulacji ciągami, które możesz zrobić, tym lepiej.

Możesz sprawdzić CACurrentMediaTime na początku i na końcu metody wyświetlania, aby sprawdzić, ile czasu to zajmie. Idealnie powinno być wiele mniej niż 16,6 ms. Miej oko na wykorzystanie procesora (i ogólne zużycie energii) w nawigatorze debugowania Xcode.

+0

Dziękuję bardzo. Popraw mnie, jeśli się mylę, ale nie mogę użyć zmiennej z CACurrentMediaTime(), ponieważ wywołanie zmiennej nie zaktualizowałoby zmiennej do bieżącego czasu, to byłby moment, w którym zmienna została po raz pierwszy zapisana. Ale potem znowu, odpowiadanie na to może być bezużyteczne, zakładając, że program CADisplayLink działa inaczej niż mój obecny kod, gdy sprawdzę go jutro. – mothy

+1

Prawidłowo, ale chodzi o to, że w ramach jednej funkcji lepiej jest powiedzieć: "let currentTime = CACurrentMediaTime()" i ponownie użyć tego, niż wielokrotnie wywoływać. – jtbandes

+0

Tak, to nie wyglądało dobrze dla mnie, to ma sens teraz. Jest wywoływany co najmniej 2x, nawet jeśli pierwsze stwierdzenie jest prawdziwe. Ostatnie pytanie: Jeśli nazwałem to zmienną let, to czy nadal można jej używać, ponieważ wartość CACurrentMediaTime() jest inną wartością za każdym razem, gdy wywoływana jest funkcja? AKA czy zmienna niech przestanie istnieć po zakończeniu funkcji? – mothy

1

Rozwiązałem ten sam problem dzisiaj i znalazłem tę odpowiedź. Porady Rob i Jtbandes są bardzo pomocne i udało mi się stworzyć czyste i działające rozwiązanie z całego internetu. Dzięki wam. I dzięki mamie za pytanie.

Zdecydowałem się użyć CADisplayLink bo nie ma sensu wypalania zwrotnego synchronizatora częściej niż aktualizacji ekranu:

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) 
     //... 
    } 
    //... 
} 

mam śledzenie czasu, zwiększając zmienną ElapsedTime przez displayLink.duration każdego kleszcza :

var elapsedTime: CFTimeInterval! 

override init() { 
    //... 
    self.elapsedTime = 0.0 
    //... 
} 

func tick(sender: CADisplayLink) { 
    elapsedTime = elapsedTime + displayLink.duration 
    //... 
} 

Time-formatowanie odbywa się poprzez NSDateFormatter:

private let formatter = NSDateFormatter() 

override init() { 
//... 
     formatter.dateFormat = "mm:ss,SS" 
} 

func elapsedTimeAsString() -> String { 
     return formatter.stringFromDate(NSDate(timeIntervalSinceReferenceDate: elapsedTime)) 
} 

UI może być aktualizowany w zamknięciu zwrotnego, który nazywa Stoper na każdym tiku:

var callback: (() -> Void)? 

func tick(sender: CADisplayLink) { 
    elapsedTime = elapsedTime + displayLink.duration 

    // Calling the callback function if available 
    callback?() 
} 

I to jest wszystko, co trzeba zrobić w ViewController wykorzystać Stoper:

let stopwatch = Stopwatch() 
stopwatch.callback = self.tick 

func tick() { 
    elapsedTimeLabel.text = stopwatch.elapsedTimeAsString() 
} 

Oto Istota z pełnym kodem Stopwatch i instrukcją użycia: https://gist.github.com/Flar49/06b8c9894458a3ff1b14

Mam nadzieję, że to wyjaśnienie i istotę pomogą innym, którzy natkną się na ten wątek w przyszłości z tym samym problemem :)

+0

Czy masz odpowiednik wersji dla OS X? – Matt