I have a tableView made out of several custom NSTableCellView's. Some of the views have to show a timer (how much time has passed) together with a NSProgressIndicator.
I created a timer (with grand central display) and every 100 ms I update the textView with setNeedsDisplay (or .needsDisplay = true in swift).
The code I have works fine in a regular view with a NSTextField, but it's not working once the textfield is part of a NSTableCellView. The timer fires but the field is not redrawn.
It's also not working if I reload the entire table every 100 ms from within the viewController. Reloading the entire table was not a good solution in any case because the selection is lost every 100 ms and the user can't edit the (regular) cells anymore.
So how should I reload one particular textField in a few cells of an entire tableview every second?
@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?
}
In general, it is very bad idea to reload cell just to change one element in it, let alone its state, because as you said, it presents a lot of problems with states, animation + it also creates problem with scrolling and your re-rendering. That being said, it is for sure the easiest solution. Lets try to go little more in conceptual level of coding, since I can clearly see that you are capable of coding that by yourself with proper direction
Controllers vs. Views
While I am letting myself on a thin ice here because there will always be people who have different opinion about this, here is what I think is proper usage of views and controllers:
Solution
So now if we established what we should follow, here is what I think would be the best solution:
This solution should work even without use of GCD. The problem is that if you have too much requests to update, it can basically clog the UI and render it only sometimes, or maybe never. For that, you should update UI on main thread, but called in async mode, like this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { () -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Do update here
})
})
If you have a single text field, it's a simple matter to get it to update by invoking it's setNeedsDisplay method.
However, with a table view, there is no longer a single text view. Each of your cells presumably has it's own, and you're not supposed to address the views in the table view directly (since they can scroll off-screen, get recycled to display data for other indexPaths, etc.)
What you should do is update your model and then either call reloadData on the whole table view, or call reloadRowsAtIndexPaths:withRowAnimation:
(If you only want to reload the indexPaths that have changed.)
BTW, this has nothing to do with Core Animation. You should remove that tag from your post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With