Using NSTreeController with NSOutlineView

I am trying (unsuccessfully) to build an NSOutlineView running a TreeController. I went through a bunch of tutorials, but they all preload the data before starting anything, and that won't work for me.

I have a simple class for a device:

import Cocoa class Device: NSObject { let name : String var children = [Service]() var serviceNo = 1 var count = 0 init(name: String){ self.name = name } func addService(serviceName: String){ let serv = "\(serviceName) # \(serviceNo)" children.append(Service(name: serv)) serviceNo += 1 count = children.count } func isLeaf() -> Bool { return children.count < 1 } } 

I also have an even simpler class for "Service":

 import Cocoa class Service: NSObject { let name: String init(name: String){ self.name = name } } 

Finally, I have a ViewController:

 class ViewController: NSViewController { var stepper = 0 dynamic var devices = [Device]() override func viewDidLoad() { super.viewDidLoad() } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBAction func addDeviceAction(_ sender: Any) { let str = "New Device #\(stepper)" devices.append(Device(name: str)) stepper += 1 print("Added Device: \(devices[devices.count-1].name)") } @IBAction func addService(_ sender: Any) { for i in 0..<devices.count { devices[i].addService(serviceName: "New Service") } } } 

Obviously, I have two buttons, one of which adds a “device” and the other a “service” for each device.

What I cannot do is any of this data in NSOutlineView. I set the Control property of the TreeController object to: Class and Class: Device mode, and without setting the Children, Count or Leaf properties, I get (predictably):

2017-01-04 17: 20: 19.337129 OutlineTest [12550: 1536405] Attention: [object class: device] childrenKeyPath cannot be nil. To resolve this log message, set the childrenKeyPath attribute in Interface Builder

If I set the Children property to "children", everything will be very bad:

2017-01-04 17: 23: 11.150627 OutlineTest [12695: 1548039] [General] [addObserver: forKeyPath: options: context:] is not supported. The path to the key: children

All I'm trying to do is configure NSOutlineView to enter data from the NSTreeController so that when a new “device” is added to the device array [], it displays as a structure.

If anyone could point me in the right direction, I would be very grateful.

Many thanks to Warren for his extremely useful work. I have a (mostly) job. A few things I also needed to do besides Warren's suggestions:

Install Data Warehouse for TreeController Define the data store for the tree controller

Snap to TreeView Link OutlineView to TreeController

Binding a column to a TreeController Associate a column with a TreeController

Bind a Table View to a table cell view Bind a TableView element to a table cell view (yes, really)

As soon as all that was done, I had to play a little with the actual data store:

  var name = "Bluetooth Devices Root" var deviceStore = [Device]() @IBOutlet var treeController: NSTreeController! @IBOutlet weak var outlineView: NSOutlineView! override func viewDidLoad() { super.viewDidLoad() deviceStore.append(Device(name: "Bluetooth Devices")) self.treeController.content = self } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBAction func addDeviceAction(_ sender: Any) { if(deviceStore[0].name == "Bluetooth Devices"){ deviceStore.remove(at: 0) } 

It turns out that the root cannot be without children at the beginning, at least as far as I can tell. When I add the child, I can delete the value of the owner of the place, and the tree seems to work (basically) the way I want. Another thing is that I have to reload the data and re-display the outline every time the data changes:

  outlineView.reloadData() outlineView.setNeedsDisplay() 

Nothing without it. I still don't have the correct data update (see comments below Warren's answer), but I'm almost there.

+5
source share
1 answer

To indicate the obvious, a NSTreeController manages a tree of objects, all of which should answer the following three questions / queries.

  • Are you a leaf, you have no children? = leafKeyPath
  • If you are not a leaf, how many children do you have? = countKeyPath
  • Give me your children! = childrenKeyPath

It is easy to install in IB or programmatically. A fairly standard set of properties, respectively.

  • isLeaf
  • childCount
  • children

But its completely arbitrary and can be any set of properties that answer these questions.

I usually configure a protocol named TreeNode and make all its objects correspond to it.

 @objc protocol TreeNode:class { var isLeaf:Bool { get } var childCount:Int { get } var children:[TreeNode] { get } } 

For your Device object, you answer 2 questions 3 with isLeaf and children , but you do not answer the childCount question.

The children of your device are Service objects, and they do not respond to any of the reasons why you receive exceptions.

So, to fix your code, maybe a solution ...

Service object

 class Service: NSObject, TreeNode { let name: String init(name: String){ self.name = name } var isLeaf:Bool { return true } var childCount:Int { return 0 } var children:[TreeNode] { return [] } } 

Device object

 class Device: NSObject, TreeNode { let name : String var serviceStore = [Service]() init(name: String){ self.name = name } var isLeaf:Bool { return serviceStore.isEmpty } var childCount:Int { return serviceStore.count } var children:[TreeNode] { return serviceStore } } 

And the terrible thing to do from the point of view of MVC, but convenient for this answer. The root object.

 class ViewController: NSViewController, TreeNode { var deviceStore = [Device]() var name = "Henry" //whatever you want to name your root var isLeaf:Bool { return deviceStore.isEmpty } var childCount:Int { return deviceStore.count } var children:[TreeNode] { return deviceStore } } 

So, all you have to do is set the contents of your treeController. Suppose you have an IBOutlet for it in your ViewController .

 class ViewController: NSViewController, TreeNode { @IBOutlet var treeController:NSTreeController! @IBOutlet var outlineView:NSOutlineView! override func viewDidLoad() { super.viewDidLoad() treeController.content = self } 

Now every time you add a device or add a Service, just call reloadItem on an outlineView (which also needs to exit)

 @IBAction func addDeviceAction(_ sender: Any) { let str = "New Device #\(stepper)" devices.append(Device(name: str)) stepper += 1 print("Added Device: \(devices[devices.count-1].name)") outlineView.reloadItem(self, reloadChildren: true) } 

Here is the basic information and you should start, but the documents for NSOutlineView and NSTreeController have a lot more information.

EDIT

In addition to the material above, you need to bind your outline view to your tree controller.

First make sure Outline View is in view mode.

place diagram in view mode

Then bind the table column to builtObjects on the tree controller.

bind column to tree controller

The latter binds the text cell to the corresponding key path. In your case, this is the name . objectValue is a reference to your object in the cell.

bind data to cell

+5
source

Source: https://habr.com/ru/post/1262320/


All Articles