Trombone0904
Trombone0904

Reputation: 4258

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?

Upvotes: 2

Views: 1359

Answers (2)

dirtydanee
dirtydanee

Reputation: 6151

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

Upvotes: 1

Willeke
Willeke

Reputation: 15589

If you want to display hierarchical data, the data can't be a simple array of strings. Instead of item strings, use dictionaries or custom objects which have title/name and children properties. The children property is an array of child items.

func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any returns the children of item. If item is "Item 1" and index is 1, the return value is "Sub Item 1.2".

func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int returns the number of children of ``. If item is "Item 1" , the return value is 2.

Upvotes: 0

Related Questions