NSPredicateEditor tutorial

Cocoa offers a nice visual editor for editing NSPredicate objects templates, called NSPredicateEditor. The NSPredicateEditor can be set up using code or in Interface Builder, which is preferable for simple use. The setup is fairly easy once you know how to do it. In this tutorial, we’ll be building a simple predicate editor example which shows the basic functionality of the predicate editor.

Setting up the AppDelegate

Begin by creating a new Xcode project (Cmd+Shift+N). Name your project wisely and create a new class in the Classes group, called AppDelegate.

Switch to the header file and declare two IBOutlets for the main window and the sheet on which we’re going to display the editor in a few minutes. Also, add two IBActions called -openEditor: and -closeEditor:. Finally, add an ivar that holds the NSPredicate we’re going to be editing.

Your project should look like this

Next, we’re going to fire up Interface Builder to build the UI. Double click on the MainMenu.xib file under the Resources group.

Drag an NSObject object from the Library into the XIB and call it App Delegate. Hit Cmd+6 and make it a subclass of the AppDelegate class we just created. Then, hook it up to the delegate property of the File’s Owner.

Make the object a subclass of the just-defined AppDelegate classHooking up the AppDelegate to the application

Drag a new NSWindow to the XIB-file and call it Sheet. Make sure the checkbox “Visible At Launch” is deselected or the sheet will not display properly at runtime. Open the main window and add a NSButton and a NSTextView to it. To the sheet window, drag a NSPredicateEditor and a NSButton. They should look somewhat like this now:

The main windowThe predicate editor window

Now, we can hook up the outlets and actions as usual. Hook up the Edit Predicate button on the main window to -openEditor: and the OK button on the sheet window to closeEditor:. Then hook up the mainWindow and sheet outlets of the AppDelegate class to the respective NSWindow objects.

Hooking up the windows

Configure the NSPredicateEditor

Once we have all of the connections between Xcode and Interface Builder set up, we can continue to configure the predicate editor itself, which is actually what this tutorial is all about. An NSPredicateEditor control uses a list of NSPredicateEditorRowTemplate objects that can handle individual (simple) NSPredicate objects. Combining these row templates enables the NSPredicateEditor to edit compound predicates. There is no limitation to the depth of nested compound predicates, although nesting too deep would not be advisable from a usability perspective.

In the edit window, click a few times until the “name contains” row template is selected. In this row template, you define which key paths are supported. Supported here means two things:

  • matching—given an existing predicate with this key path in it on the left-hand side, this row template can be used to alter the predicate;
  • generation—when using the editor to create new predicates, adding a new rule for this key path will generate a predicate for this key path.

Select the row templateDefine the left-hand sides that this row template can handle

Gotcha

A small gotcha, at least one that initially put me on the wrong foot, is that there is quite a difference between the rows that you see design-time in Interface Builder and the rows that are available run-time. At design-time, you define the NSPredicateEditorRowTemplate objects while at run-time you see instances of them. Hence, the number of rows at design-time is the number of different row templates available. At run-time, however, the number of rows is the number of (simple) predicates within the compound predicate (which each has an associated row template instance that handles it). Subtle difference.

In short, in Interface Builder, create a row template for each type of match that you want to allow. Typically, this means for each data type that you want to support. In our example, we have the following setup:

  • Row template #1 is for all string matches. Here, we have defined it for the key paths “firstname”, “lastname”, “address.street” and “address.city”. They, per definition, have the same allowed operators. If we want to have an other set of operators for a specific key path, we need to define a separate row template for it.
  • Row template #2 is for date matches, i.e. our “birthdate” key path.
  • Row template #3 is for all integer matches, i.e. our “address.number” key path.

The result looks like this:

Row templates setup

Using bindings to connect the predicate to the UI

Next up, we simply connect both the text view from the main window and the predicate editor from the sheet window to the predicate key path using Cocoa bindings. In order to do so, select the NSPredicateEditor (first click the control to select the scroll view, then click again to select the inner NSPredicateEditor), hit Cmd+4. Then, unfold the “Value” binding and hook it up to the App Delegate’s “predicate” key path.

Setting up the Cocoa bindings

Do the same for the text view in the main window, but this time hook it up to the “predicate.description” key path (since only strings can be displayed in a text view). When you do this, make sure that the text view is read-only, since the description property of objects should never be set.

Writing the code to wrap it all up

Finally, we have only a bit of code to write in our AppDelegate implementation, so let’s go:

//
//  AppDelegate.m
//  PredicateEditorTest
//
//  Created by Vincent on 20-07-09.
//
 
#import "AppDelegate.h"
 
#define DEFAULT_PREDICATE @"(firstname = 'John' AND lastname = 'Doe') " \
                          @"OR birthdate > CAST('01/01/1985', 'NSDate') " \
                          @"OR address.city = 'Chicago' " \
                          @"AND address.street != 'Main Street' " \
                          @"OR address.number > 1000"
 
@implementation AppDelegate
 
- (id)init
{
	self = [super init];
	if (self != nil) {
		predicate = [[NSPredicate predicateWithFormat:DEFAULT_PREDICATE] retain];
	}
	return self;
}
 
- (void)dealloc
{
	[predicate release];
	[super dealloc];
}
 
- (IBAction)openEditor:(id)sender
{
	[NSApp beginSheet:sheet
	   modalForWindow:mainWindow
		modalDelegate:nil
	   didEndSelector:NULL
		  contextInfo:nil];
}
 
- (IBAction)closeEditor:(id)sender
{
	[NSApp endSheet:sheet];
	[sheet orderOut:sender];
}
 
@end

In the -init: method, we initialize the AppDelegate by setting and retaining a reference to a rather complex default predicate. When the XIB is loaded at run-time, the textbox shows exactly this predicate and it can be edited by invoking the edit sheet.

The actual implementation of the -openEditor: and -closeEditor: methods aren’t too exciting.

Downloading the source

You can download the source code for this tutorial as an Xcode project here.

PredicateEditorTest.zip

Have a blast!

  • http://www.geeks.ltd.uk/Services.html geeks

    It is tuely and amazing tool makes your code cleaner and makes programming more fun than befor.
    I’ve been using it for a while now and I like it and it workes like a charm

    thanks for introfucing such a powerful tool to us

  • http://www.geeks.ltd.uk/Services.html geeks

    It is tuely and amazing tool makes your code cleaner and makes programming more fun than befor.
    I’ve been using it for a while now and I like it and it workes like a charm

    thanks for introfucing such a powerful tool to us

  • Tony Brown

    I don’t know If I said it already but …Excellent site, keep up the good work. I read a lot of blogs on a daily basis and for the most part, people lack substance but, I just wanted to make a quick comment to say I’m glad I found your blog. Thanks, :)

    A definite great read..Tony Brown

  • Tony Brown

    I don’t know If I said it already but …Excellent site, keep up the good work. I read a lot of blogs on a daily basis and for the most part, people lack substance but, I just wanted to make a quick comment to say I’m glad I found your blog. Thanks, :)

    A definite great read..Tony Brown

  • Jacob

    Nice. I have one question, though. It seems impossible to create your default predicate using the interface. i.e. starting from scratch in the editor, since the compound template (Any, All) menu is unavailable. The plus sign does not admit adding Any or All, if I’m not mistaken.

    Is there a way around this? I often need nested compounded predicates, like the default you create in code.

  • Jacob

    Nice. I have one question, though. It seems impossible to create your default predicate using the interface. i.e. starting from scratch in the editor, since the compound template (Any, All) menu is unavailable. The plus sign does not admit adding Any or All, if I’m not mistaken.

    Is there a way around this? I often need nested compounded predicates, like the default you create in code.

  • http://nvie.com Vincent Driessen

    @Jacob: indeed, that one is pretty tricky. Try holding down the Alt key when you press the “+” button. It changes into a “…” button, which allows exactly that.

  • http://nvie.com Vincent Driessen

    @Jacob: indeed, that one is pretty tricky. Try holding down the Alt key when you press the “+” button. It changes into a “…” button, which allows exactly that.

  • http://www.hamsoftengineering.com Hank

    Thanks for this and good tip about using the key… I never knew that. Question: has anyone found a way to alter the width of the text fields in the predicate editor? They’re very narrow and I see no way to change it. They don’t resize as the window is resized even though the NSPredicateEditor is set to resize in IB.

  • http://www.hamsoftengineering.com Hank

    Thanks for this and good tip about using the key… I never knew that. Question: has anyone found a way to alter the width of the text fields in the predicate editor? They’re very narrow and I see no way to change it. They don’t resize as the window is resized even though the NSPredicateEditor is set to resize in IB.

  • http://www.hamsoftengineering.com Hank

    Update: I found a way to change the width of text fields. You get the row templates from the predicate editor. Then for each row template you can get the “templateViews”. Check the class of each templateView and if it is an NSTextField then you’ve got one. At that point you just modify the frame of the text field how you like and in my case I just modified the frame.size.width property.

    When I did this in the PredicateEditorTest project from this website I also had to adjust the project. It seemed that since the predicate was being set in the init method of AppDelegate that my changes to the text fields weren’t happening until after the predicate editor was already created. As such I have to move the creation of the initial predicate into applicationDidFinishLaunching of AppDelegate.

  • http://www.hamsoftengineering.com Hank

    Update: I found a way to change the width of text fields. You get the row templates from the predicate editor. Then for each row template you can get the “templateViews”. Check the class of each templateView and if it is an NSTextField then you’ve got one. At that point you just modify the frame of the text field how you like and in my case I just modified the frame.size.width property.

    When I did this in the PredicateEditorTest project from this website I also had to adjust the project. It seemed that since the predicate was being set in the init method of AppDelegate that my changes to the text fields weren’t happening until after the predicate editor was already created. As such I have to move the creation of the initial predicate into applicationDidFinishLaunching of AppDelegate.

  • A

    Good clear article. It was difficult to figure out this IB library given its extensive functionality. Thanks.
    How to hook it into the Core Data filter predicate?

  • A

    Good clear article. It was difficult to figure out this IB library given its extensive functionality. Thanks.
    How to hook it into the Core Data filter predicate?

blog comments powered by Disqus