简体   繁体   中英

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. 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. This technique closely follows what SwiftUI already does but there's one thing that I can't figure out.

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.

For example, I can implement the following custom ButtonStyle that just displays the value of the current color scheme as the button's content.

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

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?

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

What I Tried That Didn't Work

I tried initializing the view style inside a computable body of a view that wraps MyView . 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. 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.

We don't have access to whatever the EnvironmentKey is that corresponds with ColorScheme , but its defaultValue must be 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".

What you're trying to do, which is unfortunately currently impossible, is to implement your own Style . There is no protocol that all of the built-in Style s use, requiring an implementation of

makeBody(configuration: Configuration)

For example, the ButtonStyle documentation tells us,

The system calls this method for each Button instance in a view hierarchy where this style is the current button style.

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.

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. 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. You probably just need to conform to DynamicProperty for whatever you're doing with State .

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