老房东
老房东的纸条箱

老房东的纸条箱

SwiftUI应用CoreData小纸条(三)

SwiftUI应用CoreData小纸条(三)

数据过滤、为Entity建立便于使用的调用、定制万能ListView

老房东's photo
老房东
·Mar 4, 2022·

3 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

SwiftUI应用CoreData小纸条(二)中我们自定义了一个Entity并利用CoreData的唯一属性优化addItem的操作。接下来我准备对显示的数据进行过滤。这里主要会使用到NSPredicate来设置(官方的文档真的是不堪入目万年没有更新过了),它的作用其实就是将我们的条件最终变为SQL的Where查询子句,使用它来做数据过滤远比在取出来以后在内存里去过滤它要快很多,也更节约内存。

数据过滤

过滤出指定数据

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
    predicate: NSPredicate(format: "name == 'Chapter 5'"),
    animation: .default)

这就会只显示出一条名为Chapter 5的数据。

过滤条件参数化

不喜欢在双引号中打单引号,可以使用NSPredicate的参数能力:

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
    predicate: NSPredicate(format: "name == %@ or name == %@", "Chapter 6", "Chapter 7"),
    animation: .default)

这样就会在列表中显示出两个条目:Chapter 6和Chapter 7了。

同样,我们除了==外还可以使用>和<符号进行内容过滤:

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
    predicate: NSPredicate(format: "name > %@", "Chapter 3"),
    animation: .default)

这样就会显示出Chapter4 到 Chapter9的内容了。

使用IN

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
    predicate: NSPredicate(format: "name IN %@", ["Chapter 2","Chapter 4","Chapter 6"]),
    animation: .default)

IN条件可以使用一个Array来进行过滤,上面这个将会在List只显示出三个条目。

其它支持的条件

其它支持的条件可以在官方支持的查询条件列表中去了解并尝试。

简化CoreData的读取

还记得之前在ContentView.swift中我们为了显示name,写了这样的代码

NavigationLink {
    Text("Item at \(item.name ?? "UNKNOW ERROR")")
} label: {
    Text(item.name ?? "UNKNOW ERROR")
}

对于这样在View中大量??代码的行为实属不能忍受。我们来着手做点小处理。这里有两个方案,一个方案是自己做一个warpped属性,另外就是为每个Entity建立一个自己的ViewModel。看你自己的喜好了。

为CoreData属性设置wrapped属性

大多数人的"先进"经验就是将CoreData的Entity生成一个NSManagedObject的Subclass,如下图这个菜单

image.png

但是我仔细思考后,使用了一个更为轻便的方法避免在模型中进行了修改后不断重复生成再合并代码的操作。就是直接自己建立一个文件Chapter.swift文件,只extension出需要的功能即可:

import Foundation

extension Chapter{
    var wrappedName : String{
        name ?? "Unknow Chapter"
    }
}

这样之后,ContentView.swift中相应使用item.name变为item.wrappedName即可

NavigationLink {
    Text("Item at \(item.wrappedName)")
} label: {
    Text(item.wrappedName)
}

建立Entity的ViewModel

上面wrappedName的方式让你在调用时将原本的name改一个不同的名字,你可能会觉得不够爽。哪么使用ViewModel方式可能会让你感觉更为丝滑一些。Chapter.swift中的内容变为这样:

import Foundation

struct ChapterViewModel{
    let name:String
}

extension Chapter{
    var viewModel : ChapterViewModel{
        return ChapterViewModel(
            name: self.name ?? "Unknow Chapter"
        )
    }
}

这样我们在View中的代码就可以变为:

let item=item.viewModel
NavigationLink {
    Text("Item at \(item.name)")
} label: {
    Text(item.name)
}

我个人比较喜欢这个ViewModel方式,后续的代码都会使用这个方式。

定制万能查询ListView

在一个应用中可能会对多个Entity进行List的操作。这里记录一个小纸条,定制一个ListView用来灵活的显示List,除了灵活显示之外,也可以利用这个View来灵活更新View中的查询条件从而达到实时改变数据显示集合的效果。

定义FilteredList

我新建了一个FilteredList.swift用于定义FilteredList View:

import CoreData
import SwiftUI

struct FilteredList<T: NSManagedObject,Content: View>: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest var fetchRequest: FetchedResults<T>
    let content: (T) -> Content

    var body: some View {
        List {
            ForEach(fetchRequest, id:\.self) { item in
                self.content(item)
            }
            .onDelete(perform: deleteItems)
        }
    }

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

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { fetchRequest[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

struct FilteredList_Previews: PreviewProvider {
    static var previews: some View {
        FilteredList(filterKey: "name", filterOperation: "BEGINSWITH", filterValue: "C"){ (item:Chapter) in
            let item = item.viewModel
            Text("\(item.name)")
        }
        .environment(\.managedObjectContext,PersistenceController.preview.container.viewContext)
    }
}

调用FilteredList组件

它非常舒服的定义了一个可以自己随意指定Entity以及查询条件的View,所以我们可以写个简单的方式来生成一个Chapter的查询条件View:

struct ChapterListView: View {
    var body: some View {
        FilteredList(filterKey: "name", filterOperation: "BEGINSWITH", filterValue: "C"){ (item:Chapter) in
            let item = item.viewModel
            Text("\(item.name)")
        }
    }
}

更新简化ContentView

这时你会发现连ContentView都变的简单不少了:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Chapter>

    var body: some View {
        NavigationView {
            ChapterListView()
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let count = items.count
            let newItem = Chapter(context: viewContext)
            newItem.name = "Chapter \(count)"
            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

最终的FilteredList

有些时候,我们甚至不会有过滤条件,所以最终我们的FilteredList View我定型成了这样:

struct FilteredList<T: NSManagedObject,Content: View>: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest var fetchRequest: FetchedResults<T>
    let content: (T) -> Content

    var body: some View {
        List {
            ForEach(fetchRequest, id:\.self) { item in
                self.content(item)
            }
            .onDelete(perform: deleteItems)
        }
    }

    init(predicate: NSPredicate? = nil, @ViewBuilder content: @escaping (T) -> Content){
        _fetchRequest = FetchRequest<T>(
            sortDescriptors: [],
            predicate: predicate,
            animation: .default)
        self.content = content
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { fetchRequest[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

这使得predicate整体是在View中被传入的使得它更为灵活多用。以下是我们经常可以使用它的方法:

struct FilteredList_Previews: PreviewProvider {
    static var previews: some View {
        Group{
            FilteredList(predicate: NSPredicate(format: "%K BEGINSWITH %@", "name", "C")){ (item:Chapter) in
                let item = item.viewModel
                Text("\(item.name)")
}.environment(\.managedObjectContext,PersistenceController.preview.container.viewContext)

            ChapterListView()
.environment(\.managedObjectContext,PersistenceController.preview.container.viewContext)
        }
    }
}

Did you find this article valuable?

Support 老房东 by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this