SwiftUI View 和 @FetchRequest 谓词带有可以更改的变量

SwiftUI View and @FetchRequest predicate with variable that can change

我有一个视图显示团队中使用带有固定谓词的@Fetchrequest 过滤的消息 'Developers'。

struct ChatView: View {

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)],
    predicate: NSPredicate(format: "team.name == %@", "Developers"),
    animation: .default) var messages: FetchedResults<Message>

@Environment(\.managedObjectContext)
var viewContext

var body: some View {
    VStack {
        List {
            ForEach(messages, id: \.self) { message in
                VStack(alignment: .leading, spacing: 0) {
                    Text(message.text ?? "Message text Error")
                    Text("Team \(message.team?.name ?? "Team Name Error")").font(.footnote)
                }
            }...

我想使此谓词动态化,以便在用户切换团队时显示该团队的消息。下面的代码给我以下错误

Cannot use instance member 'teamName' within property initializer; property initializers run before 'self' is available

struct ChatView: View {

@Binding var teamName: String

@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)],
    predicate: NSPredicate(format: "team.name == %@", teamName),
    animation: .default) var messages: FetchedResults<Message>

@Environment(\.managedObjectContext)
var viewContext

...

我可以在这方面寻求一些帮助,到目前为止我无法自己解决这个问题。

有同样的问题,Brad Dillon 的评论给出了解决方案:

var predicate:String
var wordsRequest : FetchRequest<Word>
var words : FetchedResults<Word>{wordsRequest.wrappedValue}

    init(predicate:String){
        self.predicate = predicate
        self.wordsRequest = FetchRequest(entity: Word.entity(), sortDescriptors: [], predicate:
            NSPredicate(format: "%K == %@", #keyPath(Word.character),predicate))

    }

在此示例中,您可以修改初始化程序中的谓词。

另一种可能是:

struct ChatView: View {

@Binding var teamName: String

@FetchRequest() var messages: FetchedResults<Message>

init() {
    let fetchRequest: NSFetchRequest<Message> = Message.fetchRequest()
    fetchRequest.sortDescriptors = NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)
    fetchRequest = NSPredicate(format: "team.name == %@", teamName),
    self._messages = FetchRequest(fetchRequest:fetchRequest, animation: .default)
}

...

修改后的@FKDev 答案可以正常工作,因为它会抛出错误,我喜欢这个答案,因为它与 SwiftUI 的其余部分保持一致。只需要从获取请求中删除括号。尽管@Antoine Weber 的回答是一样的。

但是我对这两个答案都遇到了问题,请在下面包括我的答案。这会导致奇怪的副作用,其中与获取请求无关的某些行仅在获取请求数据第一次更改时从屏幕右侧动画化,然后从左侧返回到屏幕上。当以默认的 SwiftUI 方式实现获取请求时,不会发生这种情况。

更新: 通过简单地删除获取请求动画参数,修复了屏幕外随机行动画的问题。虽然如果你需要那个论点,我不确定解决方案。这很奇怪,因为您会认为动画参数只会影响与该获取请求相关的数据。

@Binding var teamName: String

@FetchRequest var messages: FetchedResults<Message>

init() {

    var predicate: NSPredicate?
    // Can do some control flow to change the predicate here
    predicate = NSPredicate(format: "team.name == %@", teamName)

    self._messages = FetchRequest(
    entity: Message.entity(),
    sortDescriptors: [],
    predicate: predicate,
//    animation: .default)
}

可能是动态过滤@FetchRequest 的更通用的解决方案。

1、创建自定义DynamicFetchView

import CoreData
import SwiftUI

struct DynamicFetchView<T: NSManagedObject, Content: View>: View {
    let fetchRequest: FetchRequest<T>
    let content: (FetchedResults<T>) -> Content

    var body: some View {
        self.content(fetchRequest.wrappedValue)
    }

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

    init(fetchRequest: NSFetchRequest<T>, @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
        self.fetchRequest = FetchRequest<T>(fetchRequest: fetchRequest)
        self.content = content
    }
}

2、使用方法

//our managed object
public class Event: NSManagedObject{
    @NSManaged public var status: String?
    @NSManaged public var createTime: Date?
    ... ...
}

// some view

struct DynamicFetchViewExample: View {
    @State var status: String = "undo"

    var body: some View {
        VStack {
            Button(action: {
                self.status = self.status == "done" ? "undo" : "done"
            }) {
                Text("change status")
                    .padding()
            }

            // use like this
            DynamicFetchView(predicate: NSPredicate(format: "status==%@", self.status as String), sortDescriptors: [NSSortDescriptor(key: "createTime", ascending: true)]) { (events: FetchedResults<Event>) in
                // use you wanted result
                // ...
                HStack {
                    Text(String(events.count))
                    ForEach(events, id: \.self) { event in
                        Text(event.name ?? "")
                    }
                }
            }

            // or this
            DynamicFetchView(fetchRequest: createRequest(status: self.status)) { (events: FetchedResults<Event>) in
                // use you wanted result
                // ...
                HStack {
                    Text(String(events.count))
                    ForEach(events, id: \.self) { event in
                        Text(event.name ?? "")
                    }
                }
            }
        }
    }

    func createRequest(status: String) -> NSFetchRequest<Event> {
        let request = Event.fetchRequest() as! NSFetchRequest<Event>
        request.predicate = NSPredicate(format: "status==%@", status as String)
        // warning: FetchRequest must have a sort descriptor
        request.sortDescriptors = [NSSortDescriptor(key: "createTime", ascending: true)]
        return request
    }
}

通过这种方式,您可以动态更改您的 NSPredicate 或 NSSortDescriptor。

对于 SwiftUI,重要的是视图结构不会出现更改,否则 body 将被不必要地调用,在 @FetchRequest 的情况下也会访问数据库。 SwiftUI 仅使用相等来检查 View 结构中的更改,如果不相等则调用 body,即如果任何 属性 已更改。在 iOS 14 上,即使使用相同的参数重新创建 @FetchRequest,它也会导致一个不同的 View 结构,从而导致 SwiftUI 的相等性检查失败,并导致主体在通常情况下不会被重新计算. @AppStorage@SceneStorage也有这个问题所以我觉得很奇怪,大多数人最先学习的@State却没有!无论如何,我们可以使用属性不变的包装视图来解决这个问题,这可以阻止 SwiftUI 的差异算法:

struct ContentView: View {
    @State var teamName "Team" // source of truth, could also be @AppStorage if would like it to persist between app launches.
    @State var counter = 0
    var body: some View {
        VStack {
            ChatView(teamName:teamName) // its body will only run if teamName is different, so not if counter being changed was the reason for this body to be called.
            Text("Count \(counter)")
        }
    }
}

struct ChatView: View {
    let teamName: String
    var body: some View {
        // ChatList body will be called every time but this ChatView body is only run when there is a new teamName so that's ok.
        ChatList(messages: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)], predicate: NSPredicate(format: "team.name = %@", teamName)))
    }
}

struct ChatList : View {
    @FetchRequest var messages: FetchedResults<Message>
    var body: some View {
        ForEach(messages) { message in
             Text("Message at \(message.createdAt!, formatter: messageFormatter)")
        }
    }
}

编辑:使用 EquatableView 而不是包装器 View 来允许 SwiftUI 仅在 teamName 而不是 FetchRequest 变种。更多信息在这里: https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/