如何制作NSPredicateEditorRowTemplate以支持NULL值搜索选项?
嘿,这个需求我刚好折腾过,其实不用完全从零写自定义RowTemplate,不过确实需要对默认的模板做一些扩展调整,下面给你一步步说清楚:
核心思路
默认的NSComparisonPredicate操作符集合里没有直接的「检查NULL」选项(比如IS NULL/IS NOT NULL),所以我们需要把这两个操作符加到现有的下拉菜单里,同时处理对应的谓词生成逻辑——而这就需要借助自定义NSPredicateEditorRowTemplate子类来实现,不过工作量很小,不需要完全重写模板。
具体实现步骤
1. 定义自定义NULL操作符
首先我们需要为NULL检查定义专属的操作符常量,注意要避开系统已有的NSComparisonPredicateOperatorType数值,避免冲突:
// Objective-C typedef NS_ENUM(NSUInteger, CustomPredicateOperator) { CustomPredicateOperatorIsNULL = 100, // 选系统未使用的数值 CustomPredicateOperatorIsNOTNULL = 101 };
// Swift enum CustomPredicateOperator: UInt { case isNULL = 100 case isNOTNULL = 101 }
2. 创建自定义RowTemplate子类
我们继承默认的NSPredicateEditorRowTemplate,重写几个关键方法来扩展功能:
Objective-C版本
@interface NullablePredicateRowTemplate : NSPredicateEditorRowTemplate @end @implementation NullablePredicateRowTemplate // 扩展操作符下拉菜单,添加NULL选项 - (NSArray *)templateViews { NSArray *defaultViews = [super templateViews]; NSPopUpButton *operatorPopup = defaultViews[1]; // 默认模板的第二个视图是操作符下拉框 // 把NULL检查选项加到菜单末尾 [operatorPopup addItemWithTitle:@"is NULL"]; [operatorPopup addItemWithTitle:@"is NOT NULL"]; return defaultViews; } // 根据选中的操作符生成对应的谓词 - (NSPredicate *)predicateWithSubpredicates:(NSArray<NSPredicate *> *)subpredicates { // 获取当前选中的键路径 NSString *keyPath = [(NSComparisonPredicate *)subpredicates.firstObject leftExpression].keyPath; NSPopUpButton *operatorPopup = self.templateViews[1]; NSInteger selectedIndex = operatorPopup.selectedIndex; // 判断是否选中NULL操作符 NSInteger nullIndex = operatorPopup.numberOfItems - 2; NSInteger notNullIndex = operatorPopup.numberOfItems - 1; if (selectedIndex == nullIndex) { return [NSPredicate predicateWithFormat:@"%K IS NULL", keyPath]; } else if (selectedIndex == notNullIndex) { return [NSPredicate predicateWithFormat:@"%K IS NOT NULL", keyPath]; } else { // 其他操作符沿用默认逻辑 return [super predicateWithSubpredicates:subpredicates]; } } // 控制界面显示:选中NULL操作符时隐藏右侧输入框 - (NSInteger)displayableSubpredicateCount { NSPopUpButton *operatorPopup = self.templateViews[1]; NSInteger selectedIndex = operatorPopup.selectedIndex; NSInteger nullIndex = operatorPopup.numberOfItems - 2; NSInteger notNullIndex = operatorPopup.numberOfItems - 1; if (selectedIndex == nullIndex || selectedIndex == notNullIndex) { return 1; // 只需要显示键路径选择,不需要输入值 } return [super displayableSubpredicateCount]; } @end
Swift版本
class NullablePredicateRowTemplate: NSPredicateEditorRowTemplate { override var templateViews: [NSView] { let defaultViews = super.templateViews guard let operatorPopup = defaultViews[1] as? NSPopUpButton else { return defaultViews } operatorPopup.addItem(withTitle: "is NULL") operatorPopup.addItem(withTitle: "is NOT NULL") return defaultViews } override func predicate(withSubpredicates subpredicates: [NSPredicate]) -> NSPredicate { guard let compPredicate = subpredicates.first as? NSComparisonPredicate, let keyPath = compPredicate.leftExpression.keyPath, let operatorPopup = templateViews[1] as? NSPopUpButton else { return super.predicate(withSubpredicates: subpredicates) } let selectedIndex = operatorPopup.selectedIndex let nullIndex = operatorPopup.numberOfItems - 2 let notNullIndex = operatorPopup.numberOfItems - 1 if selectedIndex == nullIndex { return NSPredicate(format: "%K IS NULL", keyPath) } else if selectedIndex == notNullIndex { return NSPredicate(format: "%K IS NOT NULL", keyPath) } else { return super.predicate(withSubpredicates: subpredicates) } } override var displayableSubpredicateCount: Int { guard let operatorPopup = templateViews[1] as? NSPopUpButton else { return super.displayableSubpredicateCount } let selectedIndex = operatorPopup.selectedIndex let nullIndex = operatorPopup.numberOfItems - 2 let notNullIndex = operatorPopup.numberOfItems - 1 if selectedIndex == nullIndex || selectedIndex == notNullIndex { return 1 } return super.displayableSubpredicateCount } }
3. 配置NSPredicateEditor使用自定义模板
最后把这个自定义模板设置给你的NSPredicateEditor,替换原来的默认比较模板:
// Objective-C NSArray *keyPaths = @[@"name", @"email", @"address"]; // 你的字段列表 NullablePredicateRowTemplate *nullableTemplate = [[NullablePredicateRowTemplate alloc] initWithLeftExpressions:[keyPaths valueForKeyPath:@"SELF"] rightExpressionAttributeType:NSStringAttributeType modifier:NSDirectPredicateModifier operators:@[@(NSLessThanPredicateOperatorType), @(NSGreaterThanPredicateOperatorType), @(NSEqualToPredicateOperatorType)] options:0]; self.predicateEditor.rowTemplates = @[nullableTemplate];
// Swift let keyPaths = ["name", "email", "address"].map { NSExpression(forKeyPath: $0) } let nullableTemplate = NullablePredicateRowTemplate(leftExpressions: keyPaths, rightExpressionAttributeType: .stringAttributeType, modifier: .direct, operators: [NSNumber(value: NSComparisonPredicate.Operator.lessThan.rawValue), NSNumber(value: NSComparisonPredicate.Operator.greaterThan.rawValue), NSNumber(value: NSComparisonPredicate.Operator.equalTo.rawValue)], options: 0) predicateEditor.rowTemplates = [nullableTemplate]
关键注意点
- 自定义操作符的数值一定要避开系统已有的
NSComparisonPredicateOperatorType值,防止逻辑冲突; displayableSubpredicateCount方法是控制输入框显示的关键,返回1时系统会自动隐藏右侧的输入视图;- 如果你的字段有不同类型(比如数字、日期),可以创建多个对应类型的自定义模板,或者在模板里做类型适配。
简单来说,确实需要自定义NSPredicateEditorRowTemplate类,但不需要完全从零构建,只需要在默认模板基础上扩展操作符列表,并重写几个核心方法就能实现你要的效果——把NULL检查选项和其他比较操作符放在同一个下拉菜单里。
内容的提问来源于stack exchange,提问作者Ely
相关产品推荐
相关产品推荐

