Sez
Sez

Reputation: 1293

Cocoa OS X Bindings and Non-trivial data model

This project is to create an editor tool in Cocoa & Swift for Mac OS X that will edit a non-trivial data structure. A pared down schema looks like this:

Game
    title : String
    [ Room ]

Room
    roomKey : String
    roomName : String
    roomDescription : String
    [ Object ]
    [ Exit ]

Object
    objectDescription: String

Exit 
    destinationRoomKey : String
    sourceRoomKey : String

The current implementation - the third go-around - has a single Document.xib file (the app is document based) and in that I'm hooking up a NSObjectController to the base game object loaded by the document, and an NSArrayController to the game objects array of rooms. There's an NSObjectController for the Room. I have not done the objects or exits yet.

The views are handled by a base root view controller, which swaps sub-views in and out as you go up and down the view hierarchy. On the view for the root game state, you click an "edit" button that slides in the table view for the list of rooms. Clicking a button in one of the room rows slides in a room detail view which has its own controller.

This is all working well enough. I have hooked up the object controller of the room so that it gets the selected room of the rooms array as its object, using Interface Builder bindings. I can do this because I have all the views, view controllers and data model controllers in the one XIB file.

However: now I am adding the game objects to this mix and the XIB file is getting very unwieldy. I really feel like I want to do this in separate XIB files, but when I tried that previously I was not able to hook up the controllers to each other. I tried manually writing code to load & save the data at the same time as the controllers had their view displayed and removed but this was flakey and error prone. So far the most elegant and robust result I have had is with this one XIB approach.

I looked at the programmatic API for binding but could not understand how to get it to work, or how to discover what the key path would look like. I suppose if it was possible to do the bindings programatically you could put the different parts in different XIB's and do the bindings at load time. But I could not find any examples of anyone doing that successfully and it seemed a road to madness.

At present I'm having no problems with Swift and its relations to Cocoa and Objective-C so if anyone has answers in Objective-C or Swift I'd be happy to hear them. I have not put Swift as a key word for this question as its not part of the problem.

I've seen the StackOverflow answer about hierarchical models, and its what I'm currently doing, so it doesn't help. The problem is that this approach gets unwieldy when there's several layers of master-detail.

I've also seen the StackOverflow answer about sharing controllers, and it was what I tried before and where I ran into the issue described there, that if you specify a controller object in a NIB it will get instantiated as an independent object. Hence why I have the huge-mega-NIB-of-death approach at present.

I could make the title of this question "cannot make programmatic bindings work" but I'm not sure that that is the right approach anyway.

Surely someone has done the job of making a non-trivial data model work with Cocoa before?

Upvotes: 0

Views: 267

Answers (1)

Ken Thomases
Ken Thomases

Reputation: 90641

Your secondary NIBs should be view NIBs, their owners would be an instance of NSViewController or a custom subclass. That has a representedObject property. The NIB and its view controller class should be thought of as stand-alone, theoretically-reusable components. That is, in theory, that NIB could be used in multiple contexts to represent a particular kind of object. So, you should typically not want connections to other parts of your UI or their controllers, other than knowing what object this view is being loaded to represent.

Within the NIB, you can either bind to the File's Owner with a model key path that goes through representedObject or add an NSObjectController that binds to File's Owner's representedObject and then bind your views through that with controller key selection.

When you load such a secondary NIB, you would have to set its representedObject to the object it's supposed to represent, taken from the array controller's selection. This should be done in code, presumably the same code that decides it needs to load the NIB and does so.

If the design of your UI is such that a detail view needs to trigger a behavior that's best handled at a higher level — for example, a Room view needs to arrange for an Exit view be slid into the window, but not as a subview of its own view — the detail view controller should define a delegate protocol and implement a delegate property. For example, the Room view controller's delegate protocol might have a method -roomViewDidChangeSelectedExit:. The Room view controller would call that on its delegate, passing self. You would set some coordinating controller (perhaps the window controller) as the the detail view's delegate.


It's not clear to me if the "detail" views and the "master" views are visible simultaneously. That is, can the user change the object that the detail view is meant to show without backing up first? If so, there are a couple of approaches.

You could set up the bindings programmatically when the view is loaded. This would be the responsibility of the controller that loaded the detail view. It's not the responsibility of the detail view's controller. That doesn't have the higher-level perspective and knowledge to set up the binding. Anyway, you could do it like:

[detailViewController bind:@"representedObject" toObject:self.arrayController withKeyPath:@"selectedObjects.firstObject" options:@{ }];

Be sure to call -unbind: before the detail view controller is released.

The other way to do it is to simply observe the changed selection using a non-Bindings approach, and set the new representedObject in the code that gets triggered. For example, if your master view lets the user select an item in a table view, you would set up the table view's delegate (almost certainly already done) and implement -tableViewSelectionDidChange:. In that delegate method, query the newly-selected item and assign it to detailViewController.representedObject.

Upvotes: 1

Related Questions