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