NSOutlineViewの基本的な実装


概要

  • NSOutlineViewを使って下記のように表示するだけの実装を行う。

GitHub

参考

実装

Model

  • 今回セルに対応されるModelは以下の通り。
  • NodeTypeはそのセルがGroupなのかを判断するのに使用している
enum NodeType {
    case group
    case parent
    case item
}

class Node {

    // MARK: - Properties

    var title: String
    var nodeType: NodeType
    var children: [Node]

    // MARK: - Lifecycle

    init(title: String,
         children: [Node] = [],
         nodeType: NodeType = .item) {
        self.title = title
        self.children = children
        self.nodeType = nodeType
    }

    // MARK: - Helpers

    var numberOfChildren: Int {
        children.count
    }

    var hasChildren: Bool {
        !children.isEmpty
    }
}

ViewController.swift

NSOutlineViewの設定

private func initializeOutlineView() {
    // 別のXibファイルから読み込む場合は登録が必要
    outlineView.register(MyCellView.nib(inBundle: nil),
                         forIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: MyCellView.self)))
    outlineView.delegate = self
    outlineView.dataSource = self
    outlineView.autosaveExpandedItems = true

    // スタイルの設定
    outlineView.selectionHighlightStyle = .regular
    outlineView.floatsGroupRows = false

    nodes.append(contentsOf: Node.createSampleNodes())
    outlineView.reloadData()
}

Starting in OS X version 10.5, passing 'nil' will expand each item under the root in the outline view.

outlineView.expandItem(nil, expandChildren: true)

NSOutlineViewDataSource

  • NSOutlineViewのデータに関するDelegateメソッドを実装する
  • itemが含むchildrenの数を返す
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
    if let node = item as? Node {
        return node.numberOfChildren
    }

    return nodes.count
}
  • 親に対する子の情報を与える
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
    if let feed = item as? Node {
        return feed.children[index]
    }

    return nodes[index]
}
  • itemが開閉可能か(trueを返すとCellViewにDisclosure Buttonが表示される)
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
    if let node = item as? Node {
        return node.hasChildren
    }

    return false
}
  • itemがグループ行のものかどうか
// グループ行かどうか(NSOutlineViewの設定如何では見た目と挙動が特別となる)
func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
    guard let node = item as? Node else {
        return false
    }

    return node.nodeType == .group
}

NSOutlineViewDelegate

  • NSOutlineViewのDelegateメソッドを実装する
  • 各行に表示するCellViewの設定
public func outlineView(_ outlineView: NSOutlineView,
                        viewFor tableColumn: NSTableColumn?,
                        item: Any) -> NSView? {

    guard
        let myCellView = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: MyCellView.self)), owner: self) as? MyCellView,
        let node = item as? Node
    else {
        return nil
    }

    myCellView.configureUI(withNode: node)

    return myCellView
}
  • 行選択が変更された場合に呼ばれるNotification
func outlineViewSelectionDidChange(_ notification: Notification) {
    guard let outlineView = notification.object as? NSOutlineView else {
        return
    }

    let selectedIndex = outlineView.selectedRow
    guard let node = outlineView.item(atRow: selectedIndex) as? Node else {
        return
    }

    print("Selected Title: \(node.title)")
}

NSTreeControllerの話

  • NSOutlineViewで調べているとNSTreeControllerを使っている例が多くヒットする。
  • ただCocoaBindingが絡んでいて、私の理解が浅いためちょっと使いこなせなかった。
    • ので今回はNSOutlineView単体で実装している
  • イメージに過ぎないが、NSTreeControllerを使うことで煩雑な処理、例えば複数選択のアクション・入れ替え・削除が簡単に扱えると思っている。

参考