NSOutlineView 使用指南

简介

NSOutlineView(大纲视图)继承着NSTableView,两者有许多相同的地方,同样能够展示列表数据,但是NSOutlineView能够方便的实现数据列表的折叠和展开,就像macOS上Finder中的目录和子目录一样.所以它对于显示层次结构数据非常有用。

NSOutlineView

使用

  • 1.创建NSOutlineView
    NSOutlineViewNSTableView一样具有滑动功能,所以同样需要将其设置为NScrollViewDocumentView.然后添加对应的列NSTableColumn.
let tableColumn = NSTableColumn.init()

let outlineView = NSOutlineView.init()
outlineView.delegate = self;
outlineView.dataSource = self;
outlineView.headerView = nil
outlineView.addTableColumn(tableColumn)
outlineView.outlineTableColumn = tableColumn

let scrollView = NSScrollView.init(frame: NSMakeRect(0, 0, 500, 500))
scrollView.documentView = outlineView
view.addSubview(scrollView)
  • 2.定义CellView
    与NSTableView的TableColumn类似,创建一个CellView,作为每行的显示样式.这里比较简单添加一个ImageView和一个TextField作为展示.
class TableCellView: NSTableCellView {
    
    let icon = NSImageView.init()
    let title = NSTextField.init()
    
    var model: TreeNodeModel? {
        didSet {
            title.stringValue = model?.name ?? ""
            if model?.childNodes.count ?? 0 > 0 {
                icon.image = NSImage.init(named: NSImage.Name.folder)
            }else{
                icon.image = NSImage.init(named: NSImage.Name.listViewTemplate)
            }
        }
    }
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setupUI()
    }
    
    func setupUI() {
        
        // 添加图片
        icon.frame = NSRect.init(x: 5, y: 5, width: 30, height: 30)
        icon.image = NSImage.init(named: NSImage.Name(rawValue: "NSFolder"))
        addSubview(icon)
        
        // 添加标题
        title.frame = NSRect.init(x: 45, y: 10, width: 100, height: 20)
        title.font = NSFont.boldSystemFont(ofSize: 15)
        title.isBordered = false
        title.isEditable = false
        title.textColor = NSColor.black
        title.bezelStyle = .roundedBezel
        addSubview(title)
    }
    
    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // Drawing code here.
    }
}
  • 3.数据
    定义节点数据模型,包含一个名称和一个数组,数组中包含该节点中的所有子节点.项目可根据实际需求进行定义
class TreeNodeModel: NSObject {
    var name: String?
    lazy var childNodes: Array = {
        return [TreeNodeModel]()
    }()
}
var treeModel: TreeNodeModel = TreeNodeModel()

override func viewDidLoad() {
    super.viewDidLoad()
    configData()
}

func configData() {
    
    let rootNode = TreeNodeModel()
    rootNode.name = "网易"
    let rootNode2 = TreeNodeModel()
    rootNode2.name = "腾讯"
    
    self.treeModel.childNodes.append(rootNode)
    self.treeModel.childNodes.append(rootNode2)
    
    let level11Node = TreeNodeModel()
    level11Node.name = "电商"
    let level12Node = TreeNodeModel()
    level12Node.name = "游戏"
    let level13Node = TreeNodeModel()
    level13Node.name = "音乐"
    
    rootNode.childNodes.append(level11Node)
    rootNode.childNodes.append(level12Node)
    rootNode.childNodes.append(level13Node)
    rootNode2.childNodes.append(level13Node)
   
    let level21Node = TreeNodeModel()
    level21Node.name = "研发"
    let level22Node = TreeNodeModel()
    level22Node.name = "运营"
    
    level11Node.childNodes.append(level21Node)
    level11Node.childNodes.append(level22Node)
    self.treeView.reloadData()
}
  • 4.实现数据源代理
    常用数据源

返回节点数,当item为nil时,表示根节点
-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(nullable id)item;
返回每个节点的数据模型model,当item为nil时,表示根节点
-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(nullable id)item;
节点是否允许展开
-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;

示例代码

extension ViewController: NSOutlineViewDataSource {
    
    // 返回节点数,当item为nil时,表示根节点
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        
        let rootNode:TreeNodeModel
        
        if item != nil {
            rootNode = item as! TreeNodeModel
        }else{
            rootNode =  self.treeModel
        }
        
        return rootNode.childNodes.count
    }
    
    // 返回每个节点的数据模型model,当item为nil时,表示根节点
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        
        let rootNode:TreeNodeModel
        
        if item != nil {
            rootNode = item as! TreeNodeModel
        }
        else {
            rootNode =  self.treeModel
        }
        
        return rootNode.childNodes[index]
    }
    
    // 节点是否允许展开
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        
        let rootNode:TreeNodeModel = item as! TreeNodeModel
        
        return rootNode.childNodes.count > 0
    }
}
  • 5.实现代理协议
    常用代理

返回每个节点对应的视图,类似TableView中的Cell
-(nullable NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(nullable NSTableColumn *)tableColumn item:(id)item;
返回每个节点的行高
-(CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item;
节点被选中或取消
-(void)outlineViewSelectionDidChange:(NSNotification *)notification;(id)item;

示例代码

extension ViewController: NSOutlineViewDelegate {
    
    // 返回节点对应的视图
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        
        let identifire = NSUserInterfaceItemIdentifier(rawValue: "identifire")
        var view = outlineView.makeView(withIdentifier: identifire, owner: self) as? TableCellView
        if (view == nil) {
            view = TableCellView.init()
        }
        
        let model = item as! TreeNodeModel
        view?.model = model
        
        return view
    }
    
    // 行高
    func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
        return 40
    }
    
    // 节点选中或取消
    func outlineViewSelectionDidChange(_ notification: Notification) {
        
        let treeView = notification.object as! NSOutlineView
        // 获取选中节点模型
        let row = treeView.selectedRow
        let model = treeView.item(atRow: row)
        print(model)
    }
}

完成效果

常用方法

  • 1.节点的添加与删除
    在数据模型中添加或删除对应节点模型后,调用outlineView的reloadData方法即可.
    示例代码
let addNode = TreeNodeModel()
addNode.name = nodeName
item?.childNodes.append(addNode)
outlineView.reloadData()
  • 2.节点的展开与收缩
    示例代码
// 展开节点
open func expandItem(_ item: Any?)
// 展开节点,expandChildren是否展开节点内的所有子节点
open func expandItem(_ item: Any?, expandChildren: Bool)
// 收缩节点
open func collapseItem(_ item: Any?)
// 收缩节点,collapseChildren是否收缩节点内的所有子节点
open func collapseItem(_ item: Any?, collapseChildren: Bool)
// 展开所有节点
outlineView.expandItem(nil, expandChildren: true)

// 关闭第一行节点,及节点内的所有子节点
let model = oulineView.item(atRow: 1)
outlineView.collapseItem(model, collapseChildren: true)
  • 3.通过代码选中行
    示例代码
open func selectRowIndexes(_ indexes: IndexSet, byExtendingSelection extend: Bool)
let indexSet: IndexSet = [1,3]
outlineView.selectRowIndexs(indexSet, byExtendingSelection: false)

自定义

  • 1.自定义展开收缩箭头图标
    实现NSOutlineView的子类,并重写makeView(withIdentifier identifier:, owner:)方法,在该方法中自定义箭头图标
    示例代码
override func makeView(withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?) -> NSView?{
    
    let view = super.makeView(withIdentifier: identifier, owner: owner)
    
    if (identifier == NSOutlineView.disclosureButtonIdentifier) {
        let button:NSButton = view as! NSButton
        // 自定义收缩图片
        button.image = NSImage(named: NSImage.Name(rawValue: "Plus"))!
        // 自定义展开图片
        button.alternateImage = NSImage(named: NSImage.Name(rawValue: "Minus"))!
        button.isBordered = false
        button.title = ""
        return button
    }
    return view
}
  • 2.添加自定义菜单
    同样实现NSOutlineView的子类,并重写menu(for event:)方法,在该方法中返回自定义的菜单NSMenu
    示例代码
var nodeMenu: NSMenu?

override func menu(for event: NSEvent) -> NSMenu? {

    let pt = self.convert(event.locationInWindow, from: nil)
    let row = self.row(at: pt)
    if row >= 0 {
        // 返回自定义菜单
        return self.nodeMenu
    }
    return super.menu(for: event)!
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343