Cocoa:NSTableView 实现类似 Dock 的拖拽排序

2,048 阅读3分钟

本文将分享如何实现 Table View 的 Drag & Drop 排序,如何 Open Gap for Drag Destination 和实现过程中遇到的问题及相应解决方案。

Dock 拖拽排序效果(Open Gap for Drag Destination):
Dock-Drag-and-Drop

CurrencyX 1.3 版本中,我们增加了汇率列表可拖拽排序功能,实现过程很简单;在优化 Drop 效果为 Open Gap 时(即类似 Dock 上拖拽图标排序的效果)遇到了 Row 不断抖动的问题,最终偶然发现是因为仅设置了 rowHeight 属性而没有实现 Table View Delegate 中 Row Height 相关的方法导致的。

Drag & Drop

首先设置 Table View 可以接受的 Drag Type,在这里我们对 Currency 排序使用的是 Currency Code,Type 为 NSStringPboardType:

let registeredTypes:[String] = [NSStringPboardType]  
tableView.registerForDraggedTypes(registeredTypes)  

在用户 Drag 时,将需要的信息写到剪贴板上:

func tableView(tableView: NSTableView, writeRowsWithIndexes rowIndexes: NSIndexSet, toPasteboard pboard: NSPasteboard) -> Bool {  
    let currencyCode = // Get Currency Code At Row
    let registeredTypes:[String] = [NSStringPboardType]
    pboard.declareTypes(registeredTypes, owner: self)
    pboard.setString(currencyCode, forType: NSStringPboardType)
    return true
}

在 Drop 时,首先根据用户拖拽 Row 的位置来返回适当的操作,在这里,我们希望仅当用户插入两行之间时执行 Move 操作:

func tableView(tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableViewDropOperation) -> NSDragOperation {  
    if (dropOperation == .Above) { return .Move }
    return .None;
}

对于 Valid Drop 操作,通过实现下述方法获取剪贴板上的信息并依次进行数据排序,更新 Table View:

func tableView(tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableViewDropOperation) -> Bool {  
    if let currencyCode = info.draggingPasteboard().stringForType(NSStringPboardType),
       let oldIndex = userCurrencyCodes.indexOf(currencyCode) {
        // Update Data Source
        // Update Table View
        return true
    } else {
        return false
    }
}

至此,我们已经完成了 Table View 的 Drag & Drop 排序。

Init Effect

Open Gap

同时我们也正在开发 Today Extension,发现系统自带的排序操作起来用户体验更好一些。

Extension Effect

我们也决定使用这样的 Drop 效果,实现起来也很简单,只需要对 Table View 进行如下设置即可:

tableView.draggingDestinationFeedbackStyle = .Gap  

然而运行起来得到的结果是可怕的:

Buggy Effect

Debug

首先我们新建了一个测试工程,创建了一个最简单的 Table View 并将 Drag & Drop 相关的代码加入其中,发现仍然会出现拖拽时整个 View 抖动或者某些 Row 弹跳的问题。确定引发问题的原因与我们自定义的 NSTableCellView 没有关系。

接着仔细察看文档和 Google 到如何实现“Open Gap for Drag & Drop”的相关文章,我们的实现似乎并没有问题。

在 Google 时发现 StackOverflow 上有人提到 NSTableView 的 Dragging Destination Feedback Style 是 OS X 10.9 Mavericks 之后才提供的 API,并给出了针对更早系统自定义的实现方式。基本思路是在用户 Drag 到某个 Row 上时,使用自定义 Table View 的 noteHeightOfRowsWithIndexesChanged 方法修改 Row Height 来实现的。猜测系统 API 的实现方式应该也无太大区别。

联想到我们的代码中,仅在 Table View 初始化时设置了 rowHeight 属性,拖拽时抖动的表现可能是 Table View 无法获取到 Row 正确的高度。于是实现了 Delegate 方法返回 Row Height:

 func tableView(tableView: NSTableView, heightOfRow row: Int) -> CGFloat

再次运行便成功了🎉。

Final Effect

此外,在 Drop Destination Feedback Style 为 Gap 时,为了获得更好的用户体验,可以修改 Validate Drop 函数在 dropOperation 为 .Above 或 .On 的时候都 return .Move,这样也和 Extension 中系统的默认行为更加接近。

Happy Coding 🍺

顺便看了一眼毫无进度的某下载软件,拖拽时也有些颤~抖。

支持我们

SalesX 是给 Apple 开发者使用的菜单栏工具,第一时间把 app 销售情况推送给你,7 天免费试用

CurrencyX 是 Mac 上小而美的汇率 app

如果你觉得文章对你有帮助,可以买一个支持我们

关注我们公众号,获取最新文章推送