I'm trying to create a custom Stack type for my application, exploiting the fact that its children conform to a custom protocol. Desired end-goal is
where MyView, MyView 2, etc. conform to some protocol, and in the implementation of DStack we use the protocol in some way on the child elements. For example, we apply a similar configuration to the elements, or interrogate the elements to configure the stack itself.
For my first attempt:
This produces
However, since in Swift tuples are non-nominal we can't do this.
Maybe the problem here is TupleView, and I need to traffic in my own view-group type, perhaps that models its storage as an array or something. This would require my own functionbuilder. Here's a sketch of that...
The return value... cannot be erased, because eventually we need to create a SwiftUI ZStack, and it wants to know how many arguments there are and so on.
I'm stumped. What's the right way to do this?
Code Block swift DStack { MyView() MyView2() //etc. }
where MyView, MyView 2, etc. conform to some protocol, and in the implementation of DStack we use the protocol in some way on the child elements. For example, we apply a similar configuration to the elements, or interrogate the elements to configure the stack itself.
For my first attempt:
Code Block swift import SwiftUI import PlaygroundSupport protocol Foo: View { func foo() } struct MyView1: Foo { var body: some View { Text("Hello world") } func foo() { preconditionFailure("implement this") } } struct DStack<Content>: View where Content: Foo { let builder: () -> Content init(@ViewBuilder content: @escaping () -> Content) { builder = content } var body: some View { ZStack(content: builder) //additional customization here exploiting conformance to Foo } } struct Demo: View { var body: some View { DStack { MyView1() MyView1() } } } PlaygroundPage.current.setLiveView(Demo())
This produces
Ok, fair enough. The obvious solution here is to apply a conditional conformance TupleView: Foo, which very roughly would beGeneric struct 'DStack' requires that 'TupleView<(MyView1, MyView1)>' conform to 'Foo'
Code Block swift //conform TupleView.T extension (T,V): Foo where T: Foo, V: Foo { ...} //conditionally conform TupleView extension TupleView: Foo where T: Foo { ... }
However, since in Swift tuples are non-nominal we can't do this.
Maybe the problem here is TupleView, and I need to traffic in my own view-group type, perhaps that models its storage as an array or something. This would require my own functionbuilder. Here's a sketch of that...
Code Block swift @_functionBuilder struct FooBuilder { static func buildBlock(_ children: Foo...) -> Foo { //... } }
So we need existentials for both the arguments and return type. The arguments are "straightforward", just create a type-erased wrapper.Protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements
The return value... cannot be erased, because eventually we need to create a SwiftUI ZStack, and it wants to know how many arguments there are and so on.
I'm stumped. What's the right way to do this?
Answering my own question.
So it is possible to do this via a custom _functionBuilder type. The type needs to traffic in an internal [Foo] array type and can be passed back to the ZStack via
However this is messy for a variety of reasons. One is that a custom builder is a lot to maintain. I'm not sure how many of them are optimizations, but ViewBuilder has a ton of functions for building different blocks.
Another is that this hides a lot of stuff from the typesystem (particularly, we have to use AnyView here) which is not ideal performance-wise for large hierarchies.
The real problem though is this:
In this example, we wrap the conforming view in some other opaque view which does not conform, which is the kind of idiomatic configuration users do. It is possible to implement e.g. .foregroundColor in a conforming type directly and write a .foregroundColor(...) function which returns that (and this is indeed the case for builtin SwiftUI types like Text) but doing this for any useful function added to SwiftUI.View in the indefinite future is probably fighting a losing battle.
Instead, the idiomatic solution to configure across view hierarchies at a distance like this is a combination of 2 techniques.
So it is possible to do this via a custom _functionBuilder type. The type needs to traffic in an internal [Foo] array type and can be passed back to the ZStack via
Code Block swift ZStack { let built = customBuilder() return ForEach(0..<count) { index in AnyView(built[index]) } }
However this is messy for a variety of reasons. One is that a custom builder is a lot to maintain. I'm not sure how many of them are optimizations, but ViewBuilder has a ton of functions for building different blocks.
Another is that this hides a lot of stuff from the typesystem (particularly, we have to use AnyView here) which is not ideal performance-wise for large hierarchies.
The real problem though is this:
Code Block swift DStack { MyView1() // : Foo MyView2().foregroundColor(.red) //does NOT conform to Foo }
In this example, we wrap the conforming view in some other opaque view which does not conform, which is the kind of idiomatic configuration users do. It is possible to implement e.g. .foregroundColor in a conforming type directly and write a .foregroundColor(...) function which returns that (and this is indeed the case for builtin SwiftUI types like Text) but doing this for any useful function added to SwiftUI.View in the indefinite future is probably fighting a losing battle.
Instead, the idiomatic solution to configure across view hierarchies at a distance like this is a combination of 2 techniques.
For passing configuration data from parent to child, use Environment.
For passing configuration data from child to parent, use PreferenceKey