简体   繁体   中英

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?

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, @propertyWrapper s 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.

A DynamicProperty struct can simply declare @Environment and it will be set before update is called eg

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

    func update(){
        // context will now be valid
        // set the context on the controller and do some fetching.
    }

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM