SwiftUI Core Data:动态过滤 @FetchRequest 进阶

\color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

{\Large \mathbf{Core \ Data}}

Dynamically filtering @FetchRequest Further - 韦弦zhy

在完成了基本的动态过滤实现之后,我们是时候考虑一下他的灵活性了。

为了获得更大的灵活性,我们可以改进FilteredList视图,使其与任何种类的实体一起使用,并可以在任何字段上进行过滤。为使此工作正常进行,我们需要进行一些更改:

  1. 与其专门引用Singer类,不如使用泛型并带有一个约束,即所传入的内容必须是NSManagedObject
  2. 我们需要接受第二个参数来确定要过滤的键名,因为我们可能使用的是没有lastName属性的实体。
  3. 由于我们不知道每个实体将包含的内容,因此我们将让包含视图确定。因此,我们不只是使用歌手姓名的文本视图,而是要提供一个可以运行的闭包来配置所需的视图。

那里有两个复杂的部分。第一个是用于确定每个列表行内容的闭包,因为它需要使用两个重要的语法。我们在早期的关于视图和修饰符的技术项目即将结束时研究了这些内容,但是如果您错过了它们:

  • @ViewBuilder允许我们的包含视图(无论使用什么列表)在多个视图中发送,并且我们的列表将像常规List一样创建一个隐式HStack
  • @escaping表示闭包将被存储起来并在以后使用,这意味着Swift需要管理好它的内存。

第二个复杂的部分是我们如何让我们的容器视图自定义搜索键。以前我们是这样控制过滤器值的:

NSPredicate(format: "lastName BEGINSWITH %@", filter)

因此,您可能会进行有根据的猜测并编写如下代码:

NSPredicate(format: "%@ BEGINSWITH %@", keyName, filter)

但是,那是行不通的。您会看到,当我们编写%@ Core Data时,会自动为我们插入引号,以便谓词正确读取。这很有用,因为如果我们的字符串包含引号,它将自动确保它们与添加的引号不冲突。

这意味着当我们使用%@作为属性名称时,我们可能会以这样的谓词结尾:

NSPredicate(format: "'lastName' BEGINSWITH 'S'")

这是不正确的:属性名称不应使用引号引起来。

为了解决这个问题,NSPredicate有一个特殊的符号,可用于替换属性名称:%K(用于“键”)。这将插入我们提供的值,但不会在其周围添加引号。正确的谓词是:

NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue)

因此,将当前的FilteredList结构体替换为:

import SwiftUI
import CoreData

struct FilteredList<T: NSManagedObject, Content: View>: View {
    var fetchRequest: FetchRequest<T>
    var singers: FetchedResults<T> { fetchRequest.wrappedValue }

    // 这是我们的内容闭包;我们将为列表中的每个项目调用一次
    let content: (T) -> Content

    var body: some View {
        List(fetchRequest.wrappedValue, id: \.self) { singer in
            self.content(singer)
        }
    }

    init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
        fetchRequest = FetchRequest<T>(
            entity: T.entity(),
            sortDescriptors: [],
            predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue)
        )
        self.content = content
    }
}

现在,我们可以像这样通过升级ContentView来使用新的过滤列表:

FilteredList(
    filterKey: "lastName",
    filterValue: lastNameFilter ) { (singer: Singer) in
        Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
}

请注意我是如何特别使用(singer:Singer)作为闭包的参数的——这是必需的,以便Swift理解如何使用FilteredList。记住,我们说过它可以是任何类型的NSManagedObject,但是为了让Swift确切知道它是什么类型的托管对象,我们需要明确。

无论如何,有了这一更改,我们现在可以将列表与任何类型的过滤键和任何实体一起使用——它会更加有用!

更加动态的选择是:将NSPredicate完全由外部传入,即:

init(sort: [NSSortDescriptor] = [], predicate: NSPredicate, @ViewBuilder content: @escaping (T) -> Content) {
    fetchRequest = FetchRequest<T>(
        entity: T.entity(),
        sortDescriptors: sort,
        predicate: predicate
    )
    self.content = content
}

使用则变成了这样:

FilteredList(predicate: NSPredicate(format: "lastName BEGINSWITH %@", lastNameFilter)) {(singer: Singer) in
    Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
}

或者这样:

FilteredList(
    sort: [NSSortDescriptor(keyPath: \Singer.firstName, ascending: true)],
    predicate: NSPredicate(format: "lastName BEGINSWITH %@", lastNameFilter)) { (singer: Singer) in
        Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
}

这调用似乎变得更加复杂了,但是我们没说要删除第一个,我们完全可以同时保留两个初始化器!

译自 Dynamically filtering @FetchRequest with SwiftUI