More Than Token Problems

One of my favorite typos from when I worked at Apple was “NSTokenRage”. Rage…over tokens? I don’t know what that was about, but I’ve certainly had my share of puzzlement, frustration, and yes, rage, over NSTokenField, and I have the bugs to prove it.

So let’s dish.

I’ve found three big problems with it.

Tabbing Out

First, tabbing out doesn’t invoke the same token validation behavior as typing in a tokenizing character within the field, such as a comma or a return.

In the latter case, the delegate’s tokenField:shouldAddObjects:atIndex: method is called, and so the delegate can choose whether to tokenize the user’s input or not. In the former case, however, it is not called, nor is any other standard delegate method. Tokenization of the user’s input just…happens.

Here are screenshots from the sample app I submitted with the bug. In the token field, duplicate tokens are not allowed, and if you type in duplicate text, it should disappear instead of being tokenized. Here’s an example, where I’m about to enter the duplicate text “b”:

If I then type return, focus stays in the token field, and the duplicate token is rejected and disappears:

But if I type tab, which changes focus to the text field below the token field, the duplicate token is erroneously created:

The sample app also includes a workaround.

Each token does a round trip from the string the user initially types, to a “represented object” for that string once that string becomes a token, to a display string for the represented object used when drawing the token.

The string → represented object data is provided by the delegate method tokenField:representedObjectForEditingString:, and the represented object → display string data is provided by the delegate method tokenField:displayStringForRepresentedObject:. The workaround is for the first method to return a represented object indicating that the string is invalid (I used NSNull), and for the second method to return nil for the display string for such an invalid object. While this doesn’t keep the invalid represented object from being added to the list, it does prevent it from being displayed.

The downside of this approach is that we get entries like these in Console for the application:

2011-03-13 17:20:47.668 Token Workaround[2819:903] : Failed to query display string for a represented object . Ignoring…

The bug for this issue is rdar://9000060.

Unheralded Delete

A related problem is that there is no delegate method that is called when the user deletes a token, no tokenField:willDeleteObjects:.

Without this, it becomes harder to enforce a no-duplicates rule. The workaround I used was to implement the NSControl delegate method controlTextDidChange:. It’s a lower-level access point, but at least it’s guaranteed to catch all editing changes.

The bug for this issue is rdar://9000191.

Keystroke Validation

The last major issue I saw was an inability to do keystroke validation by the standard Cocoa technique of subclassing NSFormatter.

Apple’s docs list three methods you need to override in a subclass:

  • stringForObjectValue:
  • getObjectValue:forString:errorDescription:
  • attributedStringForObjectValue:withDefaultAttributes:

Only the last actually has to do with keystroke validation. The first two do the same sort of round-tripping from display string ↔ represented object that we saw in NSTokenField itself.

The trouble is, NSTokenField has its own internal attributed string format that is impossible (or seems like a bad idea to me, anyway) to try to replicate. It would be nice if there were a way to just implement the third method, and tell the formatter that, hey, the existing formatting is just fine, thanks. But as far as I know, there isn’t.

I got around this myself by implementing a custom field editor and overriding a private method that is called to actually set the text. I’d much rather have a better way.

The bug for this issue is rdar://9092238.

There’s also the more minor issue that it seems to be impossible to set the right arrow key as a tokenizing character (rdar://8849747).

And that’s all I have to say about NSTokenRageField.