Howdy! This question is pretty long so be sure to have a seat first 🙂
At one point in my code I get a string from the user, and I analyze that string. By analyzing I mean going through every character and mathcing it against a predicate the user has set in an NSPredicateEditor. The predicate is setup progamatically this way:
NSArray* keyPaths = [NSArray arrayWithObjects:
[NSExpression expressionForKeyPath: @"Character"],
[NSExpression expressionForKeyPath: @"Character Before"],
[NSExpression expressionForKeyPath: @"Character After"],
nil];
// -----------------------------------------
NSArray* constants = [NSArray arrayWithObjects:
[NSExpression expressionForConstantValue: @"Any letter"],
[NSExpression expressionForConstantValue: @"Letter (uppercase)"],
[NSExpression expressionForConstantValue: @"Letter (lowercase)"],
[NSExpression expressionForConstantValue: @"Number"],
[NSExpression expressionForConstantValue: @"Alphanumerical"],
[NSExpression expressionForConstantValue: @"Ponctuation Mark"],
nil];
// -----------------------------------------
NSArray* compoundTypes = [NSArray arrayWithObjects:
[NSNumber numberWithInteger: NSNotPredicateType],
[NSNumber numberWithInteger: NSAndPredicateType],
[NSNumber numberWithInteger: NSOrPredicateType],
nil];
// -----------------------------------------
NSArray* operatorsA = [NSArray arrayWithObjects:
[NSNumber numberWithInteger: NSEqualToPredicateOperatorType],
[NSNumber numberWithInteger: NSNotEqualToPredicateOperatorType],
nil];
NSArray* operatorsB = [NSArray arrayWithObjects:
[NSNumber numberWithInteger: NSInPredicateOperatorType],
nil];
// -----------------------------------------
NSPredicateEditorRowTemplate* template1 = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions: keyPaths
rightExpressions: constants
modifier: NSDirectPredicateModifier
operators: operatorsA
options: 0];
NSPredicateEditorRowTemplate* template2 = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions: keyPaths
rightExpressionAttributeType: NSStringAttributeType
modifier: NSDirectPredicateModifier
operators: operatorsB
options: 0];
NSPredicateEditorRowTemplate* compound = [[NSPredicateEditorRowTemplate alloc] initWithCompoundTypes: compoundTypes];
// -----------------------------------------
NSArray* rowTemplates = [NSArray arrayWithObjects: template1, template2, compound, nil];
[myPredicateEditor setRowTemplates: rowTemplates];
So you can see I have three keypaths and some constants they can be compared with.
When analyzing the string I basically want to do this, in pseudocode:
originalString = [string from NSTextView]
for (char in originalString)
bChar = [character before char]
aChar = [character after char]
predicate = [predicate from myPredicateEditor]
// using predicate - problem!
result = [evaluate predicate with:
bChar somehow 'linked' to keypath 'Character Before'
char 'linked' to 'Character'
aChar 'linked' to 'Character After' // These values change, of course
and also:
constant "All letters" as "abc...WXYZ"
constant "Numbers" as "0123456789"
etc for all other constants set up // These don't
]
if (result) then do something with char, bChar and aChar
You can see where my problem basically lies:
-
‘Character Before/After’ cannot be keypaths because of the space, but I want to keep it that way as it is more beautiful for the user (imagine having something as ‘characterBefore’ instead…)
-
Constants such as ‘Numbers’ actually represent strings like ‘0123456789’, witch I can’t display to the user as well
I was able to find a workaround to this problem, but I now it doesn’t work with every character and it is also very unefficient (in my opinion, at least). What I do is get the predicate format from the predicate, replace what I have to replace, and evaluate that new format instead. Now for some real code that explains this:
#define kPredicateSubstitutionsDict [NSDictionary dictionaryWithObjectsAndKeys: \
@"IN 'abcdefghijklmnopqrstuvwxyz'", @"== \"Letter (lowercase)\"", \
@"IN 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'", @"== \"Letter (uppercase)\"", \
@"IN 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'", @"== \"Any letter\"", \
@"IN '1234567890'", @"== \"Number\"", \
@"IN 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'", @"== \"Alphanumerical\"", \
@"IN ',.;:!?'", @"== \"Ponctuation Mark\"", \
\
@"MATCHES '[^a-z]'" , @"!= \"Letter (lowercase)\"", \
@"MATCHES '[^A-Z]'" , @"!= \"Letter (uppercase)\"", \
@"MATCHES '[^a-zA-Z]'" , @"!= \"Any letter\"", \
@"MATCHES '[^0-9]'" , @"!= \"Number\"", \
@"MATCHES '[^a-zA-Z0-9]'" , @"!= \"Alphanumerical\"", \
@"MATCHES '[^,\.;:!\?]'" , @"!= \"Ponctuation Mark\"", \
\
nil]
// NSPredicate* predicate is setup in another place
// NSString* originalString is also setup in another place
NSString* predFormat = [predicate predicateFormat];
for (NSString* key in [kPredicateSubstitutionsDict allKeys]) {
prefFormat = [predFormat stringByReplacingOccurrencesOfString: key withString: [kPredicateSubstitutionsDict objectForKey: key]];
}
for (NSInteger i = 0; i < [originalString length]; i++) {
NSString* charString = [originalString substringWithRange: NSMakeRange(i, 1)];
NSString* bfString;
NSString* afString;
if (i == 0) {
bfString = @"";
}
else {
bfString = [originalString substringWithRange: NSMakeRange(i - 1, 1)];
}
if (i == [originalString length] - 1) {
afString = @"";
}
else {
afString = [originalString substringWithRange: NSMakeRange(i + 1, 1)];
}
predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character Before" withString: [NSString stringWithFormat: @"\"%@\"", bfString]];
predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character After" withString: [NSString stringWithFormat: @"\"%@\"", afString]];
predFormat = [predFormat stringByReplacingOccurrencesOfString: @"Character" withString: [NSString stringWithFormat: @"\"%@\"", charString]];
NSPredicate* newPred = [NSPredicate predicateWithFormat: predFormat];
if ([newPred evaluateWithObject: self]) { // self just so I give it something (nothing is actually gotten from self)
// if predicate evaluates to true, then do something exciting!
}
}
So, here you go, this is a simplified version of what I am doing. If you see any typos, most probably they’re not in my code, because I’ve edited this quite a bit so it would be simpler.
To summarize:
- I need to evaluate the predicate the user makes against many characters, modifying it quite a bit, while trying to be as efficient as possible
-
The problems I find with my approach are:
- I don’t think it’s clean at all
- I have no guarantee that it will work on every case (when one of the character is a newline/enter character, the predicate raises an error saying it can’t understand the format)
That’s all folks! Thanks for reading thus far. May your god be with you when solving this mess!
EDIT: Just to clarify things abut, I would add that what seems the trigger to this problem is the fact that I cannot, right at the start when I setup the predicate editor, define one constant with a name (that gets displayed to the user) and a value that represents that constant and gets inserted in the predicate format. The same thing for keypaths: if I could have one display name, and then one value that would be those var strings for predicate ($VAR or whatever it is) all the problems would be solved. If this is possible, please tell me how. If it is impossible, then please focus on the other problems I describe.
So the main question:
Answer: yes. You used to be able to set this up in Interface Builder, but my attempts to do so in Xcode 4 have been unsuccessful. It’s possible that the capability was removed in the IB => Xcode 4 merge.
Fortunately, you can do it in code. Unfortunately, it requires an
NSPredicateEditorRowTemplatesubclass. I haven’t found a way to do it without such a subclass.The gist is to override
-templateViewsin your row template, and do something like this:And voilá! A popup in your predicate editor where the title does not match the keypath.
edit
The other way to solve this (without subclassing!) would be to put your custom text in a .strings file and “localize” your editor to English (or whatever language you want).
NSPredicateEditorNSPredicateEditorIf you were to express these comparisons in a predicate, you probably wouldn’t use
!=and==. Likely, you’d want to do something like:And one of:
For this, the way that I can think of to do this would be to have two separate row templates. One would recognize and display predicates in the first format (
MATCHES '[0-9]'), and the second would recognize and display the negation. You’re starting to get into the weird realm ofNSPredicateEditorRowTemplates, I think. I’m still not really sure what you’re trying to accomplish and why you want to be matching every character of a string against a predicate, but whatever.For more information on how to create custom row templates, check out these two blog posts:
NSPredicateEditorRowTemplateNSPredicateEditorRowTemplate