Re: Getting started with Cocoa Bindings in My Project?
Re: Getting started with Cocoa Bindings in My Project?
- Subject: Re: Getting started with Cocoa Bindings in My Project?
- From: Alex Hall <email@hidden>
- Date: Mon, 07 Sep 2015 01:01:34 -0400
Thanks for the help, both you and Charles. I want to try to summarize the procedure as I now understand it, to make sure I have it all straight. No sense going and doing a bunch of work if I'm still missing something vital or don't have it right.
My first step is to make a KVO-compliant backing store, because Swift arrays won't do the job. As you said, my goal of updating an array and having my table automagically update itself to match the array just won't happen. What I need is a class that implements the methods described in the link you provided,so that bindings will work correctly. Obviously I can use an array internally, but the interface of the class *must* offer the necessary methods. I'll call this new class MyKVOCClass, because I'm terrible at naming things.
Next, remember that my model is a dictionary of arrays right now. The model has a single variable that can change to point at any of the arrays in the dictionary; the table is to display only that selected array (the table is view-based, one column, no multi-selecting, no editing). As the currently selected array changes, the table should update. I'll change this to be a dictionary of MyKVOCClass objects instead, so that when the array inside the selected object updates, the bound controller knows about it and can take action. Thus, instead of my model being [String:[MyObject]], it will be [String:MyKVOCClass]. The class will handle subscripting like an array, plus implement all the KVO stuff.
Next is the problem of accessing the current MyKVOCClass object from the view controller, where my model is instantiated. I'll need a computed property for this, which I'll call currentModelArray. This should return a reference to the currently active array in my model. You both suggested I implement
private func keyPathsForValuesAffectingCurrentModelArray() -> Set<String> {}
in addition, so that as I change the value of the currently selected object in my model, the controller is notified. At least, that's how I understand the purpose of this method. The set this method returns contains strings; are those all the possible values the computed property can ever hold, or are they something else entirely? I'm just not clear on which strings to provide here--what you referred to as "input properties".
Alright, we have a KVO-compliant storage system in my model, and we have a properly configured computed property that will tell the controller (here referring to the ArrayController) where in my model to look. Time to connect things.
First, I'd look at the bindings inspector for the array controller. I'd set it to use my view controller, because it likes to default to Shared User Defaults Controller. Then I'd set the key to be the computed property I made earlier. Sorry if my terms aren't quite right; I've been working on other aspects of this project, so have removed the ArrayController from it for the moment and haven't yet put it back in, because things break when I do that. :)
Next I'd look at the bindings for my table. I'd want to set the controller to my ArrayController, I don't know the specific words I'd use for keys here, but I'd *not* use any of my own variables, correct? This is all properties of the controller itself. This lets the controller figure out things like how many rows there are, what's selected, and so on.
Finally, repeat the previous step, but for the table cell. As before, I'm now using all properties of the controller, nothing *I* made anywhere. This uses the methods in my MyKVOCClass to get the actual data, then auto-generates the cells as needed. I'm not sure how this would work for a cell that's more than a text field, but that's a different topic, I guess. :)
That's how I understand the whole procedure. I thought it would be easier to just say what I've got then to try to keep replying in-line. Thanks again for the responses!
> On Sep 6, 2015, at 14:14, Ken Thomases <email@hidden> wrote:
>
> On Sep 6, 2015, at 10:59 AM, Alex Hall <email@hidden> wrote:
>
>> Since I'm using arrays, albeit retrieved from a dictionary, I thought an NSArrayController would do the job. I'm not sure what to enter for the key path, though. My view controller has a reference to both my table and my data model object, so I thought I'd give it the current array. I tried making the key path dataModel.myDict[datamodel.currentArray], but Xcode informed me that "myDict[dataModel" is not a valid key. Why it pulled just that part of what I'd actually entered, I'm not sure.
>
> Bindings is built on top of key-value coding (KVC) and key-value observing (KVO). The model key path that a binding is bound to has to be, of course, a key path. A key path is a series of keys separated by periods. So, "dataModel.myDict[datamodel.currentArray]" looks like a path consisting of three keys, "dataModel", "myDict[datamodel", and "currentArray]". Obviously, that's not right.
>
> KVC can't do a two-stage lookup. You effectively wanted it to parse datamodel.currentArray as a key path and get is value and then use that value as a key in a second key path, dataModel.myDict.<key from the first stage>. It doesn't do that.
>
> KVC can traverse dictionaries, basically by treating them like an object with properties where the keys of the dictionary as the names of the properties. It does not support the bracket syntax you tried to use and it does not support getting the key from a subexpression.
>
>
>> I then made a computed property in my view controller, so I could give the array controller a single-level property name but still get at the desired array.
>
> A computed property can work, but you need to do extra work to make it KVO-compliant. Basically, KVO needs to be told what the computed property is based on so that when those underlying properties change it can emit change notifications for the computed property, too.
>
> The easiest approach is to provided a class method named keyPathsForValuesAffecting<NameOfComputedProperty> which returns a set of key paths for the input properties. In Objective-C, this might look like:
>
> + (NSSet*) keyPathsForValuesAffectingMyComputedProperty
> {
> return [NSSet setWithObjects:@"inputProperty1", "otherInputProperty", nil];
> }
>
> In Swift, I expect this needs to be "dynamic", too.
>
>> That failed too, but with slightly different error messages. This time, I saw
>> [Swift._SwiftDeferredNSArray persistentStoreCoordinator]: unrecognized selector sent to instance 0x60800003ddc0
>
> Could your array controller be configured in Entity mode (for Core Data) rather than Object mode?
>
>
>> * I'm using Swift. Will that be a problem? I made both the computed property in my view controller and the dictionary of lists in my model "dynamic", and I made the object class of which my array's contents are instances subclass NSObject. Is there anything else I should do or know specific to Swift?
>
> I'm not an expert, but I don't think there's anything else you need to do.
>
>> * MVC I get, but why are my binding choices for my NSArrayController only controllers themselves? That is, why do I need to hook up my controller to my model *through another controller*? Should I not just give the NSArrayController my model directly?
>
> Array controllers are what Apple calls "mediating controllers". View, window, and app controllers are "coordinating controllers". Surely, it's natural for the model to be owned by a controller. I mean, something is holding a strong reference to it. Anyway, it's not a mediating controller's job to own the model, only to mediate between the view and another controller.
>
> It is technically possible to make an array controller manage an array that it owns, but I don't see it as a good idea. Why does it bother you?
>
>> * When binding my NSTableView to my array controller, what, exactly, do I use for keys for both the table as a whole and the table cell? Again, I only have one column, if that matters.
>
> From what you wrote below, I guess you're using a view-based table view rather than a cell-based one. The way you set up bindings differs for the two cases.
>
> For view-based tables, you bind the table view's Content binding to the array controller, controller key path "arrangedObjects". You would not typically specify a model key path here, but you could if the table was not representing the elements of that array itself but rather some sub-property of the elements. Usually, though, the table represents the elements and individual cell views select a property to show. You bind the table view's Selection Indexes to the array controller, controller key path "selectionIndexes", no model key path. You bind the table view's Sort Descriptors to the array controller, controller key path "sortDescriptors", no model key path.
>
> The table view uses the elements it gets via the Content binding to set the objectValue for each cell view, if the cell view has a setter for an objectValue property. NSTableCellView does. So do NSControls such as NSButton and NSTextField.
>
> For NSTableCellView, though, setting objectValue doesn't _directly_ accomplish much. That class doesn't do anything with its objectValue, it just holds a strong reference to it. So, you would typically bind its subviews to it, with a model key path going through objectValue. For example, if you have an NSTextField as a subview of your NSTableCellView, you would bind the text field's Value binding to the containing cell view, model key path "objectValue.<some property of the array element>".
>
> For a cell-based table view, you don't bind the table view's bindings. Rather, you bind the columns' Value binding to the array controller, controller key path "arrangedObjects". The model key path would be whatever property of a given array element should be displayed in that column's cell.
>
>> * When and if I get all this working, do i still need my data source/delegate?
>
> The data source and the delegate are two different roles.
>
> Bindings replace the central functionality of the data source: -numberOfRowsInTableView: and -tableView:objectValueForTableColumn:row:. However, the data source still has a role for copy/paste and drag-and-drop, if you support those.
>
> Bindings does not replace as much of the delegate responsibility, but see below.
>
>> It sounds like bindings replace that, but if so, where do my table cells get generated? Right now I use that function to set the textField.stringValue of the cell to the correct item in the current array; where would that happen in a bindings context?
>
> I'm guessing you're talking about -tableView:viewForTableColumn:row:, which I guess also means you're using a view-based table.
>
> Strictly speaking, it's never necessary to use that method to populate the cell views with values. It can be used for that purpose, but the -tableView:objectValueForTableColumn:row: can be sufficient depending on your cell views.
>
> Anyway, you don't need this with bindings. The table view bindings take care of getting objectValues for the cell views and intra-cell-view bindings take care of distributing that to the subviews.
>
> (You might still need a -tableView:viewForTableColumn:row: method if your cell view identifiers don't match the identifiers of their respective columns. If they do match and the method would amount to nothing more than returning the result of calling -makeViewWithIdentifier:owner: on the table view, you can eliminate the method entirely.)
>
>
>> * Assuming for a moment that my key is correct, and I want to bind to the array currently in use by the model, how would I do that?
>
> The computed property discussed earlier should be fine.
>
>> A dictionary lookup returns an optional, so for the moment--just to get things going--I'm forcing it to unwrap. Ideally, though, I'd include some way of handling a nil. How do bindings work with optionals?
>
> The same way they work with object pointers which may be nil in Objective-C, which is to say mostly fine. Some bindings have options that can control how they handle nil, but generally they do the sensible thing. If the array controller's Content Array binding is bound to a property that's nil, then the array controller will just have empty content. Likewise, a table view would just be empty. Etc.
>
>
>> At the end of the day, I want what bindings promises: my model updates, and each time it does (a row changes, is added, or is removed) my table updates to reflect that change.
>
> Note that bindings only promises that for KVO-compliant model updates. In particular, you can't just mutate a mutable array and hope to have those changes reflected automatically. Mutable arrays are not themselves KVO-compliant. Objects may be KVO-compliant for their properties. Objects are not KVO-compliant in and of themselves. Likewise, property values or backing storage are not KVO-compliant in and of themselves. KVO hooks into the object, monitoring calls to setters and mutating accessors for the specific properties being observed. So, all modifications of such properties must be performed via calls to such setters and mutating accessors on the object which has the property. Direct manipulations of the backing storage for a property are not visible to KVO.
>
> In particular, for an indexed collection (which may or may not be represented using an NSArray or NSMutableArray), you must always modify it by either wholesale replacing it using its setter or mutate it using the indexed collection mutation accessors.
> <https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/AccessorConventions.html#//apple_ref/doc/uid/20002174-SW4>
>
>> I'm using reloadData(), but I thought bindings would be far more elegant, extensible, and simple.
>
> Personally, I do generally find bindings to be the best way to connect my views to my model. But there are limitations to bindings and when you bump into them, feel free to switch back to other techniques.
>
> Regards,
> Ken
>
--
Have a great day,
Alex Hall
email@hidden
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden