Fat Cat Software

Per-object ordered relationships using Core Data

Core Data is a great technology to allow easy creation of complex data models, saving you from writing a lot of boilerplate code. One of the limitations of Core Data, however, is that when one entity has a to-many relationship with another entity, the objects in that relationship are unordered. The only order that can be imposed on such a relationship is by sorting based on one or more attributes of the child objects, and there is no "inherent" ordering to the objects otherwise.Several different solutions (one example here) have been created for this problem, most of which use some variation of adding an "index" attribute to the child objects which specifies what order the objects come in (i.e. the first object has an index of 0, the next and index of 1, and so on). The tricky part come in keeping the index attributes up to date, but there are several solutions for that which all work pretty well.The major disadvantage to this approach comes when you have child objects which can be contained by more than one parent object. The classic example of this is a program like iTunes, where you can have multiple playlists, each of which can contain multiple tracks, and the user can arrange the tracks in any order they wish in each playlist. The problem is that there is only one "index" attribute for each track, but each track will have a different index depending on what playlist we're looking at.I decided to take a stab at this problem using a new approach, which stores the ordering information for each container object in the container itself, and not in the children. I also wanted this solution to be generic and reusable, so you don't have to reimplement everything for each relatonship you want to give a custom ordering to.The solution I came up with is a custom subclass of NSManagedObject called BWOrderedManagedObject, which provides support for imposing an ordering on any to-many relationship the object has. At the end of this post, you'll find a download link containing the source code for this class, as well as a corresponding BWOrderedArrayController class which provides support for sorting a table view by this custom ordering, and a simple demo showing how to hook everything up.BWOrderedManagedObjectIn order to keep track of what order the objects in a particular relationship are in, we need to store a separate piece of data that provides that ordering. The way BWOrderedManagedObject handles this is to store the ordering in a separate attribute. By default, the key used for this attribute is simply the relationship key with the string "Ordering" appended to it, so for example, if you have a "tracks" relationship, the ordering info for your tracks will be stored under the "tracksOrdering" key.The ordering attribute should be defined in your Core Data model as a transformable property attribute with the appropriate name. The attribute will actually consist of an NSArray holding NSURL objects, with each URL object containing the URIRepresentation of the NSManagedObject it represents. The URIRepresentation is obtained from the NSManagedObjectID for a given object, and is a simple, persistent identifier that can be used to identify the object and won't change even when saved to disk and read back again (with one important exception - see below). The order of the URLs in the array is what defines the order of the objects themselves in the relationship.To actually specify an ordering for your objects, NSManagedObject provides a set of methods that allow you to access, insert, delete, and move objects within the array (objectInOrderedValueForKey:, insertObject:inOrderedValueForKey:atIndex:, etc.). It also provides a mutableOrderedValueForKey: method (similar to Cocoa's mutableArrayValueForKey: method) which returns an NSMutableArray proxy object which you can use to rearrange objects in the relationship as though it were a plain array. To actually access the ordered values, just use the orderedValueForKey: method, which returns an NSArray containing the objects in the correct order.BWOrderedArrayControllerIn a typical NSTableView + NSArrayController setup, sorting is accomplished when the user clicks one of the table view's column headers. Each column has an NSSortDescriptor assigned to it, which are then passed to NSArrayController, which sorts its contents based on the sort descriptors. However, since sort descriptors function by comparing attributes of the objects themselves, this method of sorting won't work for BWOrderedManagedObject, because the sort order is stored in the container object, and not the objects being sortedBWOrderedArrayController fills this gap by handling the case where the user has clicked a column which represents the "natural" ordering of the table's content. To use BWOrderedArrayController, first bind its "contentSet" binding to your object's content just like you would with a normal array controller. Then, in your table view, select the column you want the user to be able to click to sort the content by their custom ordering.Instead of binding this column's "value" binding like your other table columns, instead switch to the attributes inspector pane and enter the name of the ordering key that your content's ordering is stored under in the "Sort Key" field. For example, if you're displaying a list of tracks that's stored in the "tracks" key of the selected playlist, you would enter "tracksOrdering" in the Sort Key field.If everything is hooked up correctly, clicking that table column will sort the content of the table based on the ordering specified by the user. Performing actual rearrangement of items via drag and drop is not implemented in BWOrderedArrayController itself, but in the project download, an example of how to implement reordering is given in the MusicLibraryDocument classInner workingsMost of the nitty gritty of how the code works is documented in comments in the code itself. The tricky parts were:Making sure that objects added via both the normal Core Data KVC methods and BWOrderedManagedObject's methods are both handled properlySupporting saving and loading of data, which is made tricky by the fact that newly created objects are assigned temporary IDs which need to be swapped out for permanent IDs when the data store is saved to disk.Working out the array controller bindings so that everything would "just work" when binding the contentSet of the controller.Using the codeThe code is freely available under the MIT license and can be downloaded from this link. The download consists of an example project which demonstrates a very very basic data model with two entities: Playlist, and Track. The Playlist entity has a to-many "tracks" relationship and a "tracksOrdering" property to contain the ordering information. The demo is a document based app that lets you create new playlists, drag files from the Finder into the track list to add them, sort the tracks in order, and rearrange the tracks in the list via drag and drop. The idea is loosely modeled after iTunes, but the demo doesn't actually filter out non-music files, or play music, or anything else useful. It is a demo, after all. :)The BWManagedObject class can be used as-is and has no dependencies, and BWOrderedArrayController only depends on BWManagedObject, so you should be able to just add them to your project and compile them (do make sure to link to CoreData.framework, of course).I have not yet tested this code extensively, so use at your own risk. In particular, I have no idea yet how it performs with larger data sets or other variations in configuration. I welcome all feedback and bug reports at bwebster@fatcatsoftware.com. If I get enough interest, I may even put the code up in a public VCS of some sort, for now I'll stick with the zip file.The code will only work on OS X 10.5 and later, the primary reason being that transformable attributes are only supported on Leopard, which is what the ordering attribute must be defined as. This is possible to do under Tiger as well, but requires a lot more code, which I didn't feel was worth the time to write. Apple outlines what needs to be done in their documentation though, so it should be possible to backport if you need it to work under Tiger. Most of the other code should be mostly Tiger compatible (it doesn't use new ObjC 2.0 features, for example), but may contain other Leopard gotchas.