Gwendal Roué
Gwendal Roué

Reputation: 4044

Property wrappers and SwiftUI environment: how can a property wrapper access the environment of its enclosing object?

The @FetchRequest property wrapper that ships with SwiftUI helps declaring properties that are auto-updated whenever a 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.

Is there any public API that allows a property wrapper to access the environment of its enclosing object, or to have SwiftUI give this environment to the property wrapper?

Upvotes: 7

Views: 2071

Answers (3)

malhal
malhal

Reputation: 30649

A DynamicProperty struct can simply declare @Environment and it will be valid before wrappedValue is called e.g.

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

Also, whenever the environment var changes, body will be called in the View that is using this DynamicProperty and it will call wrappedValue again which should lead to the latest environment value always being used.

Upvotes: 1

rraphael
rraphael

Reputation: 11066

With Xcode 13 (haven't tested on earlier versions) as long as your property wrapper implements DynamicProperty you can use the @Environment property wrapper.

The following example create a property wrapper that's read the lineSpacing from the current environment.

@propertyWrapper
struct LineSpacing: DynamicProperty {
    @Environment(\.lineSpacing) var lineSpacing: CGFloat
    
    var wrappedValue: CGFloat {
        lineSpacing
    }
}

Then you can use it just like any other property wrapper:

struct LineSpacingDisplayView: View {
    @LineSpacing private var lineSpacing: CGFloat
    
    var body: some View {
        Text("Line spacing: \(lineSpacing)")
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            LineSpacingDisplayView()
            LineSpacingDisplayView()
                .environment(\.lineSpacing, 99)
        }
    }
}

This displays:

Line spacing: 0.000000

Line spacing: 99.000000

Upvotes: 7

arsenius
arsenius

Reputation: 13256

We don't know the exact internals of how SwiftUI is implemented, but we can make some educated guesses based on the information we have available.

First, @propertyWrappers do not get automatic access to any kind of context from their containing struct/class. You can check out the spec for evidence of that. This was discussed a few times during the evolution process, but not accepted.

Therefore, we know that something has to happen at runtime for the framework to inject the @EnvironmentObject(here the NSManagedObjectContext) into the @FetchRequest. For an example of how to do something like that via the Mirror API, you can see my answer in this question. (By the way, that was written before @Property was available, so the specific example is no longer useful).

However, this article suggests a sample implementation of @State and speculates (based on an assembly dump) that rather than using the Mirror API, SwiftUI is using TypeMetadata for speed:

Reflection without Mirror

There is still a way to get fields without using Mirror. It's using metadata.

Metadata has Field Descriptor which contains accessors for fields of the type. It's possible to get fields by using it.

My various experiments result AttributeGraph.framework uses metadata internally. AttributeGraph.framework is a private framework that SwiftUI use internally for constructing ViewGraph.

You can see it by the symbols of the framework.

$ nm /System/Library/PrivateFrameworks/AttributeGraph.framework/AttributeGraph There is AG::swift::metadata_visitor::visit_field in the list of symbols. i didn't analysis the whole of assembly code but the name implies that AttributeGraph use visitor pattern to parse metadata.

Upvotes: 5

Related Questions