首页 > 解决方案 > 将 CollectionDifference 应用于 NSTableView

问题描述

对于这个例子,假设我CollectionDifference从 annInt数组生成一个,然后inferringMoves像这样调用它

let a = [18, 18, 19, 11]
let b = [11, 19]
let diff = b.difference(from: a).inferringMoves()

for change in diff {
    switch change {
    case let .insert(offset, _, associatedWith):
        if let from = associatedWith {
            print("MOVE", from, offset)
        } else {
            print("INSERT", offset)
        }
    case let .remove(offset, _, associatedWith):
        // If it is a MOVE it was already recorded in .insert
        if associatedWith == nil {
            print("REMOVE", offset)
        }
    }
}

现在我需要获取更改数组并将其提供给NSTableViews更新方法

以这种方式,它可以干净地应用。我的问题是move条目的偏移量。上面的代码片段产生:

REMOVE 1
REMOVE 0
MOVE 2 1

现在显然我不能要求removeRowsand 01然后moveRow(2, 1),但这就是差异所暗示的。

我怎样才能干净地应用它?

问题似乎是NSTableView在应用插入/删除时立即更新其内部计数,因此移动将不起作用。

标签: swiftmacoscocoa

解决方案


所以这比我最初想象的要复杂得多!

这是 CollectionDifference 的扩展,它将返回一组包含移动的步骤。我已经在各种复杂的序列上对此进行了测试,它看起来很可靠。

/*
 This extension generates an array of steps that can be applied sequentially to an interface, or
 associated collection, to remove, insert AND move items. Apart from the first and last steps, all
 step indexes are transient and do not relate directly to the start or end collections.
 
 This is complicated than it first appears and not something that I could reduced further. The
 standard Changes are ordered: removals high->low, insertions low->high. Generating moves based on
 insertions means that the associated removes are pulled out-of-order, which requires all the
 later indexes to be offset in subtly different ways.
 
 Delayed removals modify the insert indexes. Out of order removals, and insertions made before the
 delayed removals modify the removal indexes. The effect of something that hasn't happeded yet, is
 different to something that has happened but in the wrong order.
 */

extension CollectionDifference where ChangeElement: Hashable
{
    public typealias Steps = Array<CollectionDifference<ChangeElement>.ChangeStep>
    
    public enum ChangeStep {
        case insert(_ element: ChangeElement, at: Int)
        case remove(_ element: ChangeElement, at: Int)
        case move(_ element: ChangeElement, from: Int, to: Int)
    }
    
    var maxOffset: Int { Swift.max(removals.last?.offset ?? 0, insertions.last?.offset ?? 0) }
    
    public var steps: Steps {
        guard !isEmpty else { return [] }
        
        // A mapping to modify insertion indexees
        let mapSize = maxOffset + count
        var insertionMap = Array(0 ... mapSize)
        
        // Items that may have been completed early relative to the Changes
        var completeRemovals = Set<Int>()
        var completeInsertions = Set<Int>()
        
        var steps = Steps()
        
        inferringMoves().forEach { change in
            switch change {
            case let .remove(offset, element, associatedWith):
                if associatedWith != nil {
                    // Delayed removals can make step changes in insert locations
                    insertionMap.remove(at: offset)
                } else {
                    steps.append(.remove(element, at: offset))
                    completeRemovals.insert(offset)
                }

            case let.insert(offset, element, associatedWith):
                if let associatedWith = associatedWith
                {
                    let from = associatedWith
                        - completeRemovals.filter({ $0 < associatedWith}).count
                        + completeInsertions.filter({ $0 < associatedWith}).count
                    
                    // Late removals re-adjust the insertion map by reducing higher indexes
                    insertionMap.indices.forEach {
                        if insertionMap[$0] >= associatedWith { insertionMap[$0] -= 1} }
                    
                    let to = insertionMap[offset]
                    
                    steps.append(.move(element, from: from, to: to))
                    
                    completeRemovals.insert(associatedWith)
                    completeInsertions.insert(to)
                } else {
                    let to = insertionMap[offset]
                    steps.append(.insert(element, at: to))
                    completeInsertions.insert(to)
                }
            }
        }

        return steps
    }
}

extension CollectionDifference.Change
{
    var offset: Int {
        switch self {
        case let .insert(offset, _, _): return offset
        case let .remove(offset, _, _): return offset
        }
    }
}

这些步骤可以应用于 NSTableView 或 NSOutlineView,如下所示:

for step in updates {
    switch step {
    case let .remove(_, index):
        outlineView.removeItems(at: [index], inParent: node, withAnimation: animation)
                    
    case let .insert(element, index):
        outlineView.insertItems(at: [index], inParent: node, withAnimation: animation)

    case let .move(element, from, to):
        outlineView.moveItem(at: from, inParent: node, to: to, inParent: node)
    }
}

推荐阅读