欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

macOS: Source List

程序员文章站 2022-04-26 22:11:18
...

最近在学习 MacOS Cocoa 编程,一直想实现和Finder(访达)左侧菜单一样的效果。通过查资料知道Xcode自带的一个组合控件可以达到这种效果,它就是 Source List

注意,Source List 并不是 Cocoa 控件 而是Xcode 里面提供的一个组合控件。

前言

我想实现的参照(访达的左边的导航)

macOS: Source List

本文实现的效果

本文并不涉及左右分离布局相关的内容

macOS: Source List

拖控件,绑定属性

  1. 新建一个 Cocoa app 工程
  2. 从控件栏搜索 source list 并拖入到 左边视图上。
  3. 将 当前视图树中唯一的 OutlineView 绑定到 ViewController.swfit 中

代码

我在代码中写了很多详细的中文注视,所以博客中就不写了。
我将示例工程放在了 Gitee上,需要查阅的朋友可以拉取运行就可以了。

//
//  ViewController.swift
//  Left Navigation bar
//
//  Created by Joel on 2020/7/3.
//  Copyright © 2020 Joel. All rights reserved.
//

import Cocoa

// MARK:- Group and Item definitions

// 子标签类
class GroupItem {
    var name: String // 名字
    var icon: String // 图标
    
    init(name: String, icon: String) {
        self.name = name
        self.icon = icon
    }
}

class Group {
    // 检查一个对象是否是该类的有效实例
    static func isInstance(any: Any)-> Bool {
        if let _ = any as? Group {
            return true
        }
        return false
    }
    
    var name: String // 组名
    var items: [GroupItem] // 子标签
    
    // 获取当前组有多少个子标签
    var numberOfItems: Int {
        get {
            return self.items.count
        }
    }
    
    init(name: String) {
        self.name = name
        self.items = []
    }
    
    // 为当前组添加一个子标签
    func addChild(item: GroupItem) {
        self.items.append(item)
    }
    
    // 根据下标获取一个子标签
    // 通常来说下标一定不会越界,但是为了安全我们在下表越界之后返回一个无效的标签对象
    func childAt(index: Int) -> GroupItem {
        if index >= 0 && index < numberOfItems {
            return self.items[index]
        } else {
            return GroupItem(name: "ERROR", icon: NSImage.stopProgressTemplateName)
        }
    }
}

// MARK:- View controller
class ViewController: NSViewController {
    @IBOutlet var outline: NSOutlineView!
    
    var data: [Group]!
    override func viewDidLoad() {
        super.viewDidLoad()
        initData()
        // 将 floatsGroupRows 设置为false, 可以禁止第一行点击展开时往上浮动的现象
        outline.floatsGroupRows = false
        outline.dataSource = self
        outline.delegate = self
    }
    
    override func viewDidAppear() {
        // 在界面即将显示的时候展开所有标签组
        outline.expandItem(nil, expandChildren: true)
    }

    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }
    
    func initData() {
        // Favorites
        let favorites = Group(name: "Favorites")
        favorites.addChild(item: GroupItem(name: "Google", icon: NSImage.bookmarksTemplateName))
        favorites.addChild(item: GroupItem(name: "百度", icon: NSImage.quickLookTemplateName))
        favorites.addChild(item: GroupItem(name: "CSDN", icon: NSImage.refreshTemplateName))
        favorites.addChild(item: GroupItem(name: "Segmentfault", icon: NSImage.slideshowTemplateName))
        favorites.addChild(item: GroupItem(name: "Joel", icon: NSImage.listViewTemplateName))
        favorites.addChild(item: GroupItem(name: "C++", icon: NSImage.goForwardTemplateName))
        let users = Group(name: "名字")
        users.addChild(item: GroupItem(name: "谷歌", icon: NSImage.lockLockedTemplateName))
        users.addChild(item: GroupItem(name: "Baidu", icon: NSImage.lockUnlockedTemplateName))
        users.addChild(item: GroupItem(name: "Apple", icon: NSImage.enterFullScreenTemplateName))
        users.addChild(item: GroupItem(name: "小猫咪", icon: NSImage.exitFullScreenTemplateName))
        // Transfer list
        let tasks = Group(name: "Tags")
        tasks.addChild(item: GroupItem(name: "Available", icon: NSImage.statusAvailableName))
        tasks.addChild(item: GroupItem(name: "None", icon: NSImage.statusNoneName))
        tasks.addChild(item: GroupItem(name: "Partially Available", icon: NSImage.statusPartiallyAvailableName))
        tasks.addChild(item: GroupItem(name: "Unavailable", icon: NSImage.statusUnavailableName))
        //
        let empty = Group(name: "Empty")
        //
        self.data = [favorites, empty, users, tasks]
    }
}

// MARK:- View controller with NSOutlineViewDelegate
extension ViewController: NSOutlineViewDelegate {
    // 判断当前元素是否是一个组元素。
    // 如果返回false, 那么展开按钮是一个三角形并且在文字前面。
    // 如果返回true,那么展开按钮是一个悬浮才可见的纯文字按钮(仅有文字没有边框)并且在组标签的后面(靠近outline view 的右边界)
    func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool {
        Group.isInstance(any: item)
    }
    
    // 判断当前元素是否可以被选中。
    // 默认情况下是允许任意节点被选中的,我们希望表示分组的节点不可以被选中,只有子标签可以被选中。
    func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
        !Group.isInstance(any: item)
    }
    
    // 获取每一行的视图
    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
        var ret: NSView?
        if let group = item as? Group {
            let cell = outlineView.makeView(withIdentifier: .init("HeaderCell"), owner: self) as! NSTableCellView
            // 注意:此处我们不能改变 cell.textField,调用 cell.textField = myTextField是无效的。
            // 只能修改它的内容。
            cell.textField!.stringValue = group.name
            ret = cell
        } else if let groupItem = item as? GroupItem {
            let cell = outlineView.makeView(withIdentifier: .init("DataCell"), owner: self) as! NSTableCellView
            // 注意:此处我们不能改变 cell.textField,只能修改它的内容。
            cell.textField!.stringValue = groupItem.name
            // 同上,我们只能修改 imageView 里面的图片,调用 cell.imageView = myImageView是无效的
            cell.imageView?.image = NSImage(named: groupItem.icon)
            ret = cell
        }
        return ret
    }
}

// MARK:- View controller with NSOutlineViewDataSource
extension ViewController: NSOutlineViewDataSource {
    // 询问当前组有多少个子节点。
    // 值得注意的是 Outline 初始化时会调用这个方法,但是 item 是nill. 所以第一次调用的意思是询问有多少个分组。
    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if let group = item as? Group {
            return group.numberOfItems
        }
        return self.data.count
    }
    
    // 根据下标获取某个分组下面的子节点数据
    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if let group = item as? Group {
            return group.childAt(index: index)
        }
        return self.data[index]
    }
    
    // 询问当前节点是否可以被展开,会在某个标签进入可见状态时调用。
    // 如果返回 false 表示此行当前不可被展开,即不显示展开按钮。
    // 大多数情况下,即使当前组下面没有任何子标签我们也让他显示展开按钮
    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
       Group.isInstance(any: item)
    }
}