大苏尔大纲视图可扩展项目损坏

Big Sur outline view expandable items broken

我已经启动了一个新的 macOS 项目(目前在 Big Sur beta 3 上),NSOutlineView 节点似乎已损坏。分不清这是我还是 os.

Here's a sample project 说明了这个问题。还有一张图片...

如您所见,单元格与展开的人字形重叠。单击任一个 V 形可将第一行恢复为正确的布局,但不会恢复第二行。此外,永远不会调用 autosave 方法 persistentObjectForItemitemForPersistentObject

测试项目非常简单——我所做的只是将 SourceView 组件从视图库添加到默认应用程序项目,并将 delegate/data 源代码连接到视图控制器。还检查了 IB 中的 Autosave Expanded Items 并在 Autosave 字段中输入了名称。这是控制器代码的全部:

class ViewController: NSViewController {
    @IBOutlet var outlineView: NSOutlineView?

    let data = [Node("First item", 1), Node("Second item", 2)]
}

extension ViewController: NSOutlineViewDataSource {
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        data[index]
    }
    
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        true
    }
    
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        item == nil ? data.count : 0
    }
    
    func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
        item
    }
    
    func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
        (item as? Node)?.id
    }
    
    func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
        guard let id = object as? Int else { return nil }
        return data.first { [=10=].id == id }
    }
}


extension ViewController: NSOutlineViewDelegate {
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let node = item as? Node else {
            preconditionFailure("Invalid data item \(item)")
        }
        let view = outlineView.makeView(withIdentifier: nodeCellIdentifier, owner: self) as? NSTableCellView
        view?.textField?.stringValue = node.name
        view?.imageView?.image = NSImage(systemSymbolName: node.icon, accessibilityDescription: nil)
        return view
    }
}


final class Node {
    let id: Int
    let name: String
    let icon: String
    
    init(_ name: String, _ id: Int, _ icon: String = "folder") {
        self.id = id
        self.name = name
        self.icon = icon
    }
}

private let nodeCellIdentifier = NSUserInterfaceItemIdentifier("DataCell")

还有 Mac 开发人员可以提供帮助吗?

来源列表

什么是源列表?它是 NSOutlineView(它是 NSTableView) 有一个特殊的 治疗。 Finder 屏幕截图:

要创建源列表,您只需设置 selectionHighlightStyle 属性 到 .sourceList。文档说:

The source list style of NSTableView. On 10.5, a light blue gradient is used to highlight selected rows.

它的具体作用是什么? 跳转到 Xcode 中的定义 并阅读评论(未包含在文档中):

The source list style of NSTableView. On 10.10 and higher, a blur selection is used to highlight rows. Prior to that, a light blue gradient was used. Note: Cells that have a drawsBackground property should have it set to NO. Otherwise, they will draw over the highlighting that NSTableView does. Setting this style will have the side effect of setting the background color to the "source list" background color. Additionally in NSOutlineView, the following properties are changed to get the standard "source list" look: indentationPerLevel, rowHeight and intercellSpacing. After calling setSelectionHighlightStyle: one can change any of the other properties as required. In 10.11, if the background color has been changed from the "source list" background color to something else, the table will no longer draw the selection as a source list blur style, and instead will do a normal blue highlight.

由于您在大苏尔,请注意 SelectionHighlightStyle.sourceList 已弃用。 应该使用 style & effectiveStyle.

示例项目

Xcode:

  • 新项目
    • macOS 和应用程序(故事板和 AppKit 应用程序委托和 Swift)
  • Main.storyboard
    • 添加源列表控件
      • 定位并修复约束
      • 将委托和数据源设置为 ViewController
      • 启用自动保存展开的项目
      • 将自动保存设置为任何你想要的(我有FinderLikeSidebar
        • 明智地选择,因为扩展状态保存在用户默认值中 NSOutlineView Items FinderLikeSidebar键下
      • 创建@IBOutlet var outlineView: NSOutlineView!
    • 添加另一个文本 Table 单元格视图(无图像)
      • 将标识符设置为 GroupCell
  • ViewController.swift
    • 下面的注释代码

截图

如您所见,它几乎与 Finder 一样 - 第 2 级仍然是缩进的。原因 因为 Documents 节点是可扩展的(有子节点)。我把它们放在这里是为了演示自动保存。

如果您想将所有第 2 级节点移到左侧,只需删除它们。

ViewController.swift代码

没什么可说的,除了 - 阅读评论 :)

import Cocoa

// Sample Node class covering groups & regular items
class Node {
    let id: Int
    let title: String
    let symbolName: String?
    let children: [Node]
    let isGroup: Bool
    
    init(id: Int, title: String, symbolName: String? = nil, children: [Node] = [], isGroup: Bool = false) {
        self.id = id
        self.title = title
        self.symbolName = symbolName
        self.children = children
        self.isGroup = isGroup
    }
    
    convenience init(groupId: Int, title: String, children: [Node]) {
        self.init(id: groupId, title: title, children: children, isGroup: true)
    }
}

extension Node {
    var cellIdentifier: NSUserInterfaceItemIdentifier {
        // These must match identifiers in Main.storyboard
        NSUserInterfaceItemIdentifier(rawValue: isGroup ? "GroupCell" : "DataCell")
    }
}

extension Array where Self.Element == Node {
    // Search for a node (recursively) until a matching element is found
    func firstNode(where predicate: (Element) throws -> Bool) rethrows -> Element? {
        for element in self {
            if try predicate(element) {
                return element
            }
            if let matched = try element.children.firstNode(where: predicate) {
                return matched
            }
        }
        return nil
    }
}

class ViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
    @IBOutlet var outlineView: NSOutlineView!
    
    let data = [
        Node(groupId: 1, title: "Favorites", children: [
            Node(id: 11, title: "AirDrop", symbolName: "wifi"),
            Node(id: 12, title: "Recents", symbolName: "clock"),
            Node(id: 13, title: "Applications", symbolName: "hammer")
        ]),
        Node(groupId: 2, title: "iCloud", children: [
            Node(id: 21, title: "iCloud Drive", symbolName: "icloud"),
            Node(id: 22, title: "Documents", symbolName: "doc", children: [
                Node(id: 221, title: "Work", symbolName: "folder"),
                Node(id: 221, title: "Personal", symbolName: "folder.badge.person.crop"),
            ])
        ]),
    ]
    
    override func viewWillAppear() {
        super.viewWillAppear()
        
        // Expanded items are saved in the UserDefaults under the key:
        //
        // "NSOutlineView Items \(autosaveName)"
        //
        // By default, this value is not present. When you expand some nodes,
        // an array with persistent objects is saved. When you collapse all nodes,
        // the array is removed from the user defaults (not an empty array,
        // but back to nil = removed).
        //
        // IOW there's no way to check if user already saw this source list,
        // modified expansion state, etc. We will use custom key for this
        // purpose, so we can expand group nodes (top level) when the source
        // list is displayed for the first time.
        //
        // Next time, we wont expand anything and will honor autosaved expanded
        // items.
        if UserDefaults.standard.object(forKey: "FinderLikeSidebarAppeared") == nil {
            data.forEach {
                outlineView.expandItem([=10=])
            }
            UserDefaults.standard.set(true, forKey: "FinderLikeSidebarAppeared")
        }
    }
    
    // Number of children or groups (item == nil)
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        item == nil ? data.count : (item as! Node).children.count
    }
    
    // Child of a node or group (item == nil)
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        item == nil ? data[index] : (item as! Node).children[index]
    }
    
    // View for our node
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        guard let node = item as? Node,
              let cell = outlineView.makeView(withIdentifier: node.cellIdentifier, owner: self) as? NSTableCellView else {
            return nil
        }
        
        cell.textField?.stringValue = node.title
        
        if !node.isGroup {
            cell.imageView?.image = NSImage(systemSymbolName: node.symbolName ?? "folder", accessibilityDescription: nil)
        }
        
        return cell
    }

    // Mark top level items as group items
    func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
        (item as! Node).isGroup
    }
    
    // Every node is expandable if it has children
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        !(item as! Node).children.isEmpty
    }
    
    // Top level items (group items) are not selectable
    func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
        !(item as! Node).isGroup
    }
    
    // Object to save in the user defaults (NSOutlineView Items FinderLikeSidebar)
    func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
        (item as! Node).id
    }
    
    // Find an item from the saved object (NSOutlineView Items FinderLikeSidebar)
    func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
        guard let id = object as? Int else { return nil }
        return data.firstNode { [=10=].id == id }
    }
}