NSTableView works with row/column or row/section identifiers while NSOutlineView works with 'item' identifiers.
Item identifiers are of the type AnyObject. Which is to say that they cannot be String's, Int's or similar things. Structs are out. If we do make the mistake of using -for example- String's then the effects are rather peculiar and the elements shown by the view will be 'all over the place'.
So use real object identifiers. And make sure that they are different for each element that you want to show. Don't use identifiers that are 'static'. (Unless of course you already know exactly what you are doing).
A picture, this is what I want to achieve:
By clicking on a value that value will become editable.
The Domain is implemented as a class and has the following structure:
class Domain {
var name: String = "domain-name.extension"
var wwwIncluded: Bool = true
var root: String = "root-folder"
var forwardUrl: String = ""
var enabled: Bool = false
}
I also used a domains managing class called Domains:
class Domains: SequenceType {
private var domains: Dictionary<String, Domain> = [:]
var count: Int {...}
func contains(domainName: String) -> Bool {...}
func domainForName(name: String) -> Domain? {...}
func add(domain: Domain) -> Bool {...}
func remove(name: String) -> Bool {...}
func update(name: String, withDomain new: Domain) -> Bool {...}
func updateWithDomains(domains: Domains) {...}
struct DomainGenerator: GeneratorType {...}
typealias Generator = DomainGenerator
func generate() -> Generator {...}
}
The view controller has to implement the following operations from the NSOutlineViewDataSource and NSOutlineViewDelegate protocols:
class MainWindowViewController: NSViewController {
var domains = Domains()
@IBOutlet weak var domainNameColumn: NSTableColumn!
@IBOutlet weak var domainValueColumn: NSTableColumn!
@IBOutlet weak var domainOutlineView: NSOutlineView!
}
extension MainWindowViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {...}
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {...}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {...}
// Using "Cell Based" content mode (specify this in IB)
func outlineView(outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject? {...}
func outlineView(outlineView: NSOutlineView, shouldEditTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> Bool {...}
func outlineView(outlineView: NSOutlineView, setObjectValue object: AnyObject?, forTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) {...}
}
Given that, the implementation of outlineView:numberOfChildrenOfItem can be simpel:
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if item == nil { return domains.count }
return Domain.nofContainedItems
}
class Domain {
static let nofContainedItems: Int = 4
var name: String {..}
There is NO NEED for this item to refer to the actual item that will be displayed!
Note that we should NEVER change the item identifier as long as the corresponding item is visible in the outline view!
I used bold capitals because this is crucial to understanding how the NSOutlineView works. Get this right, and everything else is easy. Frankly, I hope that Apple will change this in future releases and adopt a protocol for this. The current approach makes sense for Obj-C, but it simply makes no sense in Swift.
This is the implementation I used for outlineView:child:ofItem :
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
if item == nil {
for (i, d) in domains.enumerate() {
if i == index { return d }
}
return domains // should never happen
}
for d in domains {
if item === d {
if let result = d.itemForIndex(index) { return result }
// should never pass here
}
}
return "Index out of range error"
}
If the item is not nil, it will refer to a Domain at the top level. Hence it is looked up in the present Domains and it returns the identifier of a sub-item of the domain. Note that an operation has been implemented on the domain to return the actual value of the identifier. This way we can avoid including knowledge about the internals of the domain inside the controller. The implementation of the d.itemForIndex(index) is as follows:
extension Domain {
func itemForIndex(index: Int) -> AnyObject? {
switch index {
case 0: return wwwIncludedItemTitle
case 1: return enabledItemTitle
case 2: return rootItemTitle
case 3: return forwardUrlItemTitle
default: return nil
}
}
}
Note that it makes sense to implement this as an extension because it has nothing to do with the actual data in the domain.
Also note that there is no reference to any of the previously defined members of the domain. Instead new objects have been introduced:
class Domain {
let nameItemTitle: NSString = "Domain"
let wwwIncludedItemTitle: NSString = "Also map 'www' prefix:"
let rootItemTitle: NSString = "Root folder:"
let forwardUrlItemTitle: NSString = "Foreward to URL:"
let enabledItemTitle: NSString = "Enable Domain:"
The reason for this is that the members themselves can change over the lifetime of the domain because they are structs, not objects. But the identifier used by the NSOutlineView must remain the same over the lifetime over the GUI elements.
Since we also need some label text to display for each member, we can just as well use the same object for both. Normally you would probably want to make these definitions 'static'. Resist that urge here, remember that the identifier of the sub-items must be different for each domain. Yes, it will take up a little extra storage, but so what? don't start optimizing until its really necessary. Also note that the compiler is really clever, so introducing an in-between object will most likely be optimized away and you will wonder afterwards why the NSOutlineView does not work. So don't make these static!
With this infrastructure in place, the rest becomes easy:
To find out if an item is expandable:
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
guard let ditem = item as? Domain else { return false }
return domains.contains(ditem)
}
I.e. only the domains them self are expandable, not the sub-items.
The all-important content provider:
func outlineView(outlineView: NSOutlineView, objectValueForTableColumn tableColumn: NSTableColumn?, byItem item: AnyObject?) -> AnyObject? {
if tableColumn === domainNameColumn {
for d in domains {
if let title = d.titleForItem(item) { return title }
}
} else if tableColumn === domainValueColumn {
for d in domains {
if item === d { return "" }
if let value = d.valueForItem(item) { return value }
}
}
return nil
}
This operation returns the values that are actually displayed in the NSOutlineView. Note that we need to be able to differentiate between columns and do so by having an IBOutlet for each column. This way we can find out if we need to display the label or the actual value.
The d.titleForItem(item) and d.valueForItem(item) operations are also extensions on the Domain class:
func titleForItem(item: AnyObject?) -> NSString? {
if item === self { return name as NSString }
if item === wwwIncludedItemTitle { return wwwIncludedItemTitle }
if item === enabledItemTitle { return enabledItemTitle }
if item === rootItemTitle { return rootItemTitle }
if item === forwardUrlItemTitle { return forwardUrlItemTitle }
return nil
}
func valueForItem(item: AnyObject?) -> NSString? {
if item === self { return name as NSString }
if item === wwwIncludedItemTitle { return wwwIncluded.description as NSString }
if item === enabledItemTitle { return enabled.description as NSString }
if item === rootItemTitle { return root as NSString }
if item === forwardUrlItemTitle {
if forwardUrl.isEmpty { return "-" }
return forwardUrl as NSString
}
return nil
}
By now you should have no problem implementing outlineView:shouldEditTableColumn:item and outlineView:setObjectValue:forTableColumn:byItem yourself. They follow the same pattern as above.
Conclusion
NSOutlineView is not the easiest class to use, but once the trick with the item identifiers is understood it also does not pose a big problem anymore. The example above also shows how to isolate the know how of the data internals from the GUI. This makes updating the data structures much easier as all changes are made to the data structures locally (or in the extension) and the GUI will automatically follow the data as it evolves.
Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH
We don't get the world we wish for... we get the world we pay for.