NSArrayController.add - how to get the result

Hello,


I'm using NSTableView with binding to NSArrayController without CoreData. I succeed with everything - except add a new row - object.


I know, that with add "the result of this method is deferred until the next iteration of the runloop". But I do not find any way to get the result. For remove, it's no problem although it's results are deferred as well. After remove, I do nothing but the NSTableView shows the result immediately. With add, I tried a lot, including rearrange after a certain timeInterval. No success.


The only chance I have: I append new object to NSContellers content array directly and then call rearrange. But that's not what I want.


Do you have any ideas?


Kind regards


Wolfgang

Answered by QuinceyMorris in 113728022

One of the quirks of NSArrayController is that it mostly doesn't matter what the object is class is set to. It only really matters when it needs to create an object on your behalf, such as when you use its 'add:' method.


I'm not entirely sure what you need to enter in IB to get to recognize a nested, Swift class type. Looking at the 'objectClass' property after setting it in code might be misleading, because the property of type AnyClass, so it displayed representation is just a human-readable form of something else, and may not be anything you can use directly. One possibility would be to add @objc(MyObjectClassName) to your MyObject definition. I believe that makes the class available to Obj-C contexts as simply "MyObjectClassName", without any namespacing (module or containing type name) complications.


The other problem with 'add:', in general, is that it creates an object by invoking its 'init' method, so there is no way to specify parameters — and, since you likely have an array of these objects because you're putting them in a table view, you probably want to configure each object differently. So:


1. If you're using 'add:' as a convenience, don't bother. It usually ends up being inconvenient before you get much further.


2. If you're using 'add:' as an actual action method (e.g. from a "+" button in your UI), don't hestitate to ignore it, and write your own action method, which you put in your window controller subclass — which is a much better place for it than in the array controller. (The view controller would be even better, if you're using a storyboard, but you've shown code using a window controller, so I'll go with that.)


3. When you get the point of having created a new object and want to add it a data model, you can do it either of two ways:


3a. Using an outlet to the array controller in your window controller, invoke the array controller's 'addObject:' method.


3b. Add the object KVO compliantly to your data model's indexed collection property. This is your "myData", but I strongly encourage you to think of it as an indexed collection property, rather than as an array. The correct way to add a new object KVO-compliantly is via a mutable array proxy like this:


myData.mutableArrayValueForKey ("myData").append (myNewObject)


Because this is KVO compliant, the array controller will observe and act on the change, without any need to "rearrange" its contents explicitly.


Personally, I use 3b rather than 3a, because I hate having any references to the array controller in my code, wherever possible, because NSArrayController is a truly horrible class, useful as it is. (Note: 3b is exactly what 3a does on your behalf.) (Note: If you use 3a or 3b, you don't give a **** what the array controller thinks the object class is.)


If you follow this approach, you can create and configure your added object without needing to negotiate shark-infested array controller waters at all.


The reason [NSArrayController add:] defers its result to a later iteration of the main event loop is that it might need to display an error dialog, and it wants to do this as a sheet, which makes its actions asynchronous. If you have similar needs, you might also choose to code asynchronously like this, but if the logic of adding a new object is simpler, you don't have to bother. KVO compliance will get you the result you want.

You're using 'add:' — the action method in class NSArrayController. In that case, the "result" is not a thing, but a behavior — the table updates. (I guess that what you meant. Just checking.)


Step 1 is to check that you get to add: at all.


Step 2 is to think about the consequences of that method. It's going to create a new object — does that happen? It's then going to add that object to the data model that the array controller is bound to or connected. Is the object added to the data model?


NSArrayController does this adding KVO-compliantly — it uses [NSArray mutableArrayValueForKey:] to go through a compliant mutable proxy. Are you doing something to prevent this from propagating back to the table view?

Thanks for your answer,


I tried -add and -newObject & -addObject(newObject). In both cases the data model is not changed. I checked data array.count and it still keeps its value. So, I believe its not the table update thats missing.


Peculiar: the following sequence:


  1. NSArrayController.add(nil)
  2. dataArray.append(DataObject())
  3. NSArrayController.rearrange()


leads two TWO new entries i the table????

B.T.W: I published "ExtComments", an XCodePlugin to extract comments and documentation. In this project I had no problems to do it with

  • NSCollectionView instead of NSTableView
  • have dataArray in subclass of NSViewController instead of NSWindowController.


Any idea, if this influences the behaviour of NSArrayController?

First: stop flailing. Randomly trying different approaches just leads to more questions than answers, and muddies the waters.


Second: understand that NSArrayController works. It's been around for a long time. It's easy, in situations like this, to get into the mindset that it's "doing something weird". It's not.


You can't understand NSArrayController without understanding something about KVO (key-value observing). In the 3-step sequence you tried, of course you got two entries in the table, because that's what you asked for. In step 1, you asked the array controller to add a new object to your data model for you. In step 2, you added a new object yourself. But because you didn't do that KVO-compliantly, you needed step 3 to cause the UI to update.


Important: the array controller is not a container. It is a glue object (a "mediating controller", if you want a MVC name for it) that handles certain aspects of the relationship between a data model and a UI element such as a table view. It pretends that it contains objects, as a convenience to you.


Incidentally: you said something contradictory. You said that using 'add' didn't do anything to your data model. Then you said that it did. It's beginning sound like there's something odd or wrong in your data model, or in the way the array controller is connected (or bound) to it.

Sorry for confusing - I'm confused too :-(


After testing a lot, I'm sure it's a problem of the NSObjectController.objectClass.


The situation I have is this:


MyProject:

class MyDialog: NSWindowController {

static var myDialog = MyDialog(windowNibName: "MyDialog")


class MyObject: NSObject {

var ident : String = ""

var x : Double = 0

var y : Double = 0

}


@IBOutlet var myDataController: NSArrayController!

dynamic var myData : [MyObject] = []


override func windowDidLoad() {

super.windowDidLoad()

............


//I tried this, and it works!

myDataController.objectClass = MyObject.self

let aclass = myDataController.objectClass //shows "MyProject.MyDialog.MyObject"


//doing it without setting objectClass like

//myDataController.objectClass = MyObject.self

let aclass = myDataController.objectClass //no error is reported but window is not loaded

// In IB ArrayController Class Name is set to MyProject.MyDialog.MyObject or MyObect - does not matter.

}

}

Accepted Answer

One of the quirks of NSArrayController is that it mostly doesn't matter what the object is class is set to. It only really matters when it needs to create an object on your behalf, such as when you use its 'add:' method.


I'm not entirely sure what you need to enter in IB to get to recognize a nested, Swift class type. Looking at the 'objectClass' property after setting it in code might be misleading, because the property of type AnyClass, so it displayed representation is just a human-readable form of something else, and may not be anything you can use directly. One possibility would be to add @objc(MyObjectClassName) to your MyObject definition. I believe that makes the class available to Obj-C contexts as simply "MyObjectClassName", without any namespacing (module or containing type name) complications.


The other problem with 'add:', in general, is that it creates an object by invoking its 'init' method, so there is no way to specify parameters — and, since you likely have an array of these objects because you're putting them in a table view, you probably want to configure each object differently. So:


1. If you're using 'add:' as a convenience, don't bother. It usually ends up being inconvenient before you get much further.


2. If you're using 'add:' as an actual action method (e.g. from a "+" button in your UI), don't hestitate to ignore it, and write your own action method, which you put in your window controller subclass — which is a much better place for it than in the array controller. (The view controller would be even better, if you're using a storyboard, but you've shown code using a window controller, so I'll go with that.)


3. When you get the point of having created a new object and want to add it a data model, you can do it either of two ways:


3a. Using an outlet to the array controller in your window controller, invoke the array controller's 'addObject:' method.


3b. Add the object KVO compliantly to your data model's indexed collection property. This is your "myData", but I strongly encourage you to think of it as an indexed collection property, rather than as an array. The correct way to add a new object KVO-compliantly is via a mutable array proxy like this:


myData.mutableArrayValueForKey ("myData").append (myNewObject)


Because this is KVO compliant, the array controller will observe and act on the change, without any need to "rearrange" its contents explicitly.


Personally, I use 3b rather than 3a, because I hate having any references to the array controller in my code, wherever possible, because NSArrayController is a truly horrible class, useful as it is. (Note: 3b is exactly what 3a does on your behalf.) (Note: If you use 3a or 3b, you don't give a **** what the array controller thinks the object class is.)


If you follow this approach, you can create and configure your added object without needing to negotiate shark-infested array controller waters at all.


The reason [NSArrayController add:] defers its result to a later iteration of the main event loop is that it might need to display an error dialog, and it wants to do this as a sheet, which makes its actions asynchronous. If you have similar needs, you might also choose to code asynchronously like this, but if the logic of adding a new object is simpler, you don't have to bother. KVO compliance will get you the result you want.

NSArrayController.add - how to get the result
 
 
Q