使用计时器定期更新 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
})
})
我有一个 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
})
})