属性 包装器和 SwiftUI 环境:属性 包装器如何访问其封闭对象的环境?

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

SwiftUI 附带的 @FetchRequest 属性 包装器有助于声明在核心数据存储发生变化时自动更新的属性。您只需提供一个获取请求:

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

没有托管对象上下文,提取请求无法访问存储。此上下文必须在视图的环境中传递。

现在我很困惑。

是否有任何 public API 允许 属性 包装器访问其封闭对象的环境,或者让 SwiftUI 将此环境提供给属性包装器?

我们不知道 SwiftUI 的确切内部实现方式,但我们可以根据现有信息做出一些有根据的猜测。

首先,@propertyWrappers 不会 自动访问包含 struct/class 的任何类型的上下文。您可以查看 the spec 以获取相关证据。这个在进化过程中讨论过几次,但是没有接受。

因此,我们知道框架必须在运行时发生某些事情才能将 @EnvironmentObject(此处为 NSManagedObjectContext)注入 @FetchRequest。有关如何通过 Mirror API 执行类似操作的示例,您可以参见 。 (顺便说一句,那是在@Property可用之前写的,所以具体示例不再有用)。

但是,this article 建议 @State 的示例实现并推测(基于 程序集转储),而不是使用镜像 API,SwiftUI 使用 TypeMetadata 来提高速度:

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.

使用 Xcode 13(尚未在早期版本上测试)只要您的 属性 包装器实现 DynamicProperty 您就可以使用 @Environment 属性包装纸。

以下示例创建一个 属性 包装器,它从当前环境中读取 lineSpacing

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

然后您就可以像使用任何其他 属性 包装器一样使用它了:

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

这显示:

Line spacing: 0.000000

Line spacing: 99.000000

A DynamicProperty 结构可以简单地声明 @Environment 并且它将在调用 update 之前设置,例如

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