Chuck H
Chuck H

Reputation: 8276

How to use the new nsPredicate dynamic property of @FetchRequest property wrapper with object passed into View

My first attempt was to set the property wrapper's nsPredicate dynamic property in .onAppear, but if the view gets reinitialized for any reason, the predicate set by .onAppear is lost. So I went back to using the init pattern.

Here is what I thought should work (but doesn't) and something that does work (however mysteriously):

struct ItemEditView : View {
    
    var item: Item
    
    @FetchRequest(fetchRequest: Attribute.fetchRequestAllInOrder(), animation: .default)
    var attributes: FetchedResults<Attribute>

    init(item: Item) {
        self.item = item
        
        // This is how I would have expected to set the dynamic property at View initialization, however
        // it crashes on this statement
        attributes.nsPredicate = NSPredicate(format: "item == %@", item)
        
        // Not sure why the below works and the above does not.
        // It seems to work as desired, however it receives this runtime warning:
        // "Context in environment is not connected to a persistent store coordinator"
        $attributes.projectedValue.wrappedValue.nsPredicate = NSPredicate(format: "item == %@", item)
    }
    
    var body: some View {
        List {
            ForEach(attributes) { attribute in
                Text("Name:\(attribute.name) Order:\(attribute.order)")
            }
        }
    }
}

So, why does the first assignment to nsPredicate crash? And after commenting out that first one, why does the second one work? Is the warning message a real issue? Is there a better way to do this? It seems like there should be a simple way to do this using the new dynamic properties.

Upvotes: 3

Views: 387

Answers (2)

malhal
malhal

Reputation: 30811

FetchRequest is a DynamicProperty that is only ready just before body is called which is before it is about to appear. If you implement a DynamicProperty yourself you'll see the func update which is called before the View's body is called and where it can access its own wrapped properties like @State and in the case of FetchRequest how it gets the managedObjectContext from the @Environment.

So to translate your 2 errors. The first one probably was "attempting to access property without being installed in a View" that is because it was used before body is ready to be called, i.e. it is not close to appearing yet. And the other one about not finding the context, is because it was accessed before its func update was called.

Another way to set the predicate is:

struct ItemEditView : View {
    
    let item: Item
    
    var request = FetchRequest<Attribute>(predicate: NSPredicate(value: false))
    var results: FetchedResults<Attribute> {
        request.wrappedValue.nsPredicate = NSPredicate(format: "item == %@", item)
        request.wrappedValue.sortDescriptors = ...
        return request.wrappedValue
    }

An advantage to doing it this way is the predicate or sort could also use an @State like a search box or sort button.

Upvotes: 0

Chuck H
Chuck H

Reputation: 8276

It turns out that (re)setting the nsPredicate property of the @FetchRequest in onAppear is really the way to go. However, to make this work, you must make sure that your View's init() method does not get called again after onAppear is called. There are several valuable hints on how to accomplish this in the Demystify SwiftUI session from this year's WWDC (WWDC21-10022).

Upvotes: 1

Related Questions