I ran into a situation today where I can't tell if I'm doing something wrong, or if something else is broken. I have the following types, which defines a protocol with an associated type and a function that results in that type. (Much like SwiftUI.View). The function, itself, takes a generic parameter.
This seems like it should be fine, but the compiler fails with:
error: SwiftUIModifiers.playground:11:8: error: type 'BasicStyle' does not conform to protocol 'Style'
struct BasicStyle: Style {
^
SwiftUIModifiers.playground:7:20: note: protocol requires nested type 'Body'; do you want to add it?
associatedtype Body: View
^
If I remove the generic parameter, G, then it compiles. Is there something that generics does that makes the compiler not able to infer what Body is supposed to be, from the some View keyword?
If I had to guess, I think it's because technically, an opaque return type is dependent on the generic parameters of the function, and therefore can't be an associated type since generic associated types aren't a thing. So for example you can write something like
func someGenericFunction<T>(_ x: T) -> some Any {
return [x] // concrete type is Array<T>
}
If so, this should probably be fixed so that if an opaque return type can be inferred to be an associated type for a protocol, the return type can't be dependent on the generic parameters of the function.
Thanks for the explanation. That would make a lot of sense. Can you think of any workaround? I am realizing that this is hard to express the relationship that I am trying to express... I could move my generic parameter to be an associated type, but I'm not too keen on that because it makes everything else very messy--and the protocol shouldn't really care what that type is.
StaticFactory and its implementation Helper is a pattern to give a name to the opaque return type of make function. There was a topic with another issue but the workaround is pretty much identical Out of Line Initialization of Opaque Types - #2 by dmt
Obviously I don't know what the actual implementation is or why a generic parameter is needed, but I think the conventional design of something like this would be to pass the binding in the initializer. That way you don't need a generic parameter.
You mention that would be messy though; so more details on what you're trying to do would help someone come up with a better solution.
@adamkuipers I am basically trying to implement something like the SwiftUI *Style types, like PickerStyle or ListStyle. I'm passing in a binding for the selected value, so that I can set up a selector in my custom Picker. I'm trying to emulate the SwiftUI API as much as possible, but it might be more work than it's worth.
But why wouldn't passing the binding in the initializer work for you? It seems if the output type isn't reliant on the input type, it's probably fine that binding is set at initialization
I think that a "style" type should be as much removed from the actual implementation details as possible. If you're making a CustomPicker that can be customized by a more-or-less opaque CustomPickerStyle type, the consumer shouldn't have to care about what the SelectedValue type is. That all is an implementation detail.
In other words, it seems that the actual type of the selection value is unimportant to the style... At most, the style is dealing with moving the data around, but it doesn't matter what the type is. Thus, instead of being defined on the style it would just be a generic type on the method that is dealing with it.
I suspect that what you're trying to do is impossible unless you either stick to some type-erased version of Binding like Binding<Any>, or specify Body explicitly (which means either using AnyView, or messing with SwiftUI's implementation details like _ConditionalContent).
There seems to be a fundamental issue that it's impossible to have a type T that's chosen by the caller (and therefore get a Binding<T>), and then produce an opaque type that's independent of T. There's no way to return an opaque type from a generic function without it being dependent on the function's generic parameters. You also can't work around this by wrapping the Binding<T> in an existential type, because there's no way to "open" an existential and get the T type back, unless you use a generic function like in SE-0352 or a protocol extension, which again means losing the ability to produce an opaque type that's independent of the generic parameters.
If you look at the implementation of ListStyle and PickerStyle in the REPL, you get this:
So basically, the *Style types in SwiftUI have no associated types at all, and use internal magic to do what they do. With that in mind, in my opinion, you're probably better off implementing your own Style types by returning AnyView or taking a type-erased version of Binding.
I'm not aware of any way to make a named opaque type while still retaining a caller-decided type like G. In the example you posted above, the Helper.make function is non-generic, which forces you to use a type-erased Binding, and if it were generic, you'd no longer be able to return an arbitrary SwiftUI view without using type erasure.
The Coding.container(keyedBy:) workaround seems to be using an existential box of some kind, which in my opinion is about the same as using AnyView.
@ellie20 I understand that SwiftUI has no associated type in the *Style types. I had hoped that they had somehow hidden the magic they're using under the hood, but perhaps we'll never know.
I ended up with a nicer workaround that used an enum as my Style type, and then passed in a ViewModifier to the CustomPicker to customize the view in the way I needed instead of through the ViewStyle type.
I would have liked to be able to use an opaque return type with a function that has generic parameters. If Swift had a feature like Kotlin's in/out generics, that might help. Then perhaps you could do a declaration like func make<in G>(_ input: G) -> some View that tells the compiler that the generic parameter will never affect the output of the function.
I understand, that you will probably will want to use use multitude of modifiers on Text or otherwise build an "unspellable" result type but can use it behind your named type. You can even pass it the generic binding but you must box it (I'll use just Any for quick demo but you can build your G box that will work for your needs).
struct BasicStyle: Style {
struct _InferredBody: View {
@Binding var g: Any
var body: some View {
Text("Hello \(String(describing: g))")
.border(Color.red)
}
}
func makeBody<G>(binding: Binding<G?>) -> _InferredBody {
_InferredBody(
g: Binding<Any>(
get: { binding.wrappedValue as Any },
set: { binding.wrappedValue = $0 as? G }
)
)
}
}