Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

periodically update textfield inside a NSTableCellView with a timer

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?

}

like image 580
user965972 Avatar asked Mar 10 '15 21:03

user965972


2 Answers

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:

  • Controllers in iOS serve to control and configure views. In controllers, there should not be network logic etc., but it should communicate with your model layer. It should only serve as a decision maker about how to present user desired output
  • Contrary to that, Views are what should be controlled. There should be no logic that works with your application model. Views should be written so they are as reusable as possible, unless there is really good reason for them not to (very specific component etc.). Those should have methods, that allow you to change properties on that view. You should not access outlets on the cell directly (since those can change, names etc., but functionality will probably remain the same - if not, there is no difference)
  • Views should contain drawing logic (it also includes fe. if you have date formatter, it should be in there).

Solution

So now if we established what we should follow, here is what I think would be the best solution:

  • Create one timer in your controller and let it run indefinitely
  • Create a storage, from which you will dynamically add and remove cells that you are interested in, as you scroll and as your data change
  • On each timer tick, run through entire storage, and update cells that you are interested in with proper data using methods on cell (setProgress:), which will update both progress and Lb

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
        })
    })
like image 145
Jiri Trecak Avatar answered Oct 08 '22 03:10

Jiri Trecak


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.

like image 30
Duncan C Avatar answered Oct 08 '22 02:10

Duncan C