Search code examples
swift3xcode8nsoutlineview

Swift 3 - NSOutlineView with expandable items


i would like to realize an outlineView, which shows string values as root values. the following code works for me:

import Cocoa


class TestController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {


    @IBOutlet weak var outlineView: NSOutlineView!

var items: [String] = ["Item 1", "Item 2", "Item 3", "Item 4","Item 5"]

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        return items[index]
    }


    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        return true
    }


    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if item == nil {
         return items.count
        }
        return 0
    }


    func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {

        let v = outlineView.make(withIdentifier: "HeaderCell", owner: self) as! NSTableCellView
        if let tf = v.textField {
            tf.stringValue = item as! String
        }
        return v
    }

}

This is the result:

enter image description here

but i don't know, how i can assign different string values for Item 1 (for example). i wish to realize something like that:

+Item 1
++Sub Item 1.1
++Sub Item 1.2

+Item 2
++Sub Item 2.1

+Item 3
++Sub Item 3.1
++Sub Item 3.2
++Sub Item 3.3

...

can somebody help me?


Solution

  • A simple String array will not be too much of use here, you need a dictionary at least to display sub items. I would recommend to introduce a little helper model, lets call it Item. It holds a name, and a number of children Items, those will be the sub-items.

    struct Item {
        let name: String
        var childrens: [Item] = []
    
    /// Create a helper function to recursivly create items
    ///
    /// - Parameters:
    ///   - parents: number of parent items 
    ///   - childrens: number of children for each parent item
    /// - Returns: array of Item
    static func itemsForNumberOf(parents: Int, childrens: Int) -> [Item] {
        var items: [Item] = []
        for parentID in 1...parents {
            var parent = Item(name: "Index \(parentID)",childrens: [])
            for childrenID in 1...childrens {
                let children = Item(name: "Index \(parentID).\(childrenID)",childrens: [])
                parent.childrens.append(children)
            }
            items.append(parent)
        }
        return items
    }
    

    }

    Declare a property on your viewController called items, and return some Item's using the itemsForNumberOf helper function.

    class TestController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {
    
        let items: [Item] = {
            return Item.itemsForNumberOf(parents: 5, childrens: 3)
        }()
    
        @IBOutlet weak var outlineView: NSOutlineView!
    }
    

    In TestController override the viewDidLoad() function and assign the delegate and datasource to your viewController.

    override func viewDidLoad() {
        super.viewDidLoad()
        self.outlineView.delegate = self
        self.outlineView.dataSource = self
    }
    

    Check the documentation of NSOutlineViewDataSource and in specific this API

    func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
        if item == nil {
            return items[index]
        }
    
        if let item = item as? Item, item.childrens.count > index {
                return item.childrens[index] 
        }
    
        return ""
    }
    

    Return expandalbe property based on children of the received Item.
    If it is empty, no children -> not expandable
    If it is not empty, has children -> expandable

    func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
        guard let item = item as? Item else {
            return false
        }
    
        return !item.childrens.isEmpty
    }
    

    Same for returning the sub items count by using childrens property again

    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if item == nil {
            return items.count
        }
    
        if let item = item as? Item {
            return item.childrens.count
        }
    
        return 0
    }
    

    In your viewFor function, you need to make sure to return nil to everything, what is not a Item type asking for a view.

        func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
    
            guard let item = item as? Item else {
                return nil
            }
    
            let v = outlineView.make(withIdentifier: "HeaderCell", owner: self) as! NSTableCellView
            if let tf = v.textField {
                tf.stringValue = item.name
            }
            return v
        }
    }
    

    And you should end up with something like this:
    enter image description here