SwiftUI View and @FetchRequest predicate with variable that can change

后端 未结 5 1330
予麋鹿
予麋鹿 2020-12-02 14:06

I have a view showing messages in a team that are filtered using @Fetchrequest with a fixed predicate \'Developers\'.

struct ChatView: View {

@FetchRequest(         


        
相关标签:
5条回答
  • 2020-12-02 14:06

    had the same problem, and a comment of Brad Dillon showed the solution:

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

    in this example, you can modify the predicate in the initializer.

    0 讨论(0)
  • 2020-12-02 14:08

    Another possibility is:

    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)
    }
    
    ...
    
    0 讨论(0)
  • 2020-12-02 14:08

    May be a more general solution for dynamically filtering @FetchRequest.

    1、Create custom 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、how to use

    //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
        }
    }
    

    In this way, you can dynamic change your NSPredicate or NSSortDescriptor.

    0 讨论(0)
  • 2020-12-02 14:12

    Modified @FKDev answer to work, as it throws an error, I like this answer because of its cleanliness and consistency with the rest of SwiftUI. Just need to remove the parentheses from the fetch request. Although @Antoine Weber answer works just the same.

    But I am experience an issue with both answers, include mine below. This causes a weird side effect where some rows not related to the fetch request animate off screen to the right then back on screen from the left only the first time the fetch request data changes. This does not happen when the fetch request is implemented the default SwiftUI way.

    UPDATE: Fixed the issue of random rows animating off screen by simply removing the fetch request animation argument. Although if you need that argument, i'm not sure of a solution. Its very odd as you would expect that animation argument to only affect data related to that fetch request.

    @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)
    }
    
    0 讨论(0)
  • 2020-12-02 14:25

    With SwiftUI it is important that the View struct does not appear to be changed otherwise the body will be called needlessly which in the case of @FetchRequest also hits the database. SwiftUI checks for changes in View structs simply using equality and calls body if not equal. In iOS 14, even if @FetchRequest is recreated with the same parameters, it results in a View struct that is different thus fails SwiftUI's equality check and causes the body to be needlessly recomputed. Strangely although @AppStorage and @SceneStorage also have this problem, @State does not! Anyway, we can workaround this with a wrapper View with properties that do not change, which can stop SwiftUI's diffing algorithm in its tracks:

    struct ContentView: View {
        @State var teamName "Team"
        @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 {
        var 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)")
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题