Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI View and @FetchRequest predicate with variable that can change

I have a view showing messages in a team that are filtered using @Fetchrequest with a fixed predicate '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)
                }
            }...

I want to make this predicate dynamic so that when the user switches team the messages of that team are shown. The code below gives me the following error

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

...

I can use some help with this, so far I'm not able to figure this out on my own.

like image 535
user1242574 Avatar asked Sep 10 '19 12:09

user1242574


3 Answers

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.

like image 62
Antoine Weber Avatar answered Oct 24 '22 03:10

Antoine Weber


With SwiftUI it is important that the View struct does not appear to be changed otherwise 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, i.e. if any property has changed. On 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 recomputed when normally it wouldn’t be. @AppStorage and @SceneStorage also have this problem so I find it strange that @State which most people probably learn first 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" // 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)")
        }
    }
}

Edit: it might be possible to achieve the same thing using EquatableView instead of the wrapper View to allow SwiftUI to do its diffing on the teamName only and not the FetchRequest var. More info here: https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/

like image 41
malhal Avatar answered Oct 24 '22 04:10

malhal


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)
}
like image 10
Steve Da Monsta Avatar answered Oct 24 '22 03:10

Steve Da Monsta