Property wrappers and SwiftUI environment: how can @FetchRequest work?

Hello,

I discover the existence of the @FetchRequest property wrapper that ships with Core Data.

It helps declaring properties that are auto-updated whenever the core data storage changes. You only have to provide a fetch request:

struct MyView: View {
    @FetchRequest(fetchRequest: /* some fetch request */)
    var myValues: FetchedResults<MyValue>
}

The fetch request can't access the storage without a managed object context. This context has to be passed in the view's environment.

And now I'm quite puzzled.

How can a property wrapper access the environment of its enclosing object?

1 Like

Cross-posted on Stack Overflow.

Hmmm. I guess it is not the property wrapper that accesses the environment of its enclosing object. Instead, SwiftUI injects the environment in the wrapper. But how?

FetchRequest only declares conformance to DynamicProperty, a protocol that still doesn't solve this mystery.

This is exact, @malhal! It takes time to understand how DynamicProperty can be used. Take a look at GRDBQuery one day: it brings @FetchRequest powers to raw SQLite :-)

GRDBQuery is an interesting demo of DynamicProperty however I don't agree with the same application that made a change to the database hitting the database again just to find out what changes it made! (It can't detect-cross process changes) You'd be better off with a model layer.

Sure, and GRDB won't prevent you from building your proper model layer: the library is architecture-agnostic. This does not mean it doesn't care about architecture: on the contrary, it cares a lot that developer teams feel at ease, and can build their layers in their preferred way. I hope you'd feel at home there.

Now, since Apple does not hesitate pushing a full-fledged ManagedObjectContext in the SwiftUI environment, and shipping @FetchRequest, that may be the sign that some developers might prefer it this way. Who am I do decide?

On top of that, built-in database observation is useful, because it is very robust. GRDB can even detect changes performed by raw SQL queries, as well as indirect changes triggered by foreign keys actions or SQL triggers: basically it does not miss anything that happens in a given process. Relying on built-in observation means that you don't have to write code, forget things, think about concurrency, make mistakes, and ship bugs.

Here is my latest approach:

struct FetchRequest2<ResultType>: DynamicProperty {
    @Environment(\.managedObjectContext) private var context
    @StateObject private var controller = FetchController<ResultType>()

    init(params...) {
        self.params = params
    } 

    public var wrappedValue: Result<[ResultType], Error> {
        return controller.result(for: viewContext, other params...)
    }
}

Whenever the environment var changes, body will be called in the View that installs this DynamicProperty which should lead to wrappedValue being called again and the environment var will be the latest value.