views:

422

answers:

2

Background

I am creating a Source List for my application and I want it structured in a way similar to that of iTunes, with two types of items:

  • "Fixed" items – these do not change and can't be moved around – at the top
  • Editable items underneath, which can be changed by the user – moved around, renamed etc (in the iTunes example, like Playlists and Smart Playlists)

In my iTunes analogy:

iTunes Source List

The way I've structured my data so far is as follows:

  • The items which I want to be editable are "group" items, in the form of Group Core Data entities.
  • Each item in the Source List is represented as a SourceListItem regular Objective-C object so that I can associate each item with a title, icon, child items etc
  • The fixed items are currently represented by SourceListItem instances, stored in an array in my controller object.

The Question

I am unsure of how to amalgamate these two types of item into the Source List, so that the fixed items are at the top and always there and do not change, and the editable items are at the bottom and can be moved around and edited.

These are the ideas I've come up with so far:

  • Add the fixed items to the Core Data model. This means that I can create an entity to represent Source List items and have my fixed and editable items placed in instances of these. Then these can be bound to the Outline View table column with an Array/Tree Controller. However, this means that I'd have to create a new entity to represent the Source List items, and then sync the Groups with this. I'd also have to have some way of creating all the fixed items only once, and if something happened to any of the persistent store files then the fixed items would not be displayed.

  • Merge the fixed items with the group items. Whilst both are stored in separate arrays, this could be done in the controller for my window when the Outline View requests the data (if adopting the NSOutlineViewDataSource protocol, not bindings). However this means that I'd have to create new SourceListItems for each group in the array controller (to associate each with icons and other attributes), store these and then watch the group array controller for changes to remove, add or modify the SourceListItem instances when changes are made to the groups.

Does anyone have any better ideas on how I can implement this?

I would like my application to be compatible with OS X v10.5 so I'd prefer any solutions that didn't depend on having Snow Leopard installed.

+1  A: 

Don't inject nonsense into your data set simply to support a view. This not only goes against the MVC design pattern, but adds needless complexity (ie "more potential for bugs") to the single most important part: management of user data.

That said, using Bindings with this particular scenario is what's causing so much friction. Why not eschew Bindings entirely? You're on the right track, I think, using the NSOutlineViewDataSource protocol, but you didn't take it far enough. Instead, rely fully on this (still perfectly valid and in some ways superior) protocol.

You'd essentially trade ease-of-setup (and ease of change notification) for full control over the tree structure.

Joshua Nozzi
Yes I think you're right; I was just thinking of bindings as one of the potential ways of doing it.I did prefer the `NSOutlineViewDataSource` protocol idea, but something seems a little hacky about just botching the data together like that, and as I said it also means I need to watch for changes in the Core Data `Group` instances to reflect those in the array of `SourceListItem`s
Perspx
There's nothing hacky about this at all. Before Mac OS X 10.3, that was the only way to do it (in Cocoa). It's also not necessary to lump everything together as one structure. Your groups are the root items. Each group's children is an array of results. No problem, no mess, no guilt. :-)
Joshua Nozzi
+3  A: 

I'm working on an app that has this exact same behavior, and here's how I'm doing it:

I have 5 main entities in my Core Data Model:

  1. AbstractItem - an abstract Entity that has the attributes common to all items, like name, weight, and editable. Also has two relationships: parent (to-one relationship to AbstractItem) and children (to-many relationship to AbstractItem, and the inverse of parent).
  2. Group - concrete child Entity of AbstractItem.
  3. Folder - concrete child Entity of AbstractItem. Adds a many-to-many relationship to the basic Item entity.
  4. SmartFolder - concrete child Entity of Folder. Adds a binary attribute predicateData. Overrides Folder's "items" relationship accessor to return the results of executing a fetch request with the predicate defined by the predicateData attribute.
  5. DefaultFolder - concrete child Entity of SmartFolder. Adds a string attribute identifier.

For the "Library" section items, I insert DefaultFolder objects and give them a unique identifier so I can retrieve them easily and differentiate between them. I also give them an NSPredicate that corresponds to what Items they're supposed to show. For example, the "Music" DefaultFolder would have a predicate to retrieve all Music items, the "Podcasts" DefaultFolder would have a predicate to retrieve all Podcast items, etc.

The root-level items ("Library", "Shared", "Store", "Genius", etc) are all Group items with a nil parent. The groups and Folders that cannot be edited have their editable attribute set to NO.

As for actually getting this stuff in your outlineView, you'll have to implement the NSOutlineViewDataSource and NSOutlineViewDelegate protocols yourself. There's just too much behavioral complexity here to pump it out through an NSTreeController. However, in my app, I got all of the behavior in (even drag-and-drop) in under 200 lines of code (so it's not that bad).

Dave DeLong
Great answer, thanks. However my main concern with the purely Core Data route was about creation of the "fixed" items (in your case the Library items) that are always going to be there – do you just test whether they are already in the model when it is loaded and create them if not?
Perspx
@Perspx - since I give them unique identifiers (`@"Music"`, `@"Podcasts"`, etc), I can just fetch all `DefaultFolders`, check which ones I have, and create the ones I don't. (Short answer: yes)
Dave DeLong
What is the purpose of `AbstractItem`'s `weight` attribute? Or is that just specific to the data you are modelling?
jbrennan
@jbrennan - since CoreData stores everything as sets, it doesn't maintain order. I use the `weight` attribute to remember what order the items have been placed in by the user. The lower the weight, the higher in the list. The first thing in the list has a weight of zero. This weight is dynamically recalculated every time the user adds a new `AbstractItem` at the same level or whenever the user drags and drops.
Dave DeLong
I should also say that I called it "weight" because that's what it's called when I work with Drupal. Perhaps "order" might be a better name.
Dave DeLong
Aha, I was thinking it might have something to do with that. Another question, how do you determine under which Group a new node goes? Since you don't use bindings, I'm guessing when the user clicks the "New Playlist" button (for illustration), you create a playlist node and set its parent to the `PlayLists` folderNode ? Am I on the right track?
jbrennan
@jbrennan It depends on the current selection. If the current selection is a Folder, the parent is the Folder's parent. If the current selection is a Group, the parent is the selected Group. If nothing is selected, the parent is the first editable root Group (ie, the "Playlists" group).
Dave DeLong