简体   繁体   English

如何在可以生成视图但不是视图的结构中使用@State 和@Environment 属性?

[英]How to use @State and @Environment properties in a struct that can produce a view but isn't a view?

I'm trying to create a view style for customizing a custom SwiftUI view that I'm developing.我正在尝试创建一个视图样式来自定义我正在开发的自定义 SwiftUI 视图。 I declared a protocol with a makeBody function, I'm storing the style as a @State property so it can be updated, and I'm calling makeBody from inside a view.我用makeBody function 声明了一个协议,我将样式存储为@State属性以便它可以更新,并且我从视图内部调用makeBody This technique closely follows what SwiftUI already does but there's one thing that I can't figure out.该技术与SwiftUI 已经采用的技术非常相似,但有一件事我无法弄清楚。

Problem问题

There's one important behavior that the first-party view styles have that I can't replicate — that's using @State , @Environment , or any other SwiftUI property wrapper inside the custom view style.第一方视图 styles 有一个我无法复制的重要行为,即在自定义视图样式中使用@State@Environment或任何其他 SwiftUI 属性包装器。

For example, I can implement the following custom ButtonStyle that just displays the value of the current color scheme as the button's content.例如,我可以实现以下自定义ButtonStyle ,它只显示当前配色方案的值作为按钮的内容。

struct MyButtonStyle: ButtonStyle {

    @Environment(\.colorScheme) var colorScheme

    func makeBody(configuration: Configuration) -> some View {
        Text(String(describing: colorScheme))
    }

}

struct MyView: View {

    var body: some View {
        Button("foo", action: {}).buttonStyle(MyButtonStyle())
    }

}

When I preview MyView with a .dark preferred color scheme, the colorScheme property of MyButtonStyle is updated, the makeBody function is called again, and the text reads “dark”.当我使用.dark首选配色方案预览MyView时,更新了MyButtonStylecolorScheme属性,再次调用makeBody function,文本显示为“dark”。 This is all expected.这都是意料之中的。

Now, let's take a look at the custom view style that I created.现在,让我们看一下我创建的自定义视图样式。

struct MyCustomStyle /* : CustomStyle */ {

    @Environment(\.colorScheme) var colorScheme

    @ViewBuilder func makeBody(configuration: CustomStyleConfiguration) -> some View {
        Text(String(describing: colorScheme))
    }

}

struct MyView: View {

    @State var style = MyCustomStyle()

    var body: some View {
        style.makeBody(configuration: .init())
    }

}

In the above scenario, when I preview MyView with a .dark preferred color scheme, the colorScheme property is not updated, and the text reads “light” even though the label's foreground color has correctly picked up the change.在上述情况下,当我使用.dark首选配色方案预览MyView时, colorScheme属性未更新,并且文本显示为“light”,即使标签的前景色已正确拾取更改。

Question

How to make @State and @Environment properties work in the custom view style, just as they work in ButtonStyle and in views, without introducing any boilerplate to the implementation of makeBody function?如何使@State@Environment属性在自定义视图样式中工作,就像它们在ButtonStyle和视图中工作一样,而不向makeBody function 的实现引入任何样板?

I'm certain that it's possible because all the first-party view styles already support this, without styles conforming to View , and without them implementing the private _makeView API that manually adds dependencies to SwiftUI's graph (at least according to the module interface of SwiftUI ).我确信这是可能的,因为所有第一方视图 styles 已经支持这个,没有 styles 符合View ,并且没有他们实现私有_makeView API 手动添加依赖关系到 SwiftUI 的图形(至少根据模块接口SwiftUI ).

What I Tried That Didn't Work我试过但没有用

I tried initializing the view style inside a computable body of a view that wraps MyView .我尝试在包装MyView的视图的可计算body内初始化视图样式。 That doesn't work either.那也不管用。

struct ContentView: View {

    var body: some View {
        MyView(style: MyCustomStyle())
    }

}

Unacceptable Answers不可接受的答案

Wrapping the return value of the custom makeBody function in a separate view, with colorScheme property being part of that view, is a known workaround that results in correct behavior.将自定义makeBody function 的返回值包装在单独的视图中, colorScheme属性是该视图的一部分,这是一种已知的解决方法,可以产生正确的行为。 Answers suggesting this as a workaround will not be accepted because this question is all about avoiding that .建议将此作为解决方法的答案将不会被接受,因为这个问题都是为了避免这种情况

Answers saying that it is impossible to use @Environment or @State outside of a type conforming to View will not be accepted (because it is possible, see ButtonStyle ), unless they can prove that private API is what drives this.回答说不可能在符合View的类型之外使用@Environment@State将不会被接受(因为这是可能的,请参阅ButtonStyle ),除非他们可以证明private API 是驱动它的原因。

We don't have access to whatever the EnvironmentKey is that corresponds with ColorScheme , but its defaultValue must be light .我们无权访问与ColorScheme对应的任何EnvironmentKey ,但它的defaultValue必须是light That's why you can ever get a value at all, outside of a view.这就是为什么您可以在视图之外获得任何值的原因。

Your type won't ever actually be supplied with EnvironmentValues unless it's a View , ViewModifier , ButtonStyle , …Apple doesn't actually offer an exhaustive list of protocols that are considered "part of the view hierarchy".你的类型实际上永远不会提供EnvironmentValues除非它是ViewViewModifierButtonStyle ,......Apple实际上并没有提供被认为是“视图层次结构的一部分”的协议的详尽列表。

What you're trying to do, which is unfortunately currently impossible, is to implement your own Style .不幸的是,您正在尝试做的是实现您自己的Style ,这目前是不可能的。 There is no protocol that all of the built-in Style s use, requiring an implementation of没有所有内置Style使用的协议,需要实现

makeBody(configuration: Configuration)

For example, the ButtonStyle documentation tells us,例如, ButtonStyle文档告诉我们,

The system calls this method for each Button instance in a view hierarchy where this style is the current button style.系统为视图层次结构中的每个 Button 实例调用此方法,其中此样式是当前按钮样式。

All of the following fit the same profile.以下所有内容都符合相同的配置文件。 You can implement one of these styles, but you can't make a type that conforms to some parent Style protocol.您可以实现这些 styles 之一,但您不能创建符合某些父Style协议的类型。

If you don't conform to one of the "view hierarchy" protocols, then relying not on the environment, but rather, dependency injection, is necessary.如果您不遵守其中一种“视图层次结构”协议,则不依赖于环境,而是依赖注入是必要的。 Eg例如

struct MyCustomStyle {
  init(environment: EnvironmentValues) {
    colorScheme = environment.colorScheme
  }

  private let colorScheme: ColorScheme

  @ViewBuilder func makeBody() -> some View {
    Text(String(describing: colorScheme))
  }
}
struct ContentView: View {
  @Environment(\.self) private var environment

  var style: MyCustomStyle { .init(environment: environment) }

  var body: some View {
    style.makeBody()
  }
}

You have to give up on what you actually want to do unless you go work on SwiftUI at Apple.你必须放弃你真正想做的事情,除非你 go 在 Apple 工作 SwiftUI。 However, you should evaluate if you can get away with a targeted functionality subset.但是,您应该评估是否可以摆脱目标功能子集。 Eg例如

struct MyCustomStyle: ViewModifier {
  struct CustomStyleConfiguration { }

  @Environment(\.colorScheme) var colorScheme

  init(configuration: CustomStyleConfiguration = .init()) {
    self.configuration = configuration
  }

  func body(content: Content) -> some View {
    Text(String(describing: colorScheme))
  }

  private let configuration: CustomStyleConfiguration
}
struct MyView: View {
  var body: some View {
    EmptyView()
  }
}

extension MyView {
  func style(_ style: MyCustomStyle = .init()) -> some View {
    modifier(style)
  }
}
MyView().style()

Your question also mentions State , but you didn't supply an example for it.您的问题还提到State ,但您没有提供示例。 You probably just need to conform to DynamicProperty for whatever you're doing with State .无论您使用State DynamicProperty

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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