If I want to implement the "view style" pattern that various SwiftUI components have (like ButtonStyle for Button and PickerStyle for Picker, to name a few) for my own custom components, is there any guidance on how to do that? Specifically, is there a way to do that without having to resort to using AnyView?
My attempts to implement this have been fine with retaining type information using some View and generics, up until the point I need to create an interface for specifying a view style to use and propagating that down to the component that I'm styling. Using the SwiftUI environment seems like the natural way to do this propagation, but I seem to run afoul of the type system when trying to use it.
If we take ButtonStyle from SwiftUI as an example of the general structure of this pattern, we're talking about having a protocol with an associated Body type, and a corresponding makeBody function that does the actual styling of the component and returns a Body instance. Because of the associated types in this protocol, I'm unsure how to put an instance of something that conforms to the protocol into the environment for propagation.
I end up having to create a "sibling" type of sorts that more or less does the same thing as my protocol, but works with AnyView so I can have concrete types to use with the environment and pass around to my view. This doesn't seem ideal because of the use of AnyView and some of the downsides that come with that.
I've included a simplified version of this at the end of this post to showcase roughly what I'm doing now that is working, but it feels like there should be some better way to achieve this without needing to reach for AnyView. Is there something that I'm not aware of with Swift or SwiftUI that would let me ditch it, or is this the best way to achieve this right now without improvements to SwiftUI to better support this?
protocol MyViewStyle {
associatedtype Body: View
func makeBody(text: String) -> Body
}
struct TextMyViewStyle: MyViewStyle {
func makeBody(text: String) -> some View {
return Text(text)
}
}
struct AnyMyViewStyle {
let _makeBody: (String) -> AnyView
init<Style: MyViewStyle>(_ style: Style) {
self._makeBody = { text in
AnyView(style.makeBody(text: text))
}
}
func makeBody(text: String) -> AnyView {
_makeBody(text)
}
}
extension EnvironmentValues {
@Entry var myViewStyle: AnyMyViewStyle = AnyMyViewStyle(TextMyViewStyle())
}
struct MyView: View {
@Environment(\.myViewStyle)
private var style
let text: String
var body: some View {
style.makeBody(text: text)
}
}
Thank you for sharing the details of the problem you are hitting! This is exactly the kind of thing worth filing via Feedback Assistant app on macOS or on web https://feedbackassistant.apple.com: a request for first-class support for authoring custom view styles without AnyView erasure, ideally with your code sample attached.
If you do file one, posting the FB number back here will be helpful.
Julia V