2015-03-10 34 views
5

Mam tableView wykonane z kilku niestandardowych NSTableCellView. Niektóre widoki muszą pokazywać licznik czasu (ile czasu minęło) wraz z NSProgressIndicator.okresowo aktualizuj pole tekstowe wewnątrz NSTableCellView za pomocą timera

Stworzyłem timer (z dużym wyświetlaczem centralnym) i co 100 ms aktualizuję textView z setNeedsDisplay (lub .needsDisplay = true w swift).

Kod, który mam działa poprawnie w zwykłym widoku z NSTextField, ale nie działa, gdy pole tekstowe jest częścią NSTableCellView. Timer strzela, ale pole nie jest przerysowane.

Nie działa również, jeśli ponownie załaduję całą tabelę co 100 ms z kontrolki viewController. Ponowne wczytanie całej tabeli nie było dobrym rozwiązaniem, ponieważ wybór jest tracony co 100 ms, a użytkownik nie może już edytować (zwykłych) komórek.

W jaki sposób powinienem ponownie załadować jeden konkretny obszar tekstowy w kilku komórkach całego widoku tabeli co sekundę?

@IBDesignable class ProgressCellView : NSTableCellView 
{ 
//MARK: properties 
@IBInspectable var clock : Bool = false //should this type of cell show a clock? (is set to true in interface builder) 

private lazy var formatter = TimeIntervalFormatter() //move to view controller?: 1 timer for the entire table => but then selection is lost 
private var timer : dispatch_source_t! 
private var isRunning : Bool = false 

//MARK: init 
override func awakeFromNib() 
{ 
    self.timeIndex?.formatter = formatter 
    if self.clock 
    { 
     self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()) 
     dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 100 * NSEC_PER_MSEC, 50 * NSEC_PER_MSEC) //50 ms leeway, is good enough for the human eye 
     dispatch_source_set_event_handler(timer) { 
      self.timeIndex?.needsDisplay = true 
      //self.needsDisplay = true 
      //self.superview?.needsDisplay = true 
     } 
    } 

} 

//only run the clock when the view is visible 
override func viewWillMoveToSuperview(newSuperview: NSView?) 
{ 
    super.viewWillMoveToSuperview(newSuperview) 
    if self.clock && !isRunning { dispatch_resume(self.timer); isRunning = true } 
} 

override func removeFromSuperview() 
{ 
    super.removeFromSuperview() 
    if self.clock && isRunning { dispatch_suspend(self.timer); isRunning = false } 
} 

//MARK: properties 
override var objectValue: AnyObject? { 
    didSet { 
     let entry = self.objectValue as! MyEntry 
     self.progressBar?.doubleValue = entry.progress?.fractionCompleted ?? 0 

     self.timeIndex?.objectValue = entry.dateStarted   
    } 
} 

//MARK: user interface 
@IBOutlet var timeIndex  : NSTextField? 
@IBOutlet var progressBar : NSProgressIndicator? 

}

+0

Czy zdarzyło ci się to zrozumieć? – Matt

+0

Kinda. Każda komórka ma zegar (chociaż wolałbym mieć 1 zegar dla całej tabeli). W obsadzie czasowej aktualizuję objectValue pola tekstowego, zamiast ustawiania właściwości needDisplay na true. Niestandardowy formater wyświetla prawidłową ilość upływającego czasu. Jeśli masz dużą liczbę komórek wymagających aktualizacji, prawdopodobnie spowoduje to problemy z powodu dużej liczby timerów (po jednym dla każdej komórki). Nie jest to jednak możliwe w moim przypadku. YMMV. Jeden zegar zsynchronizowałby również aktualizacje, które powinny wyglądać ładniej. – user965972

Odpowiedz

6

Generalnie nie jest dobrym pomysłem przeładowanie komórki tylko po to, aby zmienić jeden element w niej, nie wspominając już o jego stanie, ponieważ, jak powiedziałeś, przedstawia on wiele problemów ze stanami, animacja + to także powoduje problem z przewijaniem i twoje ponowne renderowanie. Biorąc to pod uwagę, jest to z pewnością najłatwiejsze rozwiązanie. Spróbujmy pójść trochę bardziej w poziomie koncepcyjnym kodowania, ponieważ mogę wyraźnie zobaczyć, że jesteś zdolny do kodowania, który sam z właściwym kierunku

Kontrolery porównaniu Wyświetleń

Chociaż jestem pozwalając sobie na zasadzie cienki lód tutaj, ponieważ zawsze będą ludzie, którzy mają inne zdanie na ten temat, tutaj jest to, co myślę, że jest właściwe wykorzystanie widoków i kontrolerów:

  • Kontrolery w iOS służą do sterowania i konfiguracji widoki. W kontrolerach nie powinno być logiki sieciowej itp., Ale powinna komunikować się z warstwą modelu. Powinien on służyć jedynie jako decydent o tym, jak prezentować pożądane wyniki użytkownika. W przeciwieństwie do tego, widoki są tym, co powinno być kontrolowane. Nie powinno być żadnej logiki, która będzie działać z twoim modelem aplikacji. Widoki powinny być napisane tak, aby były jak najbardziej możliwe do ponownego użycia, chyba że naprawdę istnieje ku temu dobry powód (szczególnie specyficzny komponent itp.). Te powinny mieć metody, które pozwalają zmieniać właściwości w tym widoku. Nie należy bezpośrednio uzyskiwać dostępu do gniazd na komórce (ponieważ te mogą się zmieniać, nazwy itp., ale funkcjonalność prawdopodobnie pozostanie taka sama - jeśli nie, nie ma różnicy)
  • Widoki powinny zawierać logikę rysowania (zawiera również np. jeśli masz formatator daty, powinien tam być).

Rozwiązanie

Więc teraz, gdy ustaliliśmy co powinniśmy postępować, tutaj jest to, co myślę, że byłoby to najlepsze rozwiązanie:

  • Tworzenie jednego timera w kontrolerze i niech go uruchomić w nieskończoność
  • Utwórz pamięć, z której będziesz dynamicznie dodawać i usuwać komórki, które Cię interesują, podczas przewijania i podczas zmiany danych
  • Na każdym kleszcza zegara, które są prowadzone przez cały przechowywania i aktualizacji komórki, które są zainteresowane z odpowiednich danych z wykorzystaniem metod na komórkę (setProgress :), które będą aktualizować zarówno postęp LB

Rozwiązanie to powinno działać nawet bez użycie GCD. Problem polega na tym, że jeśli masz zbyt wiele próśb o aktualizację, może to zasadniczo zablokować interfejs użytkownika i renderować go tylko czasami, a może nigdy. W tym celu należy zaktualizować interfejs użytkownika w głównym wątku, ale wywoływany w trybie asynchronicznym, tak:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {() -> Void in 
     dispatch_async(dispatch_get_main_queue(), {() -> Void in 
      // Do update here 
     }) 
    }) 
+0

"koncepcyjny poziom kodowania". Lubię te typy odpowiedzi. Zatrzymałbym timer, gdyby nie było żadnych komórek. – user965972

+0

Tak, masz absolutną rację. Chociaż zrobiłbym to jako optymalizację, gdy wiesz, że to działa, krok po kroku. –

+0

Co się stanie, jeśli wartości, które należy pokazać/zmienić, są powiązane z innymi obiektami? – Matt

1

Jeśli masz jedno pole tekstowe, jest to prosta sprawa, aby ją zaktualizować powołując to metoda setNeedsDisplay.

Jednak w widoku tabeli nie ma już jednego widoku tekstowego. Każda z twoich komórek prawdopodobnie ma swoją własną i nie powinieneś bezpośrednio adresować widoków w widoku tabeli (ponieważ mogą przewijać ekrany poza ekranem, odzyskiwać dane, aby wyświetlać dane dla innych ścieżek indexPath), itp.

Co powinieneś zrobić, to zaktualizować swój model, a następnie wywołać polecenie reloadData w widoku całej tabeli lub zadzwonić pod numer reloadRowsAtIndexPaths:withRowAnimation: (Jeśli chcesz tylko ponownie załadować zmienne IndexPaths).

BTW, to nie ma nic wspólnego z Core Animation. Powinieneś usunąć ten tag ze swojego posta.

+0

Podstawowe dane nie są zmieniane. Data rozpoczęcia jest częścią modelu danych, a komórka pokazuje, ile czasu minęło. Również ponowne załadowanie całej tabeli nie jest opcją, ponieważ wtedy wybór zostanie utracony. Przeładowanie określonych komórek oznacza przeszukanie źródła danych dla kilku dotkniętych komórek, podczas gdy same komórki już wiedzą, czy powinny same się zaktualizować, czy nie. – user965972