Updates from October, 2008 Toggle Comment Threads | Keyboard Shortcuts

  • Jonathan Dann 18:07 on October 24, 2008 Permalink | Reply  

    I Love Outline Views – Here’s Mine 

    Apps like Coda are the de-facto standard for good user-interface design nowadays. You can do a lot with the standard Cocoa controls provided by Apple, but you can’t always get exactly what you want out-of-the-box. For example, the CSS edit section of Coda has a fantastic way of dividing tasks: the animated outline view.

    Rather than having a monolithic view that has all the controls grouped with dividing lines; each section, “Text”, “Colors and Background”, “Dimensions”, all exist in their own collapsible view. Clicking the separating bar animatedly expands or collapses the view.


    To make one of these views isn’t quite as easy as you first think, and you can quickly go down the wrong road in the design. The super-secret trick is to *not* use Core Animation, or more precisely: ignore the lure of NSViews conformance to the NSAnimatablePropertyContainer protocol. As of Mac OS X 10.5, a few of the properties of a view or window can be animated simply be replacing calls like [view setFrame:newFrame]; with [[view animator] setFrame:newFrame];. This is fine when you have one or two views that you want to move but it’s impossible to coordinate the movements of multiple view using this API. After using Core Animation, one would expect that after a call such as [[view animator] setFrame:newFrame]; requesting the frame from the view would return newFrame. Unfortunatley it returns the original frame of the view. Only when the animation is done, do you get newFrame. Furthermore, just try setting the delegate of the CABasicAnimation object that handles the implicit animation and getting any form of useful information back. You can’t. You’re in for a world of pain if you try.

    The solution: use good-ol’ NSViewAnimation. That way you can create all the dictionaries containing the animations for all the views you want to, then instantiate one NSViewAnimation object to handle the lot and set them off at the same time. Simple.

    The Animating Outline View

    The outline view is made up of a few classes, the TLDisclosureBar, a subclass of the highly-configurable TLGradientView; the TLCollapsibleView which has a TLDisclosureBar and any NSView you like as its subviews. A TLCollapsibleView itself can be used alone, and will animate the collapse/expansion of the NSView subview (which I term the “detail view”).

    The fun part comes when TLCollapsibleViews are subviews of a TLAnimatingOutlineView. In this case, the TLAnimatingOutlineView takes over all the animations, simply asking the TLCollapsibleView that the user has selected for the NSDictionary object that describes the collapse/expansion animation. With this information, it constructs the movement animations for all the subviews below the collapsing/expanding view and handles the required change of frame size to accommodate the changing content size. It’s all quite elegant, if I do say so myself.

    Not being content with example code, I’ve made sure these classes are real-world useful. This view is integral to BibTeX management in Scribbler so it needs to be good. Some of the relevant parts of the NSOutlineView API are replicated and the delegate of the TLAnimatingOutlineView gets will/did/expand/collapse notifications, along with allowing the delegate to deny/allow animations, too. Each of the TLCollapsibleViews will also ask their detail views if the collapse/expansion is allowed if the TLCollapsibleDetailView protocol is implemented. The TLAnimatingOutlineView itself works when in an NSScrollView.

    The code, along with an example project, is hosted on Google Code here. It is distributed under the new BSD license.

    So why is is not ESAnimatingOutlineView, why the TL namespace? Well, I’m working on something big, at the same time as writing Scribbler. Start guessing.

    Update

    I’ve fixed a few bugs in the code, the current version can be checked out of the repository. Fixes include: proper support for hiding subviews of the TLDisclosureBars when the outline view is resized small, a correction to the autoresizing mask of the TLAnimatingOutlineView itself, declaration of keys that the TLCollapsibleView supplies with the animation dictionaries, the delegate of the outline view and subclasses of TLGradientView are no longer unregistered for notifications that client code specifies (i.e. no longer uses [[NSNotifcationCenter defaultCenter] removeObserver:_delegate];).

     
    • Mark Aufflick 14:38 on October 25, 2008 Permalink | Reply

      So happy to hear about the bibtex support! Oh, the animated views will be nice too, and thanks for sharing the example code.

    • Jonathan Dann 17:52 on October 25, 2008 Permalink | Reply

      Hi Mark,

      Thanks for the encouragement! I’m getting really excited about this app. I just hope that I can get it out asap :)

      Glad you like the sample code too. If you do manage to use it, let me know.

    • Brad Gibbs 17:45 on January 14, 2009 Permalink | Reply

      Thanks for the sample code. It’s been very helpful!

      The sample project failed to compile initially. I changed a #import from Espresso/TLGradientView to TLGradientView and it compiled and ran fine.

      I’m trying to integrate your code into an app. I’ve copied and pasted the AppController class in your sample project to an NSViewController subclass in my project. The app compiles and runs, but crashes when I try to expand or collapse an item. The Debugger stops on this line:

      if (![(id)self.delegate respondsToSelector:@selector(outlineView:shouldCollapseItem:)])

      The sample code I downloaded gives a warning stating that AppController may not conform to the TLAnimatingOutlineViewDelegate protocol. I added to my outline view controller, which shuts up the compiler, but the app still crashes.

      Any thoughts on what I might be doing wrong?

    • Markus 23:45 on August 6, 2009 Permalink | Reply

      Great stuff. Downloaded yesterday and works perfectly. One minor issue is the delegate method names set in -setDelegate: of TLAnimatingOutlineView in TLAnimatingOutlineView.m

      In your version they’re all tied to -outlineViewItemWillExpand:, they should be wired to Did/Will Expand/Collapse. This is the fix:

      - (void)setDelegate:(id )delegate;
      {
      if (_delegate == delegate)
      return;
      [self _removeDelegateAsObserver];
      _delegate = delegate;

      if ([(id)_delegate respondsToSelector:@selector(outlineViewItemWillExpand:)])
      [[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemWillExpand:) name:TLAnimatingOutlineViewItemWillExpandNotification object:self];
      if ([(id)_delegate respondsToSelector:@selector(outlineViewItemDidExpand:)])
      [[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemDidExpand:) name:TLAnimatingOutlineViewItemDidExpandNotification object:self];
      if ([(id)_delegate respondsToSelector:@selector(outlineViewItemWillCollapse:)])
      [[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemWillCollapse:) name:TLAnimatingOutlineViewItemWillCollapseNotification object:self];
      if ([(id)_delegate respondsToSelector:@selector(outlineViewItemDidCollapse:)])
      [[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemDidCollapse:) name:TLAnimatingOutlineViewItemDidCollapseNotification object:self];
      }

    • Jonathan 08:02 on August 7, 2009 Permalink | Reply

      Excellent, I’ll file it as a TODO :)

    • Dan Pahlajani 01:33 on March 8, 2010 Permalink | Reply

      Hi Jonathan,

      This is really fantastic code you have made available to write cool Cocoa apps and is greatly appreciated.

      I also have a couple of questions. Currently, the views are being setup in the awakeFromNib which is perfect but causes a little flashing from the window opens. Is there way that the views can be set in advance to avoid the flashing?

    • Robert Payne 03:03 on April 30, 2010 Permalink | Reply

      I know this is a relatively old post but great work Jonathan,

      I would like to note when using this code in a multi-window application ( I’m currently using it on every window for a document based application ) there is some huge performance hits when creating more than a single window.

      The TLGradientView adds listeners to the window become active / resign active notifications and send those calls directly to “display” which results in massive performance hits.

      The listeners should instead call [self setNeedsDisplay:YES]; instead of directly invoking the “display” command. This increases performance massively and my application can instead open 20 or so windows before feeling the performance hit that was occuring on the first window open.

      • Jonathan Dann 08:30 on April 30, 2010 Permalink | Reply

        Wow, I remember doing that now but I have no idea what I was thinking. You’re right, of course, -[NSView display] is almost always to be avoided.

    • Robert Payne 08:33 on April 30, 2010 Permalink | Reply

      Pretty easy fix. I was having performance issues for past week or so since I implemented the outline view ( which is AWESOME by the way great job! ) and found it was the outline view itself but it took awhile to find out the problem and then how to fix it directly.

    • Mark Aufflick 23:28 on May 20, 2010 Permalink | Reply

      Hi Jonathon,

      a) there’s a minor display glitch when used in a height adjustable context in that the view can be sized smaller than the clip view and thus when the height is reduced (eg. resizing the window) the view get’s hidden behind grey nothingness. The fix is simple:

      *** tlanimatingoutlineview-read-only/Classes/TLAnimatingOutlineView.m Sun May 16 17:33:10 2010
      — ../cocoa/mine/DailyImageWorkflow/TLAnimatingOutlineView.m Thu May 20 22:20:23 2010
      ***************
      *** 91,96 ****
      — 91,103 —-

      for (TLCollapsibleView *subview in [self subviews])
      newViewFrame.size.height += NSHeight([subview frame]) + [self.delegate rowSeparation];
      +
      + if ([self enclosingScrollView]) {
      + NSSize contentSize = [[self enclosingScrollView] contentSize];
      + if (newViewFrame.size.height < contentSize.height)
      + newViewFrame.size.height = contentSize.height;
      + }
      +
      [self setFrame:newViewFrame];
      }

      b) how far did you get with your image adjust view? IKImageView is killing me!

      c) when is Scribbler coming out?!

      Cheers,

      Mark.

      • Mark Aufflick 23:57 on May 20, 2010 Permalink | Reply

        Also I have a patch for the display/setNeedsDisplay: issue – if you want to add me to the google code project I’ll make both changes. My google code username is “aufflick”.

  • Jonathan Dann 10:02 on June 18, 2008 Permalink | Reply  

    Creating iTunes Scrollers 

    Apple introduced new scrollers in iTunes 7 and then moved on to give us the HUD, which many developers want their own scrollers for too. In Leopard, many of us thought that these would come in a nice, shiny box; but as they didn’t we’re all forced to roll our own. The common method is to draw all the components in Photoshop and then make a composite image when subclassing, but now with NSGradient and some nice additions to NSBezierPath all these have become quite easy to do, even for those with little artistic ability.
    (More …)

     
    • Jonathan Watmough 18:14 on September 4, 2008 Permalink | Reply

      Hi Jonathan,
      Interesting post and project. It was funny to run it, because it looks like it’s out by a pixel, but then, looking at iTunes, that now looks wrong to me. Thanks for ruining iTunes for me!
      Anyway, Scribbler sounds like a great idea. I use a really cut down selection of styles to give my documents some regularity in OpenOffice. When using other peoples documents in Word, the generally messed up styles drives me nuts. Will Scribbler have a set of predefined document templates? It seems like the nice TeX output would be great for people who like documents to be formatted in a standard way.
      How are you planning to ensure that a TeX stack exists and is valid on the destination Mac?
      Good luck with being an ISV!

    • Jonathan Dann 20:34 on September 5, 2008 Permalink | Reply

      Hi Jonathan,

      I *think* I know what you mean about it being out by a pixel, is it on the right-hand side of the vertical scroller? If it is then you see it in apps that don’t have a 1-pixel border around their scroll views, so the edge of the scroller is exactly aligned with the edge of the window. You can then see the effect this has when you move the window so it is directly above the desktop after being above another application’s window (if that makes sense). If that’s not what you mean then et me know and I’ll take a look.

      As for Scribbler, I’m working on a set of project templates and document templates to help get people started, but these will grow as they’re added by users. If you have your own, you won’t have to send it to me for inclusion in the app either, just put it in the Scribbler directory in ~/Library/Application Support and Scribbler will find it. If you have any really good templates then please send them along!

      As for the TeX stack, the only part that doesn’t work if there’s no valid distro on the system is typesetting itself. For those that download the app without TeX installed, I’m going to point them to the most current MacTeX distro for simplicity, possibly downloading it and mounting the disk image it for them so all they have to do is install. If I’ve misunderstood your question then please forgive me!

    • Jonathan 03:15 on February 24, 2009 Permalink | Reply

      This is awesome. I especially appreciate the fact that you aren’t using images. I’ve been taking a lot of extra time to draw out my own interfaces lately. I just have one problem now. I realized it after I threw your scroll bars in. My contained table view still has Aqua headers. Know of any other Classes for taking care of that or do i need to get to work? Also I’m planning on modifying this to also accommodate the black/grey scroll bars that you also see in iTunes in addition to others like iMovie. Want me to send you my changes when/if I finish it?

    • Jonathan 09:33 on February 24, 2009 Permalink | Reply

      Hi Jonathan,

      Glad you like them, I’m afraid looking back on them now they need a bit of a redesign! I’ve just been thinking of using black ones myself so, yeah, send them along when you’re happy with them. I’d appreciate it.

      As for the headers, you just have to subclass NSTableHeaderCell. You’ll have to draw both the background and the text but read the 10.5 AppKit release notes, they give you good pointers on using NSBackgroundStyle in cells for the embossed text look.

      I’ll see what I dig up in my own source though as I’m sure I did this a while back.

    • Jonathan Badeen 17:59 on February 26, 2009 Permalink | Reply

      Thanks Jonathan,
      I’ve been working on finessing your scroll bars in addition to adding the dark styled ones. There are still at least a couple things i need to do/improve still. You can see them implemented in my app here – http://www.badeen.com/fastcapture1.png. When I’m done I’ll send it your way. I did have one question though. Do you know how I can go about drawing my own resize corner or just get it to display transparently in the lower right. I essentially would like to get it to look more like it does when there is no scroll bar – http://www.badeen.com/fastcapture2.png

    • Jonathan Badeen 04:56 on February 28, 2009 Permalink | Reply

    • Gustavo 09:09 on September 6, 2009 Permalink | Reply

      Jonathan hello.

      Thanks great example, I wish I understand it better, im a novice ( with some experience already) and I thought this goal wold be easier than I say in the example.. Im trying to implement something similar in my application, but definitely you have gave me a good idea on how to achieve it.

      Must read again to understand the whole of what you are doing in the example… :S

      G.

    • Michael 06:58 on October 6, 2009 Permalink | Reply

      Hey Jonathan.

      Thanks for the amazing NSScrollerView subclass. I’m currently working on a project made to look similar to the itunes store. http://www.publictunes.com/screenshots/pic1.png the vertical scroller works like a charm. Unfortunately the horizontal one appears to have some fundamental drawing errors. I have looked at the code and had trouble grasping how the curves were made that cut into the increment decrement arrows for the slider tracks. If you could please explain this or fix the class it would be much appreciated.

      -Mike

      • Michael 07:00 on October 6, 2009 Permalink | Reply

        Oh i forgot. Here is a blow up of the horizontal slider problems. http://www.publictunes.com/screenshots/pic2.png

        • bryscomat 16:48 on October 26, 2009 Permalink

          I was able to fix this by putting this in the arrowsSetting: method:

          ESScrollerArrowsSetting setting;
          if (self.isVertical) {
          if (NSMaxY([self rectForPart:NSScrollerDecrementLine]) == NSMinY([self rectForPart:NSScrollerIncrementLine]))
          setting = ESScrollerArrowsTogether;
          else
          setting = ESScrollerArrowsApart;
          }
          else {
          if (NSMaxX([self rectForPart:NSScrollerDecrementLine]) == NSMinX([self rectForPart:NSScrollerIncrementLine]))
          setting = ESScrollerArrowsTogether;
          else
          setting = ESScrollerArrowsApart;
          }

          return setting;

        • Jonathan Dann 12:40 on October 31, 2009 Permalink

          Sorry about that. I must have got horribly distracted. I’ll update the code soon! It need s a good looking-at.

        • Simon Strandgaard 10:15 on April 5, 2010 Permalink

          @bryscomat thank you. This solves it for me as well.

          @Jonathan Dann, thank you for putting this awesome project together. Works out of the box.

    • Philipp 13:47 on October 25, 2009 Permalink | Reply

      Hi Jonathan,

      Thank you for the great post and the code.

      What license is the code released under? I looked through the project folder but couldn’t find any.

      Thank you in advance!

      • Jonathan Dann 12:39 on October 31, 2009 Permalink | Reply

        Oh yeah, consider it BSD If you want to credit me feel free, but you don’t have to.

        I need to clean up that code. It’s embarrassingly bad now!

    • Mike 06:17 on January 16, 2010 Permalink | Reply

      Great example! Any pointers on incorporating this into a webview? I know the NSScrollView is dynamic and Im not sure how took over it.

    • Dan Pahlajani 19:55 on February 28, 2010 Permalink | Reply

      Hi Jonathan,

      I have tried to download the code for scrollers on the site you point to. But unless I am a paid user, I am unable to download the files. It is constantly busy with too many users.

      Is there any other way to get access to these files?

      Thanks,
      Dan

    • Amble 13:09 on March 12, 2010 Permalink | Reply

      I was able to fix this by putting this in the arrowsSetting: method:

      ESScrollerArrowsSetting setting;
      if (self.isVertical) {
      if (NSMaxY([self rectForPart:NSScrollerDecrementLine]) == NSMinY([self rectForPart:NSScrollerIncrementLine]))
      setting = ESScrollerArrowsTogether;
      elss
      setting = ESScrollerA4rowsApart;
      }
      else {
      if (NSMaxX([self rectForPart:NSScrollerDecrementLine]) == NSMinX([self rectForPart:NSScrollerIncrementLine]))
      setting = ESScrollrrArrowsTogether;
      else
      setting = ESScrollerArrowsApart;
      }

      return setting;;

    • David Dunham 22:42 on March 31, 2010 Permalink | Reply

      Thanks for your scroller! I managed to get it to work with the lame DoubleBoth style, and wanted to send you the changes. (There was also a bug in isActive.) I couldn’t find a contact e-mail however.

    • Luke 05:59 on May 13, 2010 Permalink | Reply

      Is there anyway to get these scroll bars working on a WebView? Not sure how to assign the ESScrollView to the WebView ScrollView as you can’t access that in IB. Any ideas as I am quite new to Cocoa?

  • Jonathan Dann 21:00 on May 13, 2008 Permalink | Reply  

    NSTreeController and Core Data, Sorted. 

    Having recently taken the plunge into Core Data I decided it was time to rip out all the model code from my current application and replace it with a Core Data version. After about a day I had my app up and running again but with one huge problem, the content of my NSOutlineView always appeared in a random order. Such is the problem with Core Data that NSManagedObjects store their to-many relationships in an NSSet, not an NSArray, which is unordered. So when your NSTreeController tries to display its data it appears in a random order.

    This is not nice, imagine if the playlists in your iTunes library always changed their order? It gets even worse if your user wants to use drag and drop. In this case they decide the order, and they’d probably want it to stay that way.
    (More …)

     
    • mark 11:40 on May 26, 2008 Permalink | Reply

      Nice and clearly written article! I have a suggestion though. Now that the sortIndex has become persistent and a part of the model, according to MVC, it means that sortIndex is now owned by the model. It should be the model who keeps the node indexes under control or says which nodes accept new/moved children.

      It bothers me that in this implementation the business logic is embedded in the controller. If you modify the model’s sort indexes programmatically, your tree controller does not get to know about it. Also, children can be added or moved programatically and the model can become inconsistent because the decision whether the move is accepted, is made by the NSOutlineView delegate (!) – not the model.

    • Jonathan Dann 12:37 on May 26, 2008 Permalink | Reply

      Thanks for the compliment Mark!

      I’m afraid I have to disagree with you on this though ;-) but I’d really like for you to explain how your implementation would work though, I’m sure there’s places I can improve this.

      Your first point is correct, the sort index is owned by the model, this is essential for the persistence of the sort order. I can’t think of another way of keeping the sorted order between sessions. When doing this, I don’t see why my model objects should have any knowledge of each other at all, that’s why the code to keep the sort indexes in check is in the controller. The controller knows about all of the nodes and the NSTreeNodes. Making the model objects (too) aware of their surroundings seems to break encapsulation, they should just be happy to exist in their own universe (in the larger sense, obviously they can traverse their parent and children relationships).

      Your point about the models saying which nodes accept or deny new nodes is fair, but I think the logic that governs which nodes accept new/moved children is forced to be in the NSOutlineView’s delegate due to the way the view works. When you log the proposedIndex when -outlineView:validateDrop….. the outline view returns -1 is the cursor is over a child or at the root of the tree. There’s clearly no way to obtain the NSManagedObject/NSTreeNode that has an indexPath with a -1, so the allowing or denying moves to the delegate.

      In my application I have a source list that looks like iTunes but has groups and files like in Xcode. I may have been influenced by my own design requirements, but dropping dragged items onto a file makes no sense and one clearly shouldn’t be able to drop a group on to one of its subgroups. The latter could be done in the model code by would still be mediated in the outline view’s delegate. It would ask the model to validate the drop of the proposed item and the model would have to see if the proposed item was somewhere along the parent relationships. The reason I didn’t do this was for flexibility, by using the model you’re not locking developers in to a certain mindset, there are cases when you would want to be able to rearrange the tree arbitrarily and this model accommodates that.

      You’re correct that modifying the sort indexes programmatically doesn’t inform the tree controller, but the resort would take place when NSTreeController’s -rearrangeObjects is called. I would think it quite difficult the modify them programmatically in the first place without messing up the tree. You’d have to decrement and increment the other children around it and, although you can get to the other children using the @”parent.children” keypath, even doing this seems to me to break encapsulation.

      You are correct that it could be done, and the model could take care of it all but there would be an immense amount of code to write and some edge cases that may be hard to find. The other confounding factor is that time = money, and getting the tree controller to steal the NSTreeNodes -lastIndex which it has just magically placed in the right place is just too easy to pass up. Although I’m still not convinced that I’ve put things in the wrong place.

      The outline view’s delegate methods don’t just allow/deny drops they also return values that the view itself uses to communicate the actions to the user. These methods tell the view to draw its little blue boxes and lines to show where (or not) the drag can go. As the sorting of the tree (in the case of my app) is really a view-related process (its just the ordering the user wants to see) then I think its OK to allow/deny in the delegate, one of the reasons delegates exist is so you can have a third party (almost) that you can ask, “what do you think?”.

      Finally, the case of adding and moving children programatically should be done using the tree controller, its whole point is to manage the tree, so if you use ESTreeController’s methods to add/move/remove then you’ll never end up with an inconsistent tree. I think it fit perfectly that the tree controller updates the model’s sort indexes as the controller then handles keeping the tree consistent. Doing otherwise, I say hesitantly, would be programmer error.

      I hope this hasn’t been to rambling, I’d love to hear of your opinions on this. Thanks again for reading the post and commenting, you’ve made me think again about my design decisions, hope you don’t mind that I’ve disagreed.

    • mark 11:49 on May 27, 2008 Permalink | Reply

      Thanks for your reply, Jonathan. I think that disagreement is a good basis for a discussion. Also, I’m glad you’re representing files in the tree of your app, because it’s a pretty good example of what I was trying to say. :-)

      With the filesystem, we have a case of model updating “programatically”. Files maybe added or removed behind our apps back. Our model might observe these changes via FSEvents and update itself. (The tree controller is notified via KVO.) Or we could have a separate model controller feeding the model, anyway it’s not a tree controllers job to do.

      Model is already a tree by itself, and file items “know” about each other via parent/children relationships. Whether the file can be renamed/moved or not, is totally dependent of the file system – it’s not a decision of NSOutlineView delegate or NSTreeController to do. Also, moving a file might actually fail even after it looked like it would succeed. Again, only the model will know.

      To make things harder, in addition to the content observed to be present in the actual file system, the model may contain other content which is only stored in the core data database. We might retain some data for the files that once deleted might reappear later, or like XCode, we could have “groups” or “smart groups” that only live in our model. So to sum up, we have many kinds of mirroring and syncing going on and we haven’t even introduced the UI yet.

      NSTreeController is there to represent the model for the view layer, not to be the model.

      Then things start to get more complicated. The possibility of having multiple views to the same model data requires introducing another model layer.
      But what do you think about this so far?

    • Jonathan Dann 18:12 on May 28, 2008 Permalink | Reply

      I think I may know why I’m disagreeing with you (now only on some points ;) ).

      You say that the model should update itself and the tree controller shouldn’t be the one doing it. The way I was thinking is that the tree controller should take care of updating the model because it is a controller, now you bring forth the idea of a model controller that does the model updating and the tree controller can then be left to handle tree stuff.

      This is how I’ve done it really in my app as I have an NSDocument subclass that is my model controller. So I have a (relatively) dumb model and a controller that does the decision-making, this I think is very much in-keeping with MVC.

      You’re correct that the tree controller is there to represent the model for the view layer, hence why I’ve made it the one to update the sort indexes of the model after the drop acceptance (wherever that may finally be) as it knows all about the user’s requested ordering.

      So do you think then that in the outline view’s delegate methods, the delegate should ask the model (or model controller) if the drop should occur?

      In my app, I don’t quite represent the file system as you’re thinking of it. It’s like (the default setup of) Xcode, where all the files are reference arbitrarily and the “groups” aren’t represented on disk, hence why I’ve allowed arbitrary re-ordering of nodes and groups that doesn’t affect the file-system itself. I’m going to be using FSEvents in the future but at the moment I’ve implemented new file creation and drag and drop onto the tree from the Finder that then moves the files or simply creates a node that references the files’ current locations.

      It’s this alternative view of the file-system that has caused me to allow the outline view’s delegate to allow/deny the drops. Interestingly though I have setup my model to control whether a node can be dragged in the first place, and other things like whether a particular node is a “group node” (see -outlineView:isGroupItem: ) or can collapse/expand. This resulted from trying to determine if the group was to be in the uppercase text like in iTunes. Originally checking the name of the group node was fine, but then I realised if a user called one of their own group “IMAGES” then it would return YES in -outlineView:isGroupItem: so the most reliable way is to give the group entity an isSpecialGroup attribute.

    • Alej 19:51 on June 5, 2008 Permalink | Reply

      The file hosting is not working anyone mind re-uploading it on another server (Eg. Rapidshare) thanks

    • Jonathan Dann 00:08 on June 6, 2008 Permalink | Reply

      Yeah really sorry about that, an it seems that although I’ve paid $20.00 for 5GB of space on WordPress I still can’t upload the zip file for the project. hmmm….. I’ll fix this. Bear with me.

    • Jonathan Dann 00:10 on June 6, 2008 Permalink | Reply

      here it is, I’ll update the post.
      http://rapidshare.com/files/120385221/SortedTree.zip.html

    • Moritz 12:51 on June 7, 2008 Permalink | Reply

      Hi,
      I’m using xCode 3.0 on PPC G4 Leopard 10.5.3.
      When I open the SortedTree xcode project I get an error message, that the project was created with a newer version of xcode than mine. Thus I cannot compile and run the project. I’ve heard rumors that there is a xcode 3.1 in beta out there. Can anyone advice as to how to solve this issue.
      Thank you very much.
      Also Jonathan thank you very much for this tutorial. It is very valuable even just the source.
      Greetings
      Moritz

    • Jonathan Dann 13:10 on June 7, 2008 Permalink | Reply

      yeah sorry about that I’m running the iPhone SDK which has 3.1 so many improvemts it’s worth getting

    • Moritz 16:16 on June 8, 2008 Permalink | Reply

      Hi Jonathan,
      Could you send me a working executable of SortedTree? I would really like to execute your programm because I don’t fully undestand the code yet. I think that would really help me to figure out what your code really does.
      Thanks and greetings
      Moritz

    • Daniel 21:57 on June 17, 2008 Permalink | Reply

      Hi I can’t get your code to compile because you built it with XCode 3.1 which is not yet available for us non-iPhone plebs.

      Can you please release a version for 3.0?

    • ian 22:47 on June 17, 2008 Permalink | Reply

      hey jonathan, i can’t thank you enough for this. i’m a hack working on my first cocoa app and i’ve been banging my head against this for weeks it seems.

    • Jonathan Dann 10:26 on June 18, 2008 Permalink | Reply

      @Daniel,

      I will when I get a sec! Before then you can create a new project with the same name and drag in all the files (including the NIB and info.plist) into Xcode after deleting the originals. That should do it.

      @ian

      You’re very welcome, people were so helpful when I started it’s good to be in the position to offer advice

    • Daniel 21:23 on June 18, 2008 Permalink | Reply

      “Before then you can create a new project with the same name and drag in all the files (including the NIB and info.plist) into Xcode after deleting the originals. That should do it.”

      Tried that. No luck. It just hangs. I there’s an incompatibility with the version of IB you’re using and the official version 3.0. You used an unreleased beta so I expect there are some differences in the format of the XIB.

    • Issi 23:27 on June 21, 2008 Permalink | Reply

      Thanks for the post. Very informative and thought provoking.

    • MDK 16:31 on June 26, 2008 Permalink | Reply

      Hey Jonathan,

      Would like to bug you again, not 100% related but since you mentioned NSPersistentDocument and core data in other posts…

      I’ve got a core-data based app which is a non-document-based app (think iTunes, etc) yet I’d like to use the document-based machinery for it. In other words, I want to use NSPersistentDocument (ie. it’s undo features) + XSControllers architecture except that I’d like the document to be hard-wired to only file on disk — with no support for opening file types etc.

      Is there some smart way to do this without changing Info.plist? Would you recommend this approach? Perhaps instantiating the Document myself and calling/simulating the makeWindowControllers machinery myself?

    • Jonathan Dann 23:52 on June 26, 2008 Permalink | Reply

      Do you want to have an app that has many windows that can show the user their data but in different ways? Like the way iTunes does when you double-click a playlist? Can you be more specific with what you’re trying to achieve in terms of using the program?

    • Rob 21:18 on October 1, 2008 Permalink | Reply

      Jonathan,

      Nice example app very helpful, was wondering if you where able to do the same thing but with undo working it, right now if you drag and drop a leaf from one place to another and hit undo the results from the undo are very random.

      Rob

    • Jonathan Dann 17:50 on October 3, 2008 Permalink | Reply

      Hi Rob,

      Sorry it’s taken me a while to get back to you on this, I’m in a rather internet-less state at the moment!

      Unfortunately I haven’t been able to get undo working for it, and you’re right that its goes haywire! I think it would require some inventive creation of undo-groupings if it’s possible to fit it into the undo architecture of Core Data at all.

      In Scribbler, I’ve actually turned of the Core Data undo management and written my own for that parts that make sense. The way I use this source list, for management of files and folders like iPhoto does, it’s not a requirement to make moving a node an undoable action, so I haven’t pursued it.

      Sorry I can’t shed any more light on it, if you get somewhere with it, please let me know.

      Jon

    • Bill Monk 20:57 on October 6, 2008 Permalink | Reply

      Nice article.

      Small bug in NSTreeController_Extensions.m: the -flattenedContent method throws a valueForUndefinedKey exception, “the entity Group is not key value coding-compliant for the key descendants.”

      The trouble is a mere copy/paste error, -flattenedNodes and -flattenedContent are very similar, and both do

      [node valueForKey:@"descendants"].

      However -flattenedContent is working with ESTreeNodes, not NSTreeNodes. The descendants key works in -flattenedNodes (calling your -descendants category on NSTreeNode), but clearly @”children” was intended here, to call ESTreeNode’s -children method.

      That is, in -flattenedContent :

      if (![[realNode valueForKey:[self leafKeyPath]] boolValue])
      [mutableArray addObjectsFromArray:[realNode valueForKey:@"children"]]; // < -

      This works fine.

      -Bill

      PS, dragging leaf objects and undoing seems to be working here – no problems.

    • Bill Monk 20:36 on October 8, 2008 Permalink | Reply

      Actually a better fix is to add a -descendants method to ESTreeNode, analogous to the category on NSTreeNode. Using children as described above is not quite right; won’t fully enumerate complex trees.

      In ESTreeNode.m:

      - (NSArray *)descendants;
      {
      NSMutableArray *array = [NSMutableArray array];
      for (ESTreeNode *child in self.children) {
      [array addObject:child];
      if (![[child isLeaf] boolValue])
      [array addObjectsFromArray:[child descendants]];
      }
      return [[array copy] autorelease];
      }

    • Jonathan Dann 21:26 on October 16, 2008 Permalink | Reply

      Hi Bill,

      Sorry for the late reply and for not updating that project properly. I should be moving over to my own server soon where I’ll be able to host whatever files I want to!

      I realise now that I’d changed my implementation of -[NSTreeController flattenedContent] ages ago to be:

      - (NSArray *)flattenedContent;
      {
      return [[self flattenedNodes] valueForKey:@”representedObject”];
      }

      which really helps in cutting down on such copy/paste errors. You’re correct that to be correct in your implementation that ESTreeNode needs a -descendants key. As I’d written the above method I hadn’t run into this one.

      I need to have another look at undo, I’m surprised that its works for you. Maybe it was fixed be an OS update?…

      Just tried it an it tells me that the undo manager is left in an invalid state with too many nested undo groups. Can you try and reproduce this on your machine?

      Jon

    • Bill Monk 06:08 on November 6, 2008 Permalink | Reply

      Hi Jon,

      Thanks for fix above. I believe I finally figured out a way to make undo work reliably. The basic problem is that while Core Data correctly does the undo and redo on the data, NSOutlineView doesn’t necessarily see the changes and as the display gets out of the sync with the model, eventually an undo/redo command is issued without the correct data on the stack to perform it, resulting in the “NSUndoManager is in invalid state, undo was called with too many nested undo groups” error.

      In my my, the user must be able to sort the outline by various criteria, as well as sorting just a few selected rows within any given group. This has to be un-doable, and however the outline happens to be sorted/subsorted when it’s saved, it should re-open with the items in the same order. Obviously, that’s where I’m using your sortIndex idea. I set the sort descriptors as needed, update the sortIndices with your -updateSortOrderOfModelObjects method. When the document is re-opened, the sort descriptors are first set to one that sorts by the sortIndex, recreating the previous order. Undo works on everything, sorts, name edits, drag reordering, collapse/expand.

      To get undo working in your sample project, I downloaded a fresh copy and made these changes. It may not be the best way, but it does seems to work.

      1) In ESTreeController.m, change -reloadData so that it expands -and- collapses items according to the reloaded data. This is to prevent the problem of collapsing a group item, undo it and having work, then redoing and having the Edit menu flash correctly (indicating there was something on the stack that gets redone) but there being no visible change.

      Something like:

      - (void)reloadData;
      {
      [super reloadData];
      NSUInteger row;
      for (row = 0 ; row < [self numberOfRows] ; row++) {
      NSTreeNode *item = [self itemAtRow:row];
      if (![item isLeaf]) {
      if ([[[item representedObject] valueForKey:@”isExpanded”] boolValue])
      [self expandItem:item];
      else
      [self collapseItem:item];
      }
      }
      }

      =====
      2) When Core Data undoes/re-does something, say a sort, the model data is changed correctly, and all the sortIndex values are restored, the outline view needs to be told to reload its data. NSUndoManagerDidUndoChangeNotification/NSUndoManagerDidRedoChangeNotification work for this. If you allow sorting on criteria other than sortIndex, as described above, Core Data may have changed a bunch of sortIndex values, and to get the un-done sort displayed, we need to tell the treeController to to re-sort by the sortIndex before anything happens to call -updateSortOrderOfModelObjects, which will overwrite all the un- or re-done sortIndexes with the item’s current, pre-undone position in the outline. If that makes sense…

      - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
      {

      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(undoManagerDidUndo:)
      name:NSUndoManagerDidUndoChangeNotification
      object:nil];
      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(undoManagerDidRedo:)
      name:NSUndoManagerDidRedoChangeNotification
      object:nil];
      }

      - (void)managedObjectsDidChange:(NSNotification *)notification
      {
      [outlineView reloadData];
      [treeController setSortDescriptors:nil];
      [treeController setSortDescriptors:[self treeNodeSortDescriptors]]; // keep treeController sorted by sortIndex (except during a sort by name, etc)
      }

      - (void)undoManagerDidUndo:(NSNotification *)notification
      {
      [outlineView reloadData];
      [treeController setSortDescriptors:nil];
      [treeController setSortDescriptors:[self treeNodeSortDescriptors]]; // keep treeController sorted by sortIndex (except during a sort by name, etc)
      }

      ====
      3) I was constantly getting this error

      SortedTree Error setting value for key path treeNodeSortDescriptors of object (from bound object [entity: TreeNode](null)): [ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key treeNodeSortDescriptors.

      To stop it, I just turned off the NSTreeController sortDescriptors binding to ESAppDelegate’s treeNodeSortDescriptors, and instead, set the sort descriptors in code in -awakeFromNib or -windowControllerDidLoadNib.

      ====
      4) These errors were constantly being logged:

      -[ESTreeNode isSpecialGroup]: unrecognized selector sent to instance 0x193e00

      -[ESLeafNode isSpecialGroup]: unrecognized selector sent to instance 0x1a4910

      [ valueForUndefinedKey:]: the entity Leaf is not key value coding-compliant for the key isExpanded.

      -[ESLeafNode canExpand]: unrecognized selector sent to instance 0x171f80

      According to http://www.cocoabuilder.com/archive/message/cocoa/2008/6/9/209710

      these seem to be due to a bug in Leopard which causes the -outlineView: isGroupItem: delegate method to sometimes be sent a dealloced object when Undo/Cmd-Z is used immediately after a creating a new object with CoreData when the NSOutlineView is set to SourceList style in InterfaceBuilder.

      Whew… My fix was just to turn off the source list style. In my app I don’t use the style, so changed -outlineView: isGroupItem: to always return NO.

      Well, sorry for the long post, but I wanted to report back since your code helped me get my CoreData/NSTreeController app off the ground. Shoot me an email at the address in this form if you’d like the edited project. Thanks again.

    • StrAbZ 00:09 on August 14, 2009 Permalink | Reply

      Great post, I learned a lot of things, and I’m starting to find my way in all those bindings, core data, and other cocoa stuff ^^

      But I have a question… how can I use this model, and now which is the select node type? (ie: Group or Leaf).

      I mean in my case, the Leaf node, contains a list of object, that i want to display in a table view… but the Group object doesn’t… and you guess, when I try to bind the content set to the tree controller selection, it doesn’t work since Group doesn’t not conform to the specified accessor…

      Do you have any clue about that?

      I also noticed that i can’t have the root object unselected… when the app starts, even with the isSelectable attribute to NO, it is still selected…

      Thank you very much !

    • Hank McShane 12:58 on September 12, 2009 Permalink | Reply

      Thank you so much for this. I was struggling with understanding a tree controller and core data… but your example makes it all clear to me. You have a tree node, and the node can be either a group or a leaf… very simple. The rest of it with the expanded state saving and the sorting is invaluable… many many thanks.

    • sd 19:28 on January 14, 2010 Permalink | Reply

      Can you please help with one issue I’m having? The issue is present in your project, so you can use that as a test case. In short, the issues is that expanded groups within a collapsed groups don’t save their “expanded” state. Steps to reproduce:

      1) Compile and run your SortedTree project
      2) Click “New Group” two times, and then click “New Leaf” one time. So now you have “Leaf 1″ within “Group 2″ within “Group 1″
      3) Both the groups should now be expanded.
      4) Now collapse only “Group 1″
      5) Expand “Group 1″ again. You see that the “Group 2″ remained expanded, as it should.
      6) Now, collapse only “Group 1″ once again, and QUIT the app.
      7) Run the app again and expand “Group 1″ 8) ALAS, you see that “Group 2″ does not expand as it should!

      Contrast this problem with the correct behavior shown in other apps, say in iTunes.

      Do you have any idea how to fix this? For your information, it seems that–as weird as it is–”Group 2″ is sending the outlineViewItemDidCollapse notification even though only “Group 1″ was collapsed.

    • sd 17:07 on January 15, 2010 Permalink | Reply

      I found a solution to my problem, although it’s not the prettiest one out there. But 1) it works and 2) I’m not even sure that pretty methods to fix this exist in the first place.

      If anyone is having the same issue as I am, just let me know and I’ll try to describe it here.

    • Ian 17:49 on March 29, 2010 Permalink | Reply

      Hi Jonathan,

      I’m using your ESTree* classes with great success in an application, but I’ve run into a problem trying to archive my object graph. I’m hoping that you have some experience with this and I’d be very grateful if I could ask a couple of questions. I searched for a contact address to no avail, so I’m hoping you’ll see this.

      Best, Ian

    • Duncan Groenewald 02:05 on August 4, 2010 Permalink | Reply

      Jonathan – I am trying to get this code to work (using whatever the latest XCode/OSX version is today!) but it does not seem to run properly. I can compile it and run it but none of the buttons appear to work and there seem to be some warnings/errors thrown on startup – see below. Has something changed in XCode ?

      [Session started at 2010-08-04 11:04:15 +1000.]
      GNU gdb 6.3.50-20050815 (Apple version gdb-1469) (Wed May 5 04:36:56 UTC 2010)
      Copyright 2004 Free Software Foundation, Inc.
      GDB is free software, covered by the GNU General Public License, and you are
      welcome to change it and/or distribute copies of it under certain conditions.
      Type “show copying” to see the conditions.
      There is absolutely no warranty for GDB. Type “show warranty” for details.
      This GDB was configured as “x86_64-apple-darwin”.tty /dev/ttys001
      Loading program into debugger…
      Program loaded.
      run
      [Switching to process 24794]
      Running…
      2010-08-04 11:04:15.687 SortedTree[24794:a0f] [ valueForUndefinedKey:]: the entity TreeNode is not key value coding-compliant for the key “isExpanded”.
      2010-08-04 11:04:15.690 SortedTree[24794:a0f] -[ESTreeNode isSpecialGroup]: unrecognized selector sent to instance 0×143500
      2010-08-04 11:04:15.690 SortedTree[24794:a0f] -[ESTreeNode isSpecialGroup]: unrecognized selector sent to instance 0×143500
      2010-08-04 11:04:16.398 SortedTree[24794:a0f] Warning: NSTableView/NSOutlineView variable row height code has detected re-entry. Avoiding a crash….

    • Jonathan 13:50 on August 23, 2009 Permalink | Reply

      @StrAbz Glad it was of some help to you. When it comes to heterogeneous nodes in the tree, selection isn’t best done exclusively with bindings. You’re better using the NSTableView delegate methods to find out when the selection changes and update the content of your table view’s array controller manually based on the newly selected object. So your table view is still bound to the array controller, but the array controller’s content is managed in code by you.

      The tree controller and outline view both have flags set in IB concerning selection, try experimenting with the “selects inserted objects” and “avoids empty selection”.

    • Brad Gibbs 21:34 on October 24, 2009 Permalink

      Hi Jonathan,

      First off, thanks for posting all of the information you’ve posted. I’m using ESScrollView, the animating outline view code and, I’m trying to use the code described in this post. I’ve learned a lot from working through your code.

      I’m having issues trying to extend the code in this post. I can see two possible ways of doing so — 1. make new model objects that are subclasses of ESGroupNode and ESLeafNode, or, 2. add a relationship to ESGroupNode and ESLeafNode for the represented Object. I’ve implemented the first approach, but, it gets a little messy and it means that these model objects can’t inherit from any other classes. The second method seems to mirror a non-Core Data NSTreeNode application more closely (ie., DragAndDropOutlineView sample code from Apple), and, it frees the objects up to be subclasses of other objects. It also seems a bit cleaner, especially given that these objects are going to be used in many different contexts in my app.

      I haven’t tried the second method, yet. I was wondering which path you’ve chosen.

      Thanks.

      Brad

    • Jonathan Dann 12:36 on October 31, 2009 Permalink

      It’s an interesting question, and there’s no correct answer. I starting with the former and then moved over to the latter. I ended up with each node having a “representedObject” relationship to other entities.

      It may be a little harder to manage, but then I also write some classes that I go through when mutating the model, so I ensure, using these “model controllers” that I’m not just changing my model ad-hoc and it keeps it in an correct state.

      Also remember that your class hierarchy can diverge from your entity hierarchy when using Core Data.

  • Jonathan Dann 14:43 on April 2, 2007 Permalink | Reply  

    DRM-Free Tracks to be Sold on iTunes 

    Gracefulflavor

    I understand why people don’t particularly like DRM on their tracks, but let’s not forget that it allowed the iTS possible. I’m hesistant to hail this is a killer move; but a great option, yes.

    I wonder how long it will take for all the non-DRM tracks to be available over sharing networks like BitTorrent. Call me naïve but I do think that too many people will take advantage of this to get more free music. I know lots will be careful enough to not put them in a shared folder for the world t see, but there’s many stupid people out there.

    I’ll get slated for this, but I like DRM, it has allowed me to get the music I want, simply and cheaply. The iTS enabled me to buy music, rather than paying for overpriced CDs. Having the option to choose your player is great, but I love iPods, and I think its very important that people don’t steal.

    Anyway, if I wanted to put all my stuff on another player I’d burn it and rip it(which would take forever), I’d never share it, I have a conscience. Please don’t think I’m implying everyone who has non-DRM music is a criminal, but there are enough people out there who copy music to damage iTunes sales in the long-run.

    The best part of this is the higher bit-rate of these unprotected tracks. I’ll buy some for that reason alone, but don’t expect to get them off me!

    UPDATE: Just listening to the news on BBC Radio 1, only the biggest radio station in the UK, broadcast worldwide; and what was their headline?

    “Why it will now be a little easier for you to share music with your mates.”

    That’s irresponsible.

     
  • Jonathan Dann 01:56 on March 23, 2007 Permalink | Reply  

    Klingon Language in Mac OS 10.4? 

    Klingon Language in 10.4

    Can somebody please tell me why I have KLINGON language support on my computer! Well, when I turned it on, no programs had a Klingon interface, but still. I think someone somewhere has way too much time on their hands. And yes, the reason I know that it says Klingon is because I used to watch Star Trek avidly, like you wouldn’t believe. I’m kinda glad those days are gone now, I still appreciate it but one day I just stopped watching. I think the sun was shining outside, so I went out.

     
  • Jonathan Dann 21:11 on January 9, 2007 Permalink | Reply  

    Macworld 2007 

    Well, I have to say it was a let-down. After all the rumours of upgraded Mac computers, more on the upcoming Leopard operating system and new iLife and iWork applications, the keynote at the expo in San Francisco was disappointing. Don’t get me wrong, the iPhone looks excellent, and seems to be the phone I’ve been wanting for such a long time. Finally we have one that is intuitive and simple to use, but does so much you want it to do. The touch screen is a fantastic addition, and the only way to allow a complicated phone to be operated. I don’t mind about the price of it, or the availability in the fourth-quarter of 2007 here in the UK, but I was expecting more about the Mac. The Apple TV isn’t bad too, but I would never need anything like that, especially since the rumoured TV and Movie download service from iTunes in the UK didn’t come to fruition.

    For a computer company, I think many people will be disappointed at the lack of computing innovation at the expo, and the loss of ‘Computer’ from the the name ‘Apple Computer Inc.’ may, for some, hail the beginning of the end for the Mac. I’m not so pessimistic, but I would definitely be loathed to return to using Windows after 3 years of an iMac doing everything I wanted to do, and more.

     
  • Jonathan Dann 14:47 on January 9, 2007 Permalink | Reply  

    Vienna – Free RSS Reader 

    Having recently begun to blog, I have found that trying to view all the blogs I want to see can be quite a task. I found a great free RSS reader called Vienna. Its a very simple and uncluttered interface and has its own built-in browser for viewing full articles in the app. I even managed to easily import my Google Reader feeds perfectly, without messing with settings.

     
  • Jonathan Dann 01:11 on January 5, 2007 Permalink | Reply  

    My First Dashcode Widget 

    So it turns out that Steve Job’s famous reality distortion field was at work again when he first talk about the ease of use of dashcode. From a cursory glance at it, to do anything even remotely out of the field of the templates still requires you to learn java, or is it javascript?

    It is very powerful, don’t get me wrong, but as someone who cant program in the relevant languages, and has no artistic bones in his body, I was only able to create a simple widget to display the RSS feed from this blog. Many of the components are drag-and-drop, so more playing may allow me to create a widget with a field on the back that allows the entering of a feed URL, only time will tell.

     
    • dwerg85 20:00 on January 25, 2007 Permalink | Reply

      So how did you get dashcode to show the wordpress feed? It only throws a null value error here.
      Apparently it will work just fine if the feed is clearly defined as an .xml file tho (like say digg.com’s feed).

    • Jonathan Dann 20:07 on January 25, 2007 Permalink | Reply

      Hi dwerg85!

      All I did was go to the “workflow steps” on the pane on the left of the screen. There’s one that says “Provide an RSS feed”, click the arrow and you the main window changes to the widget properties screen. One of the fields says “Feed URL”, into which I pasted

      feed://jonathandann.wordpress.com/feed/

      Hope this helps!

  • Jonathan Dann 20:47 on January 4, 2007 Permalink | Reply  

    Dashcode 

    On the 20th December, Apple released a public beta of ‘Dashcode’. A scaled-down version of that to be included in OS X 10.5 (Leopard). Over the next few days, I will be seeing just how easy it is to use, and begin to scratch the surface of what it can do.

    I’m no developer, I know a bit of C++ and FORTRAN, so this may help the average user to get started with makiing their own widgets for our favourite OS.

     
c
compose new post
j
next post/next comment
k
previous post/previous comment
r
reply
e
edit
o
show/hide comments
t
go to top
l
go to login
h
show/hide help
esc
cancel