Defining custom key path operators

(This was prompted by a recent question on StackOverflow.)

Key-value coding is one of my more favorite things about Cocoa (up there with NSPredicate). Basically, it allows you to access properties of objects by name (ie, as a string) rather than invoking a method (er… sending a message) directly.

Along with accessing properties, you can (with some limitations) retrieve calculated values (not strictly properties) about these objects. For example:

NSArray * anArray = .... //an NSArray of NSNumbers 
NSNumber * count = [anArray valueForKeyPath:@"@count"];
NSNumber * max = [anArray valueForKeyPath:@"@max.self"];

There are several others, like @sum, @avg, and some collection-oriented operators. For a full list, see this page in the documentation.

But what if you want something else? What if you wanted to do something like @size, which we’ll define as the size (in bytes) that the object directly occupies in memory? To answer this question, let’s see if we can figure out what’s going on with NSArray, and take some clues from that.

#import <objc/runtime.h>

unsigned int methodCount = 0;
Method * methods = class_copyMethodList([NSArray class], &methodCount);
for (int i = 0; i < methodCount; ++i) {
  Method m = methods[i];
  NSLog(@"%@", NSStringFromSelector(method_getName(m)));
}
free(methods);

When we run this, we’re going to get a bunch of methods listed, but there are some interesting ones that should catch our eye:

_distinctUnionOfSetsForKeyPath:
_distinctUnionOfObjectsForKeyPath:
_distinctUnionOfArraysForKeyPath:
_unionOfSetsForKeyPath:
_unionOfArraysForKeyPath:
_unionOfObjectsForKeyPath:
_minForKeyPath:
_maxForKeyPath:
_countForKeyPath:
_avgForKeyPath:
_sumForKeyPath:

Those look familiar! Specifically, they look like our collection operators that have undergone a bit of string manipulation. It looks like the @ has been replaced with an _ and the string ForKeyPath: has been appended. (These methods also appear on NSSet)

So theoretically, if we just add a method that matches this format (_ + {nameOfKeyPath} + ForKeyPath:), it should work!

A bit more introspection reveals that these methods return an object, which makes sense because valueForKeyPath: also returns an object (id). We need to do the same.

Here we go (we’d need an identical category for NSSet):

#import <objc/runtime.h>

@interface NSArray (CustomKVCOperator)

- (id) _sizeForKeyPath:(NSString *)keyPath;

@end

@implementation NSArray (CustomKVCOperator)

- (id) _sizeForKeyPath:(NSString *)keyPath {
  id keyPathValue = [self valueForKeyPath:keyPath];
  size_t instanceSize = class_getInstanceSize([keyPathValue class]);
  return [NSNumber numberWithInt:instanceSize];
}

@end

Now we can test this:

NSArray * array = [NSArray array];
NSLog(@"size: %@", [array valueForKeyPath:@"@size.self"]); //logs "8"

It worked! (Honestly, the first time I tried this I was totally surprised that it did)


While this example is pretty contrived, it’s not difficult to imagine a scenario when it could be useful. Unfortunately, this only works on collections. You can’t (for example) add a _sqrtForKeyPath: method to NSNumber and hope to use @sqrt as a keyPath operator on NSNumber. Bummer. :(

As always with things like this, a word of caution: this works, but only because we rely on some undocumented behavior. This is by no means something that would get an app rejected from the App Stores, but be aware that Apple can change the implementation of valueForKeyPath: at any time.

A safer (albeit more complex) way to achieve this same functionality would be to subclass the appropriate collection (NSArray or NSSet) and override the valueForKeyPath: method. However, this introduces new problems, since both are class clusters, and subclassing a class cluster is fraught with peril.

At any rate, if you find yourself wishing that you had @stdDev or @productOfEveryThirdNumber operators, now you know.

Followup to Localizing NSPredicateEditor

To expand a little bit on my previous post, here’s a little bit more information:

  • If you ever forget the format to put in the NSLocalizedString() macros, you can do the following:

    predicateEditor = ... //an NSPredicateEditor
    NSData * stringsData = [predicateEditor _generateFormattingDictionaryStringsFile];
    NSString * strings = [[NSString alloc] initWithData:stringsData encoding:NSUTF16StringEncoding];
    [strings writeToFile:@"/path/to/Predicate.strings" encoding:NSUTF16StringEncoding error:nil];
    [strings release];

    This is relying on an undocumented method of NSPredicateEditor, but will work fine to simply generate the strings data. As always, you should probably not use this in shipping code (especially with the imminent launch of the Mac App Store).

  • The .strings file used by the NSPredicateEditor must not contain any other localizations. In other words, you must use the file for localizing only the editor. If you need to localize other parts of your application, their strings must be in a separate .strings file. (The sample, mentioned below, uses the NSLocalizedStringFromTable() macro to specify a custom strings file called Predicate.strings)

  • I’ve created an example project (which was the demo shown during my session at MacTech Conference 2010) to illustrate both creating a custom NSPredicateEditorRowTemplate and localizing the predicate editor. You can download it from my website: http://davedelong.com/portfolio

  • Most of what learned regarding localizing NSPredicateEditor was gleaned from this email thread and the linked sample project.

Localizing NSPredicateEditor

Many people who know me personally (at least as a developer), know something interesting about me: I love NSPredicate. I love it so much that I stood up and blathered for an hour on how awesome it is. I won’t go into that here.

One of the things that make NSPredicate superbly awesome is the NSPredicateEditor. NSPredicateEditor, in a nutshell, is a way to visually build an NSPredicate, if you take the time to really dig in and understand what’s going on. Unfortunately that’s somewhat difficult, since the documentation on NSPredicateEditor is sparse, to say the least.

Customizing NSPredicateEditorRowTemplates is do-able, though you’re going to spend a few hours of trial-and-error programming getting it to work properly. However, if you want to localize the predicate editor, you are up a creek without a paddle. There is absolutely no documentation on it.

So, here’s a paddle.

First off, some introduction: localizing an NSPredicateEditor is a bit more complex than you would expect, because it works by displaying more-or-less natural-language phrases to the user. Localizing buttons and labels is easy, because we deal with isolated words such as “View file” or “Space used:” and so on. However, when you want to localize a whole sentence, suddenly you have to starting thinking about things like word order and the placement of prepositions in relation to nouns and verbs, and a whole host of other interesting challenges.

To illustrate: In English, when we meet someone new, we usually ask “Where are you from?” To analyze just the order of words, we have: Adverb verb pronoun preposition. In Spanish, we would ask “¿De donde es?”. Literally this means “From where are you?” (We could say this in English and people understand us, but it’s uncommon). In Spanish, the phrase is almost exactly backwards: Preposition adverb verb (with implicit pronoun). If we had a different view for displaying the adverb, and one for displaying the verb and pronoun, and one for displaying the preposition, then we would have to worry about rearranging our views to make the sentence readable. Saying “¿Donde es de?” (Literally: Where are you from?) may make sense if we were to literally translate this to English, but to a native Spanish speaker, you’re speaking nonsense.

Unfortunately, this is the precise problem we face with NSPredicateEditor. In English we might say “A is equal to B”, but it’s easy to imagine another language requiring something that’s more like “A and B are equal”.

To translate this discrepancy into NSPredicate-speak, we have to deal with “leftValue operation rightValue” versus “leftValue rightValue operation”.

Fortunately, there is a way to deal with this, and it relies on understanding three things:

  1. NSLocalizedString() and friends. I’m not going to cover this, since the documentation on how to localize is fairly straightforward
  2. printf-style positional specifiers. Here’s a basic example of what these are:

    NSLog(@"%2$@ %1$@", @"World", @"Hello"); //logs "Hello World"

    The “2$” and “1$” in the format specifier simply mean “use the second substitution value” and “use the first substitution value” (respectively). Easy.
  3. Using a special syntax for certain format specifiers (more on this later)

Now that we understand this, we can get on with localizing our NSPredicateEditor. We understand that we need NSLocalizedString() and that we can reposition substituted values using positional specifiers. Now let’s talk about the special format specifier syntax, in the context of another example.

For simplicity, let’s say we have an NSPredicateEditor with three row templates:

  • A compound row template ([Any, All, None] of the following are true)
  • A string row template (property [is, is not, contains, begins with, ends with, like, matches] a user-entered-string)
  • A selection row template (birthMonth is [January - December])

To properly localize these row templates, we simply need to produce some comments:

/**

NSLocalizedStringFromTable(@"%[Any, All, None]@ of the following are true", @"Predicate", @"localize the compound row template")
NSLocalizedStringFromTable(@"%[property]@ %[is, is not, contains, begins with, ends with, like, matches]@ %@", @"Predicate", @"localize the string row template")
NSLocalizedStringFromTable(@"%[birthMonth]@ %[is, is not]@ %[January, February, March, April, May, June, July, August, September, October, November, December]@", @"Predicate", @"localize the selection row template")

**/

Some explanation about what’s going on here:

  • Why are these in a comment?
    Because we don’t actually need this in the compiled code. We just need it in the source code so that the genstrings utility can find it. We won’t actually be using the NSLocalizedString() macros in our final, compiled version.
  • What’s with the square brackets?
    Because this is Objective-C, and we have to use square brackets everywhere, right? RIGHT???

In reality, the square brackets are what makes the magic happen. Each percent modifier (%@) represents a single view on the row template UI. Plain text (ex: “of the following are true”) is also represented as a view (specifically, an NSTextField).

For fields in a row template where you have multiple options, those options are comma-delimited within the square brackets. In the case of the compound row template, there are two fields: a popup that allows you to choose the kind of compound predicate (AND, OR, or NOT), and a field with some clarification text (“of the following are true”). The options for the template must match exactly (case, order, etc) as what you would see in the UI if you were to run your NSPredicateEditor without localization. If you mistype something, or mis-capitalize something else, that translation will not work.

Notice also that some fields don’t have anything to translate. The second row template shows “property”, some various string operators, and then %@. The %@ indicates that this will be replaced with something that cannot be translated. In this case, it would be an NSTextField, since we’re expecting the user to use this to enter some text.

When we run this code through the genstrings utility, we will get a file created (“Predicate.strings”) that contains this:

/* localize the compound row template */
"%[Any]@ of the following are true" = "%[Any]@ of the following are true";
"%[All]@ of the following are true" = "%[All]@ of the following are true";
"%[None]@ of the following are true" = "%[None]@ of the following are true";

/* localize the selection row template */
"%[birthMonth]@ %[is]@ %[January]@" = "%1$[birthMonth]@ %2$[is]@ %3$[January]@";
"%[birthMonth]@ %[is]@ %[February]@" = "%1$[birthMonth]@ %2$[is]@ %3$[February]@";
"%[birthMonth]@ %[is]@ %[March]@" = "%1$[birthMonth]@ %2$[is]@ %3$[March]@";
"%[birthMonth]@ %[is]@ %[April]@" = "%1$[birthMonth]@ %2$[is]@ %3$[April]@";
"%[birthMonth]@ %[is]@ %[May]@" = "%1$[birthMonth]@ %2$[is]@ %3$[May]@";
"%[birthMonth]@ %[is]@ %[June]@" = "%1$[birthMonth]@ %2$[is]@ %3$[June]@";
"%[birthMonth]@ %[is]@ %[July]@" = "%1$[birthMonth]@ %2$[is]@ %3$[July]@";
"%[birthMonth]@ %[is]@ %[August]@" = "%1$[birthMonth]@ %2$[is]@ %3$[August]@";
"%[birthMonth]@ %[is]@ %[September]@" = "%1$[birthMonth]@ %2$[is]@ %3$[September]@";
"%[birthMonth]@ %[is]@ %[October]@" = "%1$[birthMonth]@ %2$[is]@ %3$[October]@";
"%[birthMonth]@ %[is]@ %[November]@" = "%1$[birthMonth]@ %2$[is]@ %3$[November]@";
"%[birthMonth]@ %[is]@ %[December]@" = "%1$[birthMonth]@ %2$[is]@ %3$[December]@";
"%[birthMonth]@ %[is not]@ %[January]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[January]@";
"%[birthMonth]@ %[is not]@ %[February]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[February]@";
"%[birthMonth]@ %[is not]@ %[March]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[March]@";
"%[birthMonth]@ %[is not]@ %[April]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[April]@";
"%[birthMonth]@ %[is not]@ %[May]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[May]@";
"%[birthMonth]@ %[is not]@ %[June]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[June]@";
"%[birthMonth]@ %[is not]@ %[July]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[July]@";
"%[birthMonth]@ %[is not]@ %[August]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[August]@";
"%[birthMonth]@ %[is not]@ %[September]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[September]@";
"%[birthMonth]@ %[is not]@ %[October]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[October]@";
"%[birthMonth]@ %[is not]@ %[November]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[November]@";
"%[birthMonth]@ %[is not]@ %[December]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[December]@";

/* localize the string row template */
"%[property]@ %[is]@ %@" = "%1$[property]@ %2$[is]@ %3$@";
"%[property]@ %[is not]@ %@" = "%1$[property]@ %2$[is not]@ %3$@";
"%[property]@ %[contains]@ %@" = "%1$[property]@ %2$[contains]@ %3$@";
"%[property]@ %[begins with]@ %@" = "%1$[property]@ %2$[begins with]@ %3$@";
"%[property]@ %[ends with]@ %@" = "%1$[property]@ %2$[ends with]@ %3$@";
"%[property]@ %[like]@ %@" = "%1$[property]@ %2$[like]@ %3$@";
"%[property]@ %[matches]@ %@" = "%1$[property]@ %2$[matches]@ %3$@";

Man, that’s a lot of stuff to localize! If you look closely, you can see that every occurrence of NSLocalizedString has been expanded for all possible combinations. So if you have a row template that supports 3 different left expressions, 6 different operators, and 7 different right expressions, then you have to translate 126 different things (3 * 6 * 7 = 126).

One thing that genstrings has conveniently done for us is insert positional specifiers! This allows us to rearrange the order of the string in a translated version, but still get the appropriate value substituted in.

Now that we have this .strings file, we can translate it just like any other strings file, and we can also rearrange fields as necessary:

"%[Any]@ of the following are true" = "%[Cualquiera]@ de las siguientes son verdaderas";
"%[All]@ of the following are true" = "%[Todas]@ de las siguientes son verdaderas";
"%[None]@ of the following are true" = "%[Ningún]@ de las siguientes son verdaderas";

...

"%[property]@ %[is]@ %@" = "%1$[propiedad]@ y %3$@ %2$[son iguales]@"; //literally "property and [value] are equal"
"%[property]@ %[is not]@ %@" = "%1$[propiedad]@ y %3$@ %2$[no son iguales]@";
"%[property]@ %[contains]@ %@" = "%1$[propiedad]@ %2$[contiene]@ %3$@";

...

The final step (beyond including the translated strings files as resources in your application) is that you need to tell your NSPredicateEditor where to find this information. There are two ways to do this:

  • -[NSPredicateEditor setFormattingStringsFilename:]
  • -[NSPredicateEditor setFormattingDictionary:]

The first option is usually the way to go. You simply give it the name of your .strings file sans extension (in this case, @"Predicate") and you’re good to go. If it comes across a syntax error, it’ll log it so you can debug it. The problem with this approach is that it only looks inside [NSBundle mainBundle] for the resource. So if you’re writing a plugin (such as a system preference pane), this won’t work.

Fortunately, there’s the second option, and it works like this:

NSString * stringsFile = [[NSBundle bundleForClass:[self class]] pathForResource:@"Predicate" ofType:@"strings"];
NSString * strings = [NSString stringWithContentsOfFile:stringsFile encoding:NSUTF16StringEncoding error:nil];
NSDictionary * formattingDictionary = [strings propertyListFromStringsFileFormat];
[predicateEditor setFormattingDictionary:formattingDictionary];

This is pretty straightforward: load the strings file into memory, use a built-in method to convert it to an NSDictionary, and then give that dictionary to the predicate editor.

So there you have it! An actual explanation on how to localize an NSPredicateEditor! Hopefully anyone who comes across this will find it useful, since it was a total pain to figure out. :)

Enjoy!

Method Swizzling

Recently I wrote about how to dynamically subclass objects in Objective-C, and when that might be useful.

While dynamic subclassing can be really useful, there is one major gotcha that can make it not very effective, and that was hinted at the end of my post: Key-Value Observing.

When you add an observer to an object, the KVO mechanism will dynamically subclass that object to implement the observation logic. For example, consider the following class:

@interface DataObject : NSObject

@property NSInteger dataValue;

@end

When you observe an instance of DataObject<, a dynamic subclass is created. This class is called NSKVONotifying_DataObject, but even that is hidden from you. The KVO subclass is devious; it hides its existence. Consider this:

DataObject * dataObject = [[DataObject alloc] init];

[dataObject addObserver:self forKeyPath:@"dataValue" options:0 context:NULL];

NSLog(@"%@", [dataObject class]);  //logs "DataObject"
NSLog(@"%@", object_getClass(dataObject));  //logs "NSKVONotifying_DataObject"

In other words, the class of an observed object does everything it can to look and behave exactly like the original class. For the most part, this is appropriate behavior. However, when you want to implement your own dynamic subclassing mechanism, it can wreak havoc. A dynamic KVO subclass does not like to be subclassed, because when you remove all the observers from the object, the class of the object is changed back to its original class. This means that even if you manage to dynamically subclass a KVO class, those changes will be lost when the class of the object is changed back to its original class.

So in the case of CHLayoutManager, a different mechanism for automatic cleanup was needed, since it is not uncommon to observe a UI element in order to react to state changes.

After some experimentation, I came up with an alternative: replace the -dealloc method of NSView with a new version.

Now, I don’t have the source to AppKit. I don’t know what NSView is doing under the hood. Fortunately, with Objective-C, that’s not really a problem, because there’s this really neat function in the runtime called method_exchangeImplementations, and it works like this:

+ (void) initialize {
  if (self == [CHLayoutManager class]) {
    Class nsview = [NSView class];

    SEL dynamicDealloc = @selector(chlayoutautoremove_dynamicDealloc);

    Method newDealloc = class_getInstanceMethod(self, dynamicDealloc);
    if (newDealloc != NULL) {
      class_addMethod(nsview, dynamicDealloc, method_getImplementation(newDealloc), method_getTypeEncoding(newDealloc));
      newDealloc = class_getInstanceMethod(nsview, dynamicDealloc);

      if (newDealloc != NULL) {
        Method originalDealloc = class_getInstanceMethod(nsview, @selector(dealloc));
        method_exchangeImplementations(originalDealloc, newDealloc);
      }
    }
  }
}

- (void) chlayoutautoremove_dynamicDealloc {
  if ([self isKindOfClass:[NSView class]]) { //to prevent people from being stupid
    [[CHLayoutManager sharedLayoutManager] removeConstraintsFromView:(NSView *)self];
    [[CHLayoutManager sharedLayoutManager] setLayoutName:nil forView:(NSView *)self];
    //THIS IS NOT A RECURSIVE CALL
    [self chlayoutautoremove_dynamicDealloc];
  }
}

This is a two step process:

First, we find the NSView class and add a new method to it. This method, called -chlayoutautoremove_dynamicDealloc simply performs cleanup with the layout manager singleton. After we add the method to NSView, we swap the implementations of -dealloc and -chlayoutautoremove_dynamicDealloc. This means that -[NSView dealloc] will invoke the implementation of -chlayoutautoremove_dynamicDealloc, and -[NSView chlayoutautoremove_dynamicDealloc] will invoke the original -dealloc code. Essentially, this is more or less injecting code into NSView that will be executed right before the -dealloc code gets executed.

Neat!