[SwiftUI 100天] 在 SwiftUI 中动态过滤 @FetchRequest

1,392 阅读4分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

在 SwiftUI 中动态过滤 @FetchRequest

对于 SwiftUI ,我经常被问到的一个问题是:我要怎么样动态地改变一个 Core Data@FetchRequest,以便使用不同的谓词或者排序呢?大家之所以会提出这个问题是因为 fetch 请求是作为属性被创建的,因此如果你尝试让它们引用另外的属性,会被 Swift 拒绝。

这里有一个简单的解决方案,而且你回想的话会发现这其实是非常明显的方案:因为几乎所有其他东西也是这么运作的:我们应该把这个功能分割到一个独立的视图,然后把值注入进去。

我想用一些实际的代码来说明,所以我在下面放了一些最简单的例子:添加三个歌手到 Core Data,然后用两个按钮分别显示姓以 A 或者 S 结尾的歌手。

创建一个叫 Singer 的 Core Data 实体,给它两个字符串书写:“firstName” 和 “lastName”。用数据模型检视器把它的 Codegen 改成 Manual/None,然后进入 Editor 菜单,选择 Create NSManagedObject Subclass 以便我们得到一个可以自定义的Singer类。

等 Xcode 为我们生成好文件,打开 Singer+CoreDataProperties.swift,然后添加下面两个属性,让这个类能更好地配合 SwiftUI 使用:

var wrappedFirstName: String {
    firstName ?? "Unknown"
}

var wrappedLastName: String {
    lastName ?? "Unknown"
}

接下来是实际的工作。

接下来是设计一个托管我们的信息的视图。像我之前说过的,它会有两个按钮,改变视图信息的过滤器,还有一个用于插入测试数据的按钮。

首先,添加两个属性到 ContentView 结构体,以便我们有能够保持对象的托管对象上下文,以及一个作为过滤器使用的状态:

@Environment(\.managedObjectContext) var moc
@State private var lastNameFilter = "A"

对于视图的body,我们会用一个VStack包裹三个按钮,再加上一个放匹配的歌手列表的注释占位:

VStack {
    // list of matching singers

    Button("Add Examples") {
        let taylor = Singer(context: self.moc)
        taylor.firstName = "Taylor"
        taylor.lastName = "Swift"

        let ed = Singer(context: self.moc)
        ed.firstName = "Ed"
        ed.lastName = "Sheeran"

        let adele = Singer(context: self.moc)
        adele.firstName = "Adele"
        adele.lastName = "Adkins"

        try? self.moc.save()
    }

    Button("Show A") {
        self.lastNameFilter = "A"
    }

    Button("Show S") {
        self.lastNameFilter = "S"
    }
}

目前为止,一切都很简单,接下来是有趣的部分:我们需要替换掉// list of matching singers这个注释,用实际的实现代替。这里不会用到@FetchRequest注解,因为我们要在构造器里创建一个自定义 fetch 请求,但代码几乎是一样的。

创建一个叫 “FilteredList” 的 SwiftUI 视图,给它这个属性:

var fetchRequest: FetchRequest<Singer>

这个属性用来存储我们的 fetch 请求,以便我们可以在body里遍历它。不过,我们并不马上创建它,因为我们还不知道我们要找的是什么东西。相反,我们要创建一个自定义构造器,接收一个过滤字符串,然后用这个字符串来设置fetchRequest属性。

添加下面这个构造器:

init(filter: String) {
    fetchRequest = FetchRequest<Singer>(entity: Singer.entity(), sortDescriptors: [], predicate: NSPredicate(format: "lastName BEGINSWITH %@", filter))
}

这会得到一个用当前的托管对象上下文构建的 fetch 请求。因为这个视图会被用在ContentView内部,所以我们不必为其注入托管对象上下文到环境中 —— 它会继承来自ContentView的上下文。

剩下的事情就是完成视图的body,这里唯一有趣的事情是,没有了@FetchRequest,我们需要访问fetchRequestwrappedValue属性来拉出我们的数据。因此,视图的body实现如下:

var body: some View {
    List(fetchRequest.wrappedValue, id: \.self) { singer in
        Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
    }
}

如果你不喜欢使用fetchRequest.wrappedValue,可以创建一个简单的计算属性:

var singers: FetchedResults<Singer> { fetchRequest.wrappedValue }

至于FilteredList的预览,你可以直接移除。

这样一来视图就完成了,我们可以回到ContentView,把注释用实际的代码实现,如下:

FilteredList(filter: lastNameFilter)

运行应用尝试下:点击 Add Examples 按钮先创建三个歌手,然后点击 “Show A” 或者 “Show S” 来触发不同的姓氏过滤。你应该会看到列表随着你点击不同的按钮,动态更新不同的数据。

为了让这一切工作,用到一点新的知识,但算不上难 —— 只要你以 SwiftUI 的方式思考,答案会跃然纸上。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~