Creating a custom stack type with Child : Protocol

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

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


Generic struct 'DStack' requires that 'TupleView<(MyView1, MyView1)>' conform to 'Foo'

Ok, fair enough. The obvious solution here is to apply a conditional conformance TupleView: Foo, which very roughly would be

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 {
      //...
    }
}


Protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements

So we need existentials for both the arguments and return type. The arguments are "straightforward", just create a type-erased wrapper.

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?










Answered by drewcrawford in 621280022
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

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.
  1. For passing configuration data from parent to child, use Environment.

  2. For passing configuration data from child to parent, use PreferenceKey

Vadim Bulavin's "View Communication Patterns in SwiftUI" covers these patterns in more detail.
Accepted Answer
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

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.
  1. For passing configuration data from parent to child, use Environment.

  2. For passing configuration data from child to parent, use PreferenceKey

Vadim Bulavin's "View Communication Patterns in SwiftUI" covers these patterns in more detail.
Creating a custom stack type with Child : Protocol
 
 
Q