【问题标题】:SwiftUI View and @FetchRequest predicate with variable that can changeSwiftUI View 和 @FetchRequest 谓词,带有可以更改的变量
【发布时间】:2020-01-12 05:29:17
【问题描述】:

我有一个视图显示团队中使用带有固定谓词“开发人员”的@Fetchrequest 过滤的消息。

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)
                }
            }...

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

不能在属性初始化器中使用实例成员“teamName”;属性初始化器在“self”可用之前运行

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

...

我可以使用一些帮助来解决这个问题,到目前为止我无法自己解决这个问题。

【问题讨论】:

    标签: swift core-data nspredicate swiftui nsfetchrequest


    【解决方案1】:

    遇到了同样的问题,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 调用传递用户输入的数据字符串),但几乎逐字地,这段代码只需很少(如果有的话)修改即可插入。当您循环输出结果时,只需像任何其他 ForEach 一样使用 FetchRequest var(在本例中为 wordsRequest)并提取您想要的实体属性。顺便说一句,我正在将此解决方案与多个实体一起使用,并为每个实体创建一个视图。然后我在默认视图上调用结构并传递必要的变量以从所有对象中获取核心数据。
    • 这不会导致每次状态更改时都执行新的提取吗?
    • 如果您想在实体数据更改时接收更新,就像使用 @FetchRequest 一样,这将不起作用
    【解决方案2】:

    另一种可能是:

    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)
    }
    
    ...
    

    【讨论】:

    • 对我来说,这导致了以下编译器错误:Cannot invoke initializer for type 'FetchRequest&lt;_&gt;' with no arguments
    • 您需要删除 (),但无论如何我都会收到错误消息:表达式类型 'NSPredicate' is ambiguous without more context
    【解决方案3】:

    修改后的@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)
    }
    

    【讨论】:

    • 有效吗?我无法通过在 init 中使用 self 来创建 NSPredicate
    • 是的。我不确定我明白你的意思。我没有使用 self 在 init 中创建 NSPredicate。 self 仅用于将其分配给@FetchRequest var,但您需要确保在 self 之后使用下划线,例如 self._varName
    • 这是我在网上任何地方都能找到的最佳答案。非常感谢!
    • 表达式类型 'NSPredicate' 在没有更多上下文的情况下是模棱两可的
    • 这个解决方案实际上是最好的,因为它保留了一个 FetchRequest。仅在 init 中进行手动获取不会更新有关 CoreData 更改的视图。这行!完美答案
    【解决方案4】:

    可能是动态过滤@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。

    【讨论】:

    • 如果任何其他状态更改导致重新计算主体,也将执行新的提取!
    【解决方案5】:

    对于 SwiftUI,重要的是 View 结构似乎不会被更改,否则 body 将被不必要地调用,在 @FetchRequest 的情况下也会命中数据库。 SwiftUI 仅使用相等来检查 View 结构中的更改,如果不相等,即调用 body,即是否有任何属性已更改。在 iOS 14 上,即使 @FetchRequest 使用相同的参数重新创建,它也会导致 View 结构不同,从而导致 SwiftUI 的相等性检查失败,并导致在正常情况下不会重新计算正文。 @AppStorage@SceneStorage 也有这个问题,所以我觉得奇怪的是大多数人可能首先学习的 @State 没有!无论如何,我们可以使用属性不变的包装 View 来解决这个问题,这可以阻止 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 var 上进行差异化。更多信息在这里: https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/

    【讨论】:

    • 这似乎是最惯用的解决方案。
    • 就是这样。应该是答案。
    • 这个答案符合推荐的结构(Apple)将 SwiftUI 视图分解为许多小组件。在这样做的过程中,这个答案还打破了 SwiftUI 差异算法中使用的属性(如 Majid 的文章中所述),因此最小化了获取请求调用。我将此结构应用于我的一个列表(使用@SectionedFetchRequest)并注意到渲染速度更快,并且还消除了我在使用 .searchable 修饰符时遇到的问题。
    猜你喜欢
    • 2022-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-29
    • 1970-01-01
    • 2021-10-11
    • 1970-01-01
    • 2021-10-10
    相关资源
    最近更新 更多