在 SwiftData 中组合谓词

nOk*_*nOk 7 predicate nspredicate swift swift-data

我正在尝试使用 and/or 组合该类型的多个谓词。以前使用 CoreData 和 NSPredicate 我只会这样做:

let predicate = NSPredicate(value: true)
let predicate2 = NSPredicate(value: false)
let combinedPred = NSCompoundPredicate(type: .or, subpredicates: [predicate, predicate2])
Run Code Online (Sandbox Code Playgroud)

是否有类似的方法可以使用 SwiftData 和 #Predicate 来做到这一点?如果没有,我如何实现一种预先创建部分条件并稍后将它们组合到谓词中的方法?

我发现将其作为表达式执行此操作的唯一方法是这样的,但这将使我的谓词长数百行

let includeOnlyFavorites = true
#Predicate { includeOnlyFavorites ? $0.isFavorite : true }
Run Code Online (Sandbox Code Playgroud)

语境:

我正在开发一个应用程序,允许用户使用快捷操作保存和查询项目。这些项目使用 SwiftData 存储并使用 EntityPropertyQuery 查询

Apple 是这样实现 Query 属性的:

static var properties = QueryProperties {
    Property(\BookEntity.$title) {
        EqualToComparator { NSPredicate(format: "title = %@", $0) }
        ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将谓词与 组合起来NSCompoundPredicate


尝试过但失败了:

返回 Bool 的闭包:

let isFavorite = { (item: Item) in item.isFavorite }
let predicate = #Predicate<Item> { isFavorite($0) }
Run Code Online (Sandbox Code Playgroud)
  • 不起作用,因为谓词不允许全局函数
  • 我还尝试使用evaluate(Item) -> Bool方法创建一个对象 IsFavouriteFilter 但我也无法使用它

我还认为我也许可以在另一个谓词中使用 StandardPredicateExpression,因为在文档中它写道:

“组成谓词一部分的组件表达式,并且受标准谓词类型支持。 ”但没有对此类型的进一步解释

nOk*_*nOk 13

哇。这很难,但我找到了解决问题的方法:

如果我们使用PredicateExpression而不是谓词,我们可以稍后构建一个像这样的谓词:

let expression = PredicateExpressions.Value(true)

let predicate = Predicate<String>({ input in
    expression
})

Run Code Online (Sandbox Code Playgroud)

下一步是注入输入。我选择只创建一个接受变量并返回表达式的闭包(因为接受表达式的初始化程序不提供值,而是提供变量)

let variableExp = { (variable: PredicateExpressions.Variable<String>) in
    let value = PredicateExpressions.Value("Hello There")
    
    return PredicateExpressions.Equal(
        lhs: variable,
        rhs: value
    )
}

let variablePredicate = Predicate<String>({ input in
    variableExp(input)
})
Run Code Online (Sandbox Code Playgroud)

模型示例

斯威夫特数据模型:

@Model
class Book {
    @Attribute
    var title: String
    
    @Attribute
    var lastReadDate: Date
    
    init(title: String, lastReadDate: Date) {
        self.title = title
        self.lastReadDate = lastReadDate
    }
}
Run Code Online (Sandbox Code Playgroud)

为我们要使用的表达式创建闭包。

typealias BookVariable = PredicateExpressions.Variable<Book>
typealias BookKeyPath<T> = PredicateExpressions.KeyPath<BookVariable,T>

let dateEquals = { (input: BookKeyPath<Date>, _ value: Date) in
    return PredicateExpressions.Equal(
        lhs: input,
        rhs: PredicateExpressions.Value(value)
    )
}
Run Code Online (Sandbox Code Playgroud)

实际过滤:

// Do we want to filter by date at all?
let filterByDate = true

// The date we want to test against
let testDate = Date.now

let variablePredicate = Predicate<Book>({ input in
    // Wrap values
    let shouldFilterByDate = PredicateExpressions.Value(filterByDate)
    let alwaysTrue = PredicateExpressions.Value(true)
    
    // Create date Expression with testDate
    let dateExp = dateEquals(BookKeyPath(root: input, keyPath: \Book.lastReadDate), testDate)

    // Predicate that ,
    // if shouldFilterByDate evaluates to true returns dateExp result
    // otherwise returns expression that evaluates to true
    return PredicateExpressions.build_Conditional(
        shouldFilterByDate,
        dateExp,
        alwaysTrue
    )
})

let descriptor = FetchDescriptor(predicate: variablePredicate)
modelContext.fetch(descriptor)
Run Code Online (Sandbox Code Playgroud)


nOk*_*nOk 1

以@orgtre 的答案为基础

TLDR:这个要点conjunction()实现disjunction()了两种方法Array<Predicate<T>>

错误和随后崩溃的原因是PredicateExpressions.Variable用于解析谓词输入。

这就是谓词变量解析的内部工作原理:

您创建的谓词看起来像这样(展开时):

let predicate = Foundation.Predicate<Person>({
    PredicateExpressions.build_contains(
        PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_Arg($0),
            keyPath: \.name
        ),
        PredicateExpressions.build_Arg("Luke")
    )
})
Run Code Online (Sandbox Code Playgroud)

闭包采用 PredicateExpressions.Variable<Input>您需要将其作为参数传递给表达式的参数$0

该变量对于您创建的每个谓词都是唯一的,这意味着当您仅使用属性组合它们时predicate.expression,每个表达式都有一个不同的变量,从而导致错误unresolved Variable

我创建了一个StandardPredicateExpression带有谓词和变量的自定义,并将在其评估方法中执行以下操作:

struct VariableWrappingExpression<T>: StandardPredicateExpression {
    let predicate: Predicate<T>
    let variable: PredicateExpressions.Variable<T>
    
    func evaluate(_ bindings: PredicateBindings) throws -> Bool {
        // resolve the variable
        let value: T = try variable.evaluate(bindings)
        
        // bind the variable of the predicate to this value
        let innerBindings = bindings.binding(predicate.variable, to: value)

        // evaluate the expression with those bindings
        return try predicate.expression.evaluate(innerBindings)
    }
}
Run Code Online (Sandbox Code Playgroud)

扩展@orgtre的出色工作,创建一个解决方案,该解决方案采用一系列谓词和用于组合它们的闭包


extension Predicate {    
    typealias Expression = any StandardPredicateExpression<Bool>
    
    static func combining<T>(
        _ predicates: [Predicate<T>],
        nextPartialResult: (Expression, Expression) -> Expression
    ) -> Predicate<T> {
        return Predicate<T>({ variable in
            let expressions = predicates.map({
                VariableWrappingExpression<T>(predicate: $0, variable: variable)
            })
            guard let first = expressions.first else {
                return PredicateExpressions.Value(true)
            }
            
            let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
                nextPartialResult($0,$1)
            }
            
            return expressions.dropFirst().reduce(first, closure)
        })
    }
}

let compound = Predicate<Person>.combine([predicateA, predicateB]) {
    func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
        PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
    }
    
    return Predicate<T>.combining(self, nextPartialResult: {
        buildConjunction(lhs: $0, rhs: $1)
    })
}
Run Code Online (Sandbox Code Playgroud)

检查此要点以了解实施情况