Reputation: 4044
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
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
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
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, @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.
Upvotes: 5