使用计时器定期更新 NSTableCellView 内的文本字段

periodically update textfield inside a NSTableCellView with a timer

我有一个 tableView 由几个自定义 NSTableCellView 组成。一些视图必须与 NSProgressIndicator 一起显示计时器(已经过了多少时间)。

我创建了一个计时器(带有大中央显示屏),每 100 毫秒我用 setNeedsDisplay(或 swift 中的 .needsDisplay = true)更新 textView。

我的代码在带有 NSTextField 的常规视图中工作正常,但一旦文本字段成为 NSTableCellView 的一部分,它就无法工作。计时器触发但字段未重绘。

如果我每 100 毫秒从 viewController 中重新加载整个 table,它也不起作用。重新加载整个 table 在任何情况下都不是一个好的解决方案,因为每 100 毫秒就会丢失一次选择,并且用户无法再编辑(常规)单元格。

那么我应该如何每秒在整个 table 视图的几个单元格中重新加载一个特定的文本字段?

@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?

}

如果您只有一个文本字段,可以通过调用它的 setNeedsDisplay 方法来更新它。

但是,对于 table 视图,不再有单个文本视图。您的每个单元格大概都有自己的单元格,并且您不应该直接处理 table 视图中的视图(因为它们可以滚动到屏幕外,被回收以显示其他 indexPaths 等的数据)

您应该做的是更新您的模型,然后在整个 table 视图上调用 reloadData,或者调用 reloadRowsAtIndexPaths:withRowAnimation:(如果您只想重新加载已更改的 indexPaths。)

顺便说一句,这与核心动画无关。您应该从 post.

中删除该标签

一般来说,重新加载单元格只是为了改变其中的一个元素是非常糟糕的主意,更不用说它的状态了,因为正如你所说,它会带来很多状态、动画问题+它也会产生问题滚动和重新渲染。话虽如此,这肯定是最简单的解决方案。让我们尝试在编码的概念层面上多做一点,因为我可以清楚地看到你有能力在正确的指导下自己编码

控制器与视图

虽然我在这里让自己如履薄冰,因为总会有人对此有不同的看法,但我认为这是正确使用视图和控制器的方法:

  • iOS 中的控制器用于控制和配置视图。在控制器中,不应该有网络逻辑等,但它应该与你的模型层通信。它应该只作为如何呈现用户所需输出的决策者
  • 与此相反,应该控制视图。不应该有适用于您的应用程序模型的逻辑。视图的编写应使其尽可能可重用,除非确实有充分的理由不这样做(非常具体的组件等)。这些应该有方法,允许您更改该视图的属性。您不应该直接访问单元格上的插座(因为它们可以更改,名称等,但功能可能会保持不变 - 如果不是,则没有区别)
  • 视图应该包含绘图逻辑(它还包括 fe。如果你有日期格式化程序,它应该在里面)。

解决方案

现在,如果我们确定了应该遵循的内容,我认为这是最好的解决方案:

  • 在你的控制器中创建一个计时器并让它运行无限期
  • 创建一个存储,您可以在滚动和数据更改时从中动态添加和删除您感兴趣的单元格
  • 在每个计时器滴答声中,运行 遍历整个存储,并使用单元格 (setProgress:) 上的方法使用适当的数据更新您感兴趣的单元格,这将同时更新进度和 Lb

即使不使用 GCD,此解决方案也应该有效。问题是,如果您有太多的更新请求,它基本上会阻塞 UI 并仅有时渲染它,或者可能永远不会渲染它。为此,您应该在主线程上更新 UI,但在异步模式下调用,如下所示:

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