Tagged: Cocoa

Out of Context

Twitter’s been having a discussion about tech job interviews recently: you can see my contributions here, here, here, and here.

For the tl;dr crowd, my take is that interviews are extremely difficult, and so you (and I) should have some empathy.

A while ago, I did a bunch of interviews for a junior iOS developer. I gave them what is apparently a quite common exercise: in a simple iOS app, get some JSON data from a server and use it to populate a table, including, for each entry, a link to an image file to download separately.

I just made such a project, called Numbers, available here.

When you first run it, it shows a table filled with entries 1-20, where the brightly-colored number icons are each loaded separately.

A button to the right of the navigation bar is labeled “Wrong”, meaning you’re currently using the image loading implementation that’s incorrect.

If you scroll to the bottom of the table quickly, you’ll see that initially, the rows you uncover will be temporarily populated with incorrect icons:

Screenshot of "Numbers" application table view, scrolled to bottom, with rows 16 through 20 having incorrect icons 2 through 6.

That’s because, in the incorrect implementation, the async network calls insert the loaded images into the cells that originally requested them.

But, as Everyone Knows, cells in UITableView are reused when you scroll, which means by the time the network calls finish, the original cell might be in use for at a different row, and it shouldn’t display the original row’s contents.

Instead, the network call for an image should update the cell that currently represents the row.

If you tap the “Wrong” button, it will change to the text “Right”, and that’s how the application will behave when it reloads the table. Scrolling quickly to the bottom of the table won’t result in erroneously populated images anymore.

When I was interviewing, if the interviewee said they knew table views, I would give them this exercise, and would consider them not worth hiring if they made that rookie mistake.

Nowadays, it’s clear to me that this is a Gotcha! question like any other.

Instead of it being a question about how they think, how they solve problems, it requires a very specific piece of information you either have it your head at that moment, or you don’t.

In a recent interview where I was debugging a problematic table view implementation, I failed to recognize a similar incorrect image loading mechanism — until nudged to do so by the interviewer. I just didn’t see it. If that interviewer had been as quick to judge as I had been in the past, I wouldn’t have gotten the job.

As a final note of curiosity, you might notice that, in the Numbers project, I reset the NSURLSession each time before reloading the table view’s contents. That’s because NSURLSession has its own cache of network call results, and if I didn’t reset it, you would only be able to reproduce the “wrong” behavior the very first time you tried it. Every subsequent time (including across app relaunches), the images would “load” instantaneously from the cache.

While I would never recommend shipping the wrong implementation, even if you did, these days, Apple’s frameworks would mitigate its impact.

Naming the Beast

I’ve been writing code for a command-line input buffer.

Seems a bit odd in this day and age, eh? OS X already has a command line implementation – several, in fact.

But this one’s for my home project, which is a curious combination of Web browser and command line tool.

Per the Windows port, this command line acts a little odd. You can move up and move down in the command history using the up and down arrow keys, but if you modify any of the previous commands, those changes don’t “take”. Only changes to the last command are remembered if you move up and down the history.

Oh, and the current command – the one you’re looking at on the screen at any particular moment – needs to be held in an external buffer controlled by the cross-platform engine.

I have an NSArray of all the previous commands, and logic to copy the proper entry in that array to the buffer if the user moves up and down through the command history, but that last command line makes things a bit more fiddly. If you move up, I have to remember the contents of it, even though I have to fill the external buffer with new contents. But I don’t want to add it to the history, because you haven’t pressed return yet.

I said “fiddly,” not hard. My particular solution was to add it to the history array anyway, for the benefits I get from reusing the code that copies from the array to the buffer. As soon as you move up, we enter this special “extra entry added” mode, which we leave again if you move all the way back down again, or hit return.

So I knew that whenever the current command index didn’t point to the very end, I was in my mode, but I found myself getting a little confused, not able to nail everything down, even though the code itself wasn’t very complex. I knew what I wanted, but the code didn’t look like what I wanted.

The solution (and the point of this long-winded entry) was all about naming. Instead of the logic looking like this:

if(mCurrentCommandIndex < [mCommands count])

I made it look like this:

if([self bufferTemporarilyAddedToCommands])

and exiting the mode, instead of looking like this:

[mCommands removeLastObject]

I changed to look like this:

[self removeBufferTemporarilyAddedToCommands]

Not rocket science, but it allowed me to see where the code was doing its job, and where it wasn’t, and make the appropriate corrections.

Oh, that, and unit tests.

P.S. Yes, I use PowerPlant-style variable prefixes in my Cocoa code. So sue me.

The Cat That Ate the Rat…

Writing bugs often makes me more productive.

That, or writing requests for help.

For instance, I have a custom NSView, whose contents, mostly text, are completely laid out by a cross-platform engine, i.e. not by Cocoa. So I can’t use NSTextView.

But I want it to act like an NSTextView. For instance, I want to be able to select text just like an NSTextView, and I want a blinking cursor just like an NSTextView.

So I started writing up a query to the Cocoa-dev mailing list about this. But you don’t get much respect, or much help, if you aren’t as specific about what you need as you can be.

So first, I had to determine what I didn’t know. Which meant trying to implement it, and documenting where I got stuck.

Turns out, I didn’t get stuck, at least for the first part.

How do I get started learning about something new? Well, if I can guess an API name, I’ll start with the documentation, but usually I can’t. So I search http://search.lists.apple.com based on the words I do know, like “selected text color”. If there are a lot of false matches from other frameworks, I’ll restrict the search to Cocoa-dev, but sometimes the answer is, say, a Carbon API, so I’m inclusive at first.

I find that the spare, high-level style of Apple’s developer documentation sometimes leaves me scratching my head. Is this really what I need? The mailing lists are great for this sort of clarification.

So I find out the API I need for the selected text background is the appropriately-named:

-[NSColor selectedTextBackgroundColor]

And the post that gave me that also pointed me to the “Accessing System Colors” documentation topic, which helpfully mentioned the notification:

NSSystemColorsDidChangeNotification

which allows me to change the color in my application immediately if the user chooses a different highlight color from System Preferences.

But I wasn’t done because, when an application window is no longer foremost, the selection color changes from the user-choosable highlight color to a gray color.

I was unable to find a specific question about that, but I found another post that talked about matching API colors via the Developer color palette in the color panel. (Cmd-Shift-C in TextEdit.) There isn’t a nonForemostSelectedTextBackgroundColor system color, but there is a secondarySelectedControlColor that matches the non-foremost selected text color in TextEdit, and has a name that at least could apply to my situation.

So I have something that works, and gets its information as much from the system as possible, so it’s as future-proof as possible while still being custom. And I didn’t need to ask any questions! Except…

The same routine didn’t get me an answer as to the best, most system-friendly way to show a blinking caret. I could do the whole thing myself, but that might look weird in the radically overhauled UI in Mac OS X 10.9 “Puddy Tat,” eh? And I’d rather not have an NSTextView whose sole function is to show that caret – seems like a fragile, hacky solution.

So I have a question after all.

Omnipreferential: Rolling My Own Prefs Window, Instead of Using Omni Frameworks

After a long hiatus, I am once again looking at the project that first got me using both the Omni frameworks and unit tests.

Why now? An application already exists that does what my project’s app does, but that existing app is Classic-only. Classic seemed to be moseying along just fine until splat! it hit the brick wall of Macintel. No more Classic.

I was using the Omni frameworks primarily for how they made your application’s preferences window easier to implement. The trouble is, the Omni framework projects are sorely outdated. The version you’re supposed to use don’t have native targets, which is a requirement for building Universal binaries.

So it’s time to give them the heave-ho. Does that mean I have to make my own preferences implementation from scratch?

One alternative is to use the Preference Panes framework from Apple. It’s primarily for System Preferences panes, but the documentation makes it clear that it can be used for application preferences, too. The trouble? If you use it for your own application, you have to manage the window and the toolbar yourself. Since that’s most of the work, and since, if you use the Preferences Pane framework, you need to package each pane as a separate bundle, which is even more work, I give it a pass.

Uli Kusterer has UKPrefsPanel and UKToolbarFactory available in the source code portion of his Web site. They look promising. The prefs panel even uses an NSTabView as an easy way to deal with the prefs panes, which is how I was thinking of doing it. But it doesn’t support automatic resizing of a preferences window to the height of each individual preference pane, which is something I definitely want.

In fact, I want my preferences window to look and behave exactly like the preferences windows of standard Apple applications like Safari and Address Book.

So I decide to work directly with NSToolbar, and see what sort of progress I can make.

Apple’s ADC Web site has ToolbarSample source code that’s a good starting point.

Following that sample, I make the NSToolbar myself in code, and add it to the window by sending the window the message setToolbar:. That API actually expands the window by the height of the toolbar: I don’t have to leave room for it myself in the window’s nib. The window also fully takes care of the user hiding and reshowing the toolbar, and saving the selected toolbar item, as long as I send it a setAutosavesConfiguration:YES message. Neat.

There are a lot of things that need some tweaking, though:

• I want to use an NSWindowController to encapsulate the preferences window nib, a straightforward Cocoa technique. But the controller doesn’t seem to use NSWindow’s frameAutosaveName value, which is settable in the nib. In fact, it seems to set it to nil. Instead, if I want to go that route (which I don’t, see below) I need to set the controller’s windowFrameAutosaveName value, which can only be done in code. Annoying.

• The preferences windows of Safari et al erase themselves and then use a cool drawer-like animation to resize between preference panes. I can get the latter using setFrame:display:YES animate:YES, but not the former. I handle this by adding an extra, empty NSTabViewItem after all the other meaningful ones. I switch to that, do the resize, then switch to the NSTabViewItem I really want afterwards.

• I want the individual preference pane’s size info to be in the nib, not something I have recompile to alter. But every NSTabViewItem within an NSTabView is the same size. I work around this by putting my own custom NSView inside each NSTabViewItem, and then putting all my widgets in that subview.

In my code, I check for that subview’s size, and resize the window to match it, when switching to that NSTabViewItem.

• I need to be careful with resizing: it order to get the the effect I want — which is that the window title bar stays stationary, and the bottom of the window slides up or down — in the normal Quartz coordinate system, I need to change both the window size and its origin, because the origin is in the lower left-hand corner of the window. So, every time I select a toolbar item, as far as Cocoa is concerned, I’m both resizing and moving the window. But NSWindowController doesn’t catch this, leading to erroneous positioning when I close and reopen the application.

• If I handle user defaults manually instead of relying on NSWindowController, I can save the window’s frame to the user defaults (a) when I choose a new toolbar item (see the sample for how to associate toolbar items with actions), (b) when the window is moved around by the user (via the notification NSWindowDidMoveNotification), and (c) when the window is resized (via the notification NSWindowDidResizeNotification). Now, the window is not user-resizable, but this notification is still invoked when the user shows or hides the toolbar, because that automatically changes the size of the window.

But why do that much work? Instead, I listen for the notification NSApplicationWillTerminateNotification, and save off the window frame then. Since user defaults are generally only saved to disk upon application termination, this doesn’t lead to a mismatch even if you force-quit the application, because neither the toolbar user default changes that have already happened, nor my code’s user default changes that haven’t happened yet, are saved.

Authority Issues II: The Dialog That Wouldn’t Die

Last time around, I talked about the Security framework. It is a set of “beneath the hood” C-based APIs you can use from Carbon or Cocoa.

In addition, there’s the Security Interface framework, which is a Cocoa framework of standard security UI widgets.

One of the widgets in this framework is the SFAuthorizationView, which looks like so in its unauthorized and authorized states:
SFAuthorizationView, locked iconSFAuthorizationView, unlocked icon

You tell it which right you want it to use, and it manages an AuthorizationRef for that right. It’s a very useful widget, but it has a gotcha.

You add an SFAuthorizationView to a window in your nib file by doing the following:

1) Drag the SecurityInterface/SFAuthorizationView.h header to your nib.

2) Make a custom view.

3) Change that custom view’s custom class to SFAuthorizationView.

Here’s some code for a delegate class for such a window. It assumes the delegate has an SFAuthorizationView outlet called “authorizationView”:

- (void)awakeFromNib
{
   [authorizationView setString:"right.to.party"];
   [authorizationView updateStatus:self];

   [authorizationView setFlags:
      kAuthorizationFlagInteractionAllowed |
      kAuthorizationFlagExtendRights];

   [authorizationView setAutoupdate:YES];
}

The first two messages are necessary to set up the authorization view. Without a specified right, the view can’t function. Without the updateStatus: message, it won’t show the lock icon correctly.

You’ll notice that the value for the setFlags: message is the same value I used for the second AuthoriaztionCreate)() function call in my last post. It serves the same purpose here that it did there.

But it’s the last call that’s the gotcha. You send the message setAutoupdate:YES if you want the authorization view to automatically change its appearance and behavior when, for example, an authorization expires, which they normally do after 5 minutes. So far so good.

But if that is combined with the flag kAuthorizationFlagInteractionAllowed, and if a user decides to cancel out of an authorization dialog instead of typing in a valid password…the then dialog comes back. Cancel it again, it comes back again. If you don’t have the password — say, for example, you’re not an adminstrative user for the Mac in question — the only way to get the dialog-that-won’t-die to go away is to force-quit the entire application.

Not good. There are two workarounds for this gotcha:

1) Don’t use kAuthorizationFlagInteractionAllowed. This means that when an update occurs, for example an authorization times out, the user won’t be prompted to authorize again immediately. Instead, the lock icon will just go ahead and lock itself. There isn’t a “nyah nyah” sound, but there might as well be.

2) Don’t set your view to autoupdate. You might do this if you’re using a right that, say, never expires.

For anyone using or thinking of using the SFAuthorizationView widget, I hope this helps.

Authority Issues: Gotchas with the Security Framework

You see those pesky authorization dialogs every so often, right? "Finder requires that you type your password." "BBEdit requires that you type your password." This means the application involved is using the Security framework to perform an action with, say, administrative privileges that it wouldn’t normally have.

If you turn down the Details disclosure triangle in such a dialog, you see the name of the app and an arcane Requested Right string.

These rights should be added to what’s called the "policy database," stored in the file /etc/authorization, when the application that wants to use them is first installed. For a full explanation, see Performing Privileged Operations With Authorization Services, but for this post, I’m interested in the API you use to add those rights.

Here’s the example given in the AuthorizationDB.h header in the Security framework.

OSStatus status =
   AuthorizationRightSet(
      NULL,
      "com.ifoo.ifax.send",
      CFSTR(kAuthorizationRuleIsAdmin),
      CFSTR("Authorize sending of a fax"),
      NULL,
      NULL);

Looks reasonable, right?

Bzzzt. Sorry! If you actually try to run this code, at least on Mac OS X 10.4 "Tiger," you get a −2147418108 error. What’s a −2147418108 error? No clue. But it’s not good.

The first parameter is of type AuthorizationRef. You get a valid authorization reference if the user types her password into one of those dialogs, but you don’t need special permission to add a new right to the policy database. Hence the null value in the example. But it doesn’t work.

Here’s what you need to do instead:

AuthorizationRef authorization = NULL;

OSStatus result =
   AuthorizationCreate(
      NULL, NULL,
      kAuthorizationFlagDefaults,
      &authorization);

if (result == noErr)
{
   result =
      AuthorizationRightSet(
         authorization,
         "com.ifoo.ifax.send",
         CFSTR(kAuthorizationRuleAuthenticateAsAdmin),
         CFSTR("Authorize sending of a fax"),
         NULL,
         NULL);

   AuthorizationFree(
      authorization,
      kAuthorizationFlagDefaults);
}

AuthorizationCreate() actually has a whole bunch of options you can use, but here I’m using it with minimal values to create a blank, good-for-nothing AuthorizationRef. Even so, that’s good enough for AuthorizationRightSet(). With this code, there’s no −2147418108 error.

So we’re done? Not yet!

You’ll notice above that I changed the third, "rule" parameter of AuthorizationRightSet() from the kAuthorizationRuleIsAdmin in the original sample to kAuthorizationRuleAuthenticateAsAdmin. With that rule, you are shown an authorization dialog when I use AuthorizationCreate() like so:

AuthorizationItem myItems =
   { "com.ifoo.ifax.send", 0, NULL, 0 };

AuthorizationRights myRights =
   { 0, &myItems };

AuthorizationFlags myFlags =
   kAuthorizationFlagInteractionAllowed |
   kAuthorizationFlagExtendRights;

AuthorizationRef authorization = NULL;

OSStatus result =
   AuthorizationCreate(&myRights, kAuthorizationEmptyEnvironment,
      myFlags, &authorization);

This call’s a lot more complicated, eh? But it boils down to trying to get a valid authorization for the right we added.

Here’s the dialog you see in response:

Authorization dialog with erroneous caption

Oops! If you fill in the fourth, "description key" parameter of AuthorizationRightSet() with a string as shown in the header example, that "description key" is concatenated with the default system prompt in any authorization dialog that refers to that right.

Passing NULL for the "description key" parameter uses just the default system prompt, which works much better.

Now we’re done.

Banana Split: Overcoming Problems with NSSplitView

There are a bunch of bugs or misbehaviors in the current (10.4) version of NSSplitView. Here are the ones I’ve needed to solve:

(1) While the split view respects the minimum and maximum coordinates if you drag the divider around yourself, it doesn’t respect them if you’ve set up the view to resize with the window, and you shrink the window.

(2) If you collapse a subview, any view in it that has keystroke focus retains it. You can still type in invisible text fields, navigate invisible tables, etc. Plus, all the text fields, tables, etc. inside the collapsed subview can still be tabbed through.

(3) The coordinates of the subviews are not automatically saved and restored, the way the column sizes and positions in a NSTableView are.

In my recent project, I fixed all these problems with delegate methods. If you implement splitView:resizeSubviewsWithOldSize: and do the resizing yourself in the problem case, you can fix (1). If you implement splitViewDidResizeSubviews:, you can do (3) and, if you check for when a subview has collapsed, you can make sure (2) doesn’t happen.

However, if you want to write less of your own code, there are other options. Even with a cursory search, I have found three NSSplitView replacements:

OASplitView from the Omni frameworks. Is a subclass of NSSplitView. Solves only (3).

KFSplitView from Ken Ferry. A subclass of NSSplitView. It solves (1) and (3). (2) still happens in its demo.

RBSplitView from Rainer Brockerhoff. Not a subclass of NSSplitView. Solves (1) and (3). Solves the first part of (2), but not the second.

Admittedly, the second part of (2) might not be possible in general-purpose framework code, since it can involve an invasive solution. I solved it in the code I was working on by subclassing the NSTableView located in the subview. In the subclass, I added an _acceptsFirstResponder flag, a setAcceptsFirstResponder: method to set the flag, and overrode acceptsFirstResponder to use the flag.

With these changes, it was easy to take a table out of the tab chain when the subview was collapsed, by merely sending it the set message. It would have been more onerous if I had to subclass multiple classes to get this same behavior in different types of widgets, in order to send the same set message in them all.

You may be able to get the same behavior out of some controls by sending them a setEnabled: message, but that wasn’t successful for me with tables.

Untied III: Furniture (Table) Polish

I used my last post to concentrate on the features I could implement with a data source-backed table, but not with a Cocoa bindings-backed table.

But the Category02.zip sample code from my last post has more to it than that.

For instance, in the data source-backed table, if you add a category – which starts with a blank name – and hit return without typing anything in, it deletes the category completely, under the assumption that you must not have wanted it if you weren’t going to give it a name.

However, if you erase the name of an existing category and hit return, it reverts it to the old name, just like the Finder does. This is a nice “cancel editing” shortcut, since Undo may not take you back all the way to the original name if you type enough.

One bit of polish I didn’t implement, but which I would want in a real app, is Escape key support. If you hit the Escape key or Command-period, it should abort editing.

I have a tendency to overdo my sample applications, so I think I’ll stop here with the category table features. Next up: the detail view of “thing” entries. (See the original post for the overall description of the Categorizer application.)

Untied II: Table Data Source Tricks

Last time around, I concentrated on what I could and couldn’t get a Cocoa bindings-based NSTableView to do for me.

Today, I’m going to do everything I mentioned last time, using the data source and delegate methods in listed in NSTableView.h.

As a starting parameter, I said that I wanted a special, unmodifiable, always-on-top Category entry in my table, called “All”. Making it uneditable is as easy as implementing the method tableView:shouldEditTableColumn:row: in my delegate class, and making it return NO if row == 0. I like this way better than making an “editable” Category accessor, because it puts the responsibility where it belongs, on the specific table that has the special entry, rather than putting a hack in the Category class to handle it.

Then, the issues. First up was sorting. I sort the contents of my Categories table the way I want, without having the pesky user being able to screw it up, by manually sorting the table’s contents in the data source method that’s called every time the user adds or changes a table entry, tableView:setObjectValue:forTableColumn:row:.

Because I never set the Sort Key in Interface Builder, as far as the table itself is concerned, the contents are completely unsorted. There’s no ascending or descending triangle in the column header.

Scott Anguish, in the comments of “Untied I”, suggested overriding NSArrayController to get the same behavior, and with another tweak (see that post’s comments) that does indeed get me exactly the same behavior. I implemented his suggestion in the Bindings part of the project that comes with today’s post. I complained in my reply that clicking on the column header still ends the editing session. Just a minor annoyance, to be sure, but the Mac is all about getting things exactly right. But it turns out my data source implementation has the same problem, so it’s a draw.

Second was editing, in three parts: starting an editing session immediately on add, resorting the entry immediately after the editing session ends, and beeping and not ending the editing session if the name is invalid.

To understand how I start the editing session, you need to know how I implement the action newCategory:. Before, this invoked the NSArrayController’s add method, which was asynchronous. Now, it does all the data model manipulation itself, and so it can go ahead and start the editing session without any worries afterward.

Resorting the array takes place, as stated above, in the data source method tableView:setObjectValue:forTableColumn:row:. The same code that does it when you first add a category also works when you rename a category.

For the last part, editing verification is available by implementing the NSControl delegate method control:textShouldEndEditing:. This method is not called if I’m using bindings.

The sample is called Categorizer02.zip.

Untied: When Bindings Aren’t Enough

I’ve found, when I try to use Cocoa bindings for my user interface, that it’s never quite flexible enough for what I need.

Instead of trying to list the hurdles I’ve encountered in the abstract, I’m going to walk you through the first steps of a simplified example, called Categorizer, which is also available for download. This way, you can see the things I’ve figured out how to do, in addition to the dealbreakers.

Categorizer initial table

The Categorizer’s data model consists of Things and Categories.

The UI pictured above is the master table, of Categories. The detail table, of Things in the selected Categories, will be to the left. We won’t get to the detail table today.

The first category, "All," is special: you can’t delete it or rename it. It will contain all the things, and is always listed at the top of the category table, which is otherwise alphabetically sorted. You "categorize" things by dragging them onto the rows you’ve made yourself in the category table.

Sorting
The first issue is sorting. As I said above, categories need to be sorted alphabetically except for the "All," which is always on top.

NSArrayController will automatically sort its array – the contents of the table – according to the Sort Key and Sort Selector of the columns in the table, which are set in Interface Builder. The Sort Key needs to be set to something, it can’t be left blank if I want sorting. So I can’t, for example, try to sort on the entire Category class; I need to pick an attribute. Normally I’d pick something like "name," but in my case I want to into account the special "All" status and name. So I make a "specialSortKey" Category method and specify that in IB.

The table doesn’t start out sorted. I fix this not by manipulating the table, but by manipulating the NSArrayController. I get the sortDescriptorPrototype from the column, stick it in an NSArray, and use it as the parameter for a setSortDescriptors: message to the controller. (See the download for the exact code.) This also selects the column header and sets the indicator image (the little up or down arrow).

Turns out, though, that I don’t want this to act like a normal table column. I want the contents to be sorted the same way all the time, so I don’t want the up or down arrow in the column header that the user can toggle between. Because this would put "All" at the bottom, for one. But because of the array/controller integration, it’s basically not possible to have one without the other if I want to use an NSArrayController. This, it turns out, is dealbreaker #1.

Editing
I also want specific behavior when adding a new category. The standard NSArrayController behavior, if I connect the "Add" button to the controller’s add: action, is that a new, empty instance is added to the array and to the table. What I want, in addition, is that you automatically start editing that instance.

My first thought is to connect the "Add" button to a method in my own delegate, newCategory:. This method invokes the controller’s add: method and then starts the editing session.

Unfortunately, the new row is not yet made by the time add: returns. I’m not sure why this would be an asynchronous process, but it is. Oh, wait, here we go, from the documentation: "Note: In Mac OS X v10.4 the target-action methods are deferred so that the error mechanism can provide feedback as a sheet."

I then try implementing MyDelegate’s key-value coding method insertObject:inCategoriesAtIndex: so that, after it does its business, it initiates editing of the added row. But for some reason (maybe because it’s a horrible hack) that doesn’t work either.

This, so far, is dealbreaker #2.

After I’m done editing, I want the renamed category to be re-sorted properly. This is pretty easy, but still involves a gotcha. On 10.4, the controlTextDidEndEditing: NSTableView delegate method is never invoked when you double-click a table cell and edit it in a table that uses an NSArrayController. In my tests, it seems to still work in a table that doesn’t use an NSArryController.

Luckily, there’s another Cocoa API in a superclass of NSTableView, NSControl’s controlTextDidEndEditing:. If I define that in my table delegate, I can use it to send the rearrangeObjects message to my controller, and the renamed category will be immediately resorted.

My last editing issue, and last issue for this post, since it’s getting long, is uniqueness. I don’t want you to be able to enter the same category name twice. If you try to end editing with an invalid name, I want the UI to beep and continue the editing session.

I try to do this in the key-value coding way by implementing the "name"-specific validation method validateName:error: in Category. There’s some finagling, since now Category needs to be able to access the full list of categories in MyDelegate. But, with that achieved, Category’s validateName:error: now returns NO if the new name is the same as any other name.

Can I make it beep? Nope. My only choice of error feedback for the user is an error dialog. There are plenty of options in NSError so I can customize what the dialog says, but no option to do something else.

Dealbreaker #3.

The download, called Categorizer01.zip, contains the work I’ve described so far, including the behavior I couldn’t get working right. It’s an Xcode 2.1 project, sorry, it can’t be opened in Xcode 2.0.

If my schedule permits, soon I will make a second Categorizer, one that doesn’t use Cocoa bindings, that customizes the behavior as I wanted.