Вопрос

I am trying to generate an NSPredicateEditorRowTemplate that on the left side has a number of property names from an entity Foo, among which is the property bar. When the user selects 'bar', the right side should become popup which contains all values of 'bar'.

How can I best populate the right side popup? All unique values of bar are stored in an NSMutableArray, so perhaps I can use KVO to change the row template when the array changes.

Is there a way that I can use code to easily change the values in a right side popup in an NSPredicateEditor row? I can enter a few static values in IB, but that will not do in this situation.

EDIT

Having read a good deal of related Q&A's, including NSPredicateEditor in Xcode 4 and the excellent answer to it by @Dave DeLong, I think a good bit of the work can be done like this:

NSArray *leftexp = @[[NSExpression expressionForKeyPath:@"name"],[NSExpression expressionForKeyPath:@"married"]];
NSArray *rightexp = @[[NSExpression expressionWithFormat:@"One"],[NSExpression expressionWithFormat:@"Two"]];
NSPredicateEditorRowTemplate *template = [[NSPredicateEditorRowTemplate alloc] initWithLeftExpressions:leftexp rightExpressions:rightexp modifier:NSDirectPredicateModifier operators:@[@(NSEqualToPredicateOperatorType)] options:0];

NSPredicateEditorRowTemplate *compound = [[NSPredicateEditorRowTemplate alloc] initWithCompoundTypes:@[@(NSOrPredicateType)]];

[self.predicateEditor setRowTemplates:@[template,compound]];
NSEntityDescription *description = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
self.predicate = [NSPredicate predicateWithFormat:@"name == ''"];

I've seen a few ways to make the basic NSPredicateEditor appear (with at least the compound line), but it seems to me that there has to be an elegant way to do that, the way it was meant to be done. Can't find that though, can anyone help me along with this?

Это было полезно?

Решение

Some would probably argue that the way it is meant to be done is in Interface Builder. That would probably be fine if you're familiar with how row templates work, how they get combined, etc. Personally, I prefer to create the programmatically because I can be explicit about how the row templates function.

Let's go through an example of how this all works together, and then you can decide which approach works best for you:

  • You have an object with two NSString properties: bar and baz
  • bar can only have a certain number of possible values, which we'll call @"Bar1", @"Bar2", and @"Bar3".
  • baz can be any NSString value.

With this in mind, here's how you'd create that programmatically:

// these defines are for brevity on the web page; please don't actually do this
#define NSPERT NSPredicateEditorRowTemplate
#define KP(_kp) [NSExpression expressionForKeyPath:(_kp)]
#define CV(_cv) [NSExpression expressionForConstantValue:(_cv)]

NSPERT *compound = [[NSPERT alloc] initWithCompoundTypes:@[@(NSAndPredicateType), 
                                                           @(NSOrPredicateType), 
                                                           @(NSNotPredicateType)]];

NSPERT *bar = [[NSPERT alloc] initWithLeftExpressions:@[KP(@"bar")] 
                                     rightExpressions:@[CV(@"Bar1"), CV(@"Bar2"), CV(@"Bar3")] 
                                             modifier:NSDirectPredicateModifier 
                                            operators:@[@(NSEqualToPredicateOperatorType), 
                                                        @(NSNotEqualToPredicateOperatorType)] 
                                              options:0];

NSPERT *baz = [[NSPERT alloc] initWithLeftExpressions:@[KP(@"baz")] 
                         rightExpressionAttributeType:NSStringAttributeType 
                                             modifier:NSDirectPredicateModifier 
                                            operators:@[@(NSEqualToPredicateOperatorType),
                                                        @(NSNotEqualToPredicateOperatorType)]
                                              options:0];

NSArray *templates = @[compound, bar, baz];
[self.myPredicateEditor setRowTemplates:templates];

Granted, this is a lot of typing just to create row templates. I understand that. I just think that it's also very explicit and easy to debug. That being said, you can also configure this exact same thing in Interface Builder. To do so, put an NSPredicateEditor in your xib, and give it 3 row templates. The Compound row template you'd leave alone, but the other two you'd change to be configured like this:

For the "bar" template:

"Bar" row template configuration

For the "baz" template:

"Baz" row template configuration

If you compare these screenshots to the code above, it should (hopefully) be easy to see how one maps to the other. With the row templates configured like this, IB also shows the predicate editor in your window:

Window with predicate editor

If you want to do more advanced things, such as creating custom row templates, or populating the list of allowed constant values with something from your data model, then you'll have to drop to doing the configuration in code.


Edit to answer comment question

The modifier bit of these selectors corresponds to the NSComparisonPredicateModifer for the created NSComparisonPredicate. There are three modifiers: Direct, Any, and All.

@"bar = 'Bar1'";
  ^ No modifier means this is NSDirectPredicateModifier

@"ANY user.userName = 'markjs'";
  ^~~ use of "ANY" keyword means this is NSAnyPredicateModifier

@"ALL user.age < 42";
  ^~~ use of "ALL" keyword means this is NSAllPredicateModifier

In these three examples, they're all comparison predicates (because they have a left side, an operator, and a right side), but the use of "ANY" or "ALL" affects how the left side is interpreted. If either is present, then the predicate is expecting the left-hand side to evaluate to a collection of possibilities.

Using the "direct" option (or 0) basically means you're going to be doing a one-to-one comparison.

Другие советы

The key to filling an NSPredicateEditor with a compound row (NOT, AND, OR), as well as one row with a simple predicate, is that the NSPredicateEditor should receive a -setRowTemplates: call with an NSArray that contains both the compound, and the regular predicate. Then, its predicate should be set to a compound predicate with a subpredicate, using -andPredicateWithSubpredicates:.

This did the trick for me:

NSArray *leftexp = @[[NSExpression expressionForKeyPath:@"name"]];
NSArray *rightexp = @[[NSExpression expressionForConstantValue:@"A"],[NSExpression expressionForConstantValue:@"B"],[NSExpression expressionForConstantValue:@"C"]];
NSPredicateEditorRowTemplate *template = [[NSPredicateEditorRowTemplate alloc] \
          initWithLeftExpressions:leftexp
          rightExpressions:rightexp
          modifier:NSDirectPredicateModifier
          operators:@[@(NSEqualToPredicateOperatorType)]
          options:0];
NSPredicateEditorRowTemplate *compound = [[NSPredicateEditorRowTemplate alloc] initWithCompoundTypes:@[@(NSAndPredicateType),@(NSOrPredicateType),@(NSNotPredicateType)]];
NSMutableArray *templates = [NSMutableArray arrayWithObject:compound];
[templates addObject:template];
[self.predicateEditor setRowTemplates:templates];

NSPredicate *p = [NSPredicate predicateWithFormat:@"name == 'A'"];
self.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObject:p]];

Bassed on markjs answer, I translated it to swift 4 and placed it in a extension of NSPredicateEditorRowTemplate.

extension NSPredicateEditorRowTemplate {

    convenience init( compoundTypes: [NSCompoundPredicate.LogicalType] ) {

        var compoundTypesNSNumber: [NSNumber] = []
        for c in compoundTypes { compoundTypesNSNumber.append( NSNumber(value: c.rawValue) ) }
        self.init( compoundTypes: compoundTypesNSNumber )
    }

    convenience init( forKeyPath keyPath: String, withValues values: [Any] ) {

        let keyPaths: [NSExpression] = [NSExpression(forKeyPath: keyPath)]

        var constantValues: [NSExpression] = []
        for v in values {

            constantValues.append( NSExpression(forConstantValue: v) )
        }

        let operators: [NSComparisonPredicate.Operator] = [.equalTo, .notEqualTo]
        var operatorsNSNumber: [NSNumber] = []
        for o in operators { operatorsNSNumber.append( NSNumber(value: o.rawValue) ) }

        self.init( leftExpressions: keyPaths,
                   rightExpressions: constantValues,
                   modifier: .direct,
                   operators: operatorsNSNumber,
                   options: (Int(NSComparisonPredicate.Options.caseInsensitive.rawValue | NSComparisonPredicate.Options.diacriticInsensitive.rawValue)) )
    }

    convenience init( stringCompareForKeyPaths keyPaths: [String] ) {

        var leftExpressions: [NSExpression] = []
        for keyPath in keyPaths {

            leftExpressions.append( NSExpression(forKeyPath: keyPath) )
        }

        let operators: [NSComparisonPredicate.Operator] = [.contains, .equalTo, .notEqualTo, .beginsWith, .endsWith]
        var operatorsNSNumber: [NSNumber] = []
        for o in operators { operatorsNSNumber.append( NSNumber(value: o.rawValue) ) }

        self.init( leftExpressions: leftExpressions,
                   rightExpressionAttributeType: .stringAttributeType,
                   modifier: .direct,
                   operators: operatorsNSNumber,
                   options: (Int(NSComparisonPredicate.Options.caseInsensitive.rawValue | NSComparisonPredicate.Options.diacriticInsensitive.rawValue)) )
    }

    convenience init( IntCompareForKeyPaths keyPaths: [String] ) {

        var leftExpressions: [NSExpression] = []
        for keyPath in keyPaths {

            leftExpressions.append( NSExpression(forKeyPath: keyPath) )
        }

        let operators: [NSComparisonPredicate.Operator] = [.equalTo, .notEqualTo, .greaterThan, .greaterThanOrEqualTo, .lessThan, .lessThanOrEqualTo]
        var operatorsNSNumber: [NSNumber] = []
        for o in operators { operatorsNSNumber.append( NSNumber(value: o.rawValue) ) }

        self.init( leftExpressions: leftExpressions,
                   rightExpressionAttributeType: .integer16AttributeType,
                   modifier: .direct,
                   operators: operatorsNSNumber,
                   options: 0 )
    }
}

Uses it follows:

   func setMyPetFilter() {

        // We assume that myPredicateEditor is create/set in IB
        // Remove whatever was configured in IB
        myPredicateEditor.rowTemplates.removeAll()

        let templateCompoundTypes = NSPredicateEditorRowTemplate( compoundTypes: [.and, .or, .not] )
        let template1 = NSPredicateEditorRowTemplate( forKeyPath: "Animals", withValues: ["Dog", "Cat", "Hamster", "Canary"] )
        let template2 = NSPredicateEditorRowTemplate( stringCompareForKeyPaths: ["PetName", "OwnerName"] )
        let template3 = NSPredicateEditorRowTemplate( forKeyPath: "Legs", withValues: [2,4] )
        let template4 = NSPredicateEditorRowTemplate( IntCompareForKeyPaths: ["Age", "AgeOwner"] )
        myPredicateEditor.rowTemplates = [templateCompoundTypes, template1, template2, template3, template4]
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top