17

So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other

struct BoxWithDividerView<Content: View>: View {
    let content: () -> Content
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    var body: some View {
        VStack(alignment: .center, spacing: 0) {
            // here
            
        }
        .background(Color.black)
        .cornerRadius(14)
    }
}

so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:

ForEach(content.subviews) { view  in
     view
     Divider()
}

How to do that?

pawello2222
  • 46,897
  • 22
  • 145
  • 209
Eddy
  • 274
  • 3
  • 17
  • 2
    I'm pretty sure you can't do that with `ViewBuilder` - it just gives you a single view that is a composite of the underlying views. You want to keep the same DSL syntax, you'd need to implement your own `@_functionBuilder`, similar to `ViewBuilder` – New Dev Oct 07 '20 at 06:44
  • do you know how to do it? I’m trying to use @_functionBuilder: @_functionBuilder struct UIViewFunctionBuilder { static func buildBlock(_ views: [V]) -> some View { return ForEach(views) { view in view Divider() } } } But V should conform to Identifiable – Eddy Oct 07 '20 at 08:06
  • There are some online [blogs](https://www.vadimbulavin.com/swift-function-builders-swiftui-view-builder/). I haven't personally done it, so I wouldn't be able to help. Maybe if you have a more specific question about implementing a `@_functionBuilder`, you could ask another question – New Dev Oct 07 '20 at 14:30

3 Answers3

6

I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.

GitHub link of this (but more advanced) in a Swift Package here

However, here is the answer with the same TupleView extension, but different view code.

Usage:

struct ContentView: View {
    
    var body: some View {
        BoxWithDividerView {
            Text("Something 1")
            Text("Something 2")
            Text("Something 3")
            Image(systemName: "circle")  // Different view types work!
        }
    }
}

Your BoxWithDividerView:

struct BoxWithDividerView: View {
    let content: [AnyView]
    
    init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
        self.content = content().getViews
    }
    var body: some View {
        VStack(alignment: .center, spacing: 0) {
            ForEach(content.indices, id: \.self) { index in
                if index != 0 {
                    Divider()
                }
                
                content[index]
            }
        }
//        .background(Color.black)
        .cornerRadius(14)
    }
}

And finally the main thing, the TupleView extension:

extension TupleView {
    var getViews: [AnyView] {
        makeArray(from: value)
    }
    
    private struct GenericView {
        let body: Any
        
        var anyView: AnyView? {
            AnyView(_fromValue: body)
        }
    }
    
    private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
        func convert(child: Mirror.Child) -> AnyView? {
            withUnsafeBytes(of: child.value) { ptr -> AnyView? in
                let binded = ptr.bindMemory(to: GenericView.self)
                return binded.first?.anyView
            }
        }
        
        let tupleMirror = Mirror(reflecting: tuple)
        return tupleMirror.children.compactMap(convert)
    }
}

Result:

Result

sreveq
  • 45
  • 7
George
  • 25,988
  • 10
  • 79
  • 133
  • Hey, I love your solution, but I wanna go one step further and generate BoxWithDividerView's children by using a ForEach. How would you go about doing that? I just end up with a bunch of protocol errors when I try doing that. – Lucas C. Feijo Aug 03 '21 at 18:58
  • @LucasC.Feijo Check [the repo](https://github.com/GeorgeElsham/ViewExtractor). If the feature doesn’t exist there, create an issue with more detail and I’ll be sure to have a look – George Aug 04 '21 at 08:15
1

So I ended up doing this

@_functionBuilder
struct UIViewFunctionBuilder {
    static func buildBlock<V: View>(_ view: V) -> some View {
        return view
    }
    static func buildBlock<A: View, B: View>(
        _ viewA: A,
        _ viewB: B
    ) -> some View {
        return TupleView((viewA, Divider(), viewB))
}
}

Then I used my function builder like this

struct BoxWithDividerView<Content: View>: View {
    let content: () -> Content
    init(@UIViewFunctionBuilder content: @escaping () -> Content) {
        self.content = content
    }
    var body: some View {
        VStack(spacing: 0.0) {
            content()
        }
        .background(Color(UIColor.AdUp.carbonGrey))
        .cornerRadius(14)
    }
}

But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array

Eddy
  • 274
  • 3
  • 17
  • I'm trying to accomplish the exact same thing. Did you ever come up with a more robust solution that will work with any number of views? – ab1470 Jan 13 '21 at 21:28
  • @ab1470 nope i haven’t :( let me know if you do – Eddy Jan 14 '21 at 11:20
0

Extension of Eddy's answer, using swift 5.7 and buildPartialBlock from result builders:

@resultBuilder
struct MyViewBuilder {
    static func buildPartialBlock<C: View>(first: C) -> TupleView<(C)> {
        TupleView(first)
    }
    
    static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, Divider, C1)> where C0: View, C1: View {
        TupleView((accumulated, Divider(), next))
    }
}

struct MyView<Content: View>: View {
    @MyViewBuilder var content: Content
    var body: some View {
        VStack {
            content
        }
    }
}

struct ContentView : View {
    var body: some View {
        MyView {
            Text("Text1")
            Text("Text2")
            Image(systemName: "chart.bar.fill", variableValue: 0.3)
            Text("Text3")
        }
    }
}

However, there is one catch with this approach: it does not allow us to treat Group as a "transparent container", i.e., a Group of two elements is not the same as two elements listed without group:

var body: some View {
        MyView {
            Text("Text1")
            Text("Text2")
            Group {
                Image(systemName: "chart.bar.fill", variableValue: 0.3) // no Divider() between image and text
                Text("Text3")
            }
        }
    }

If we unite last 2 elements into one Group, divider between them disappears.

The only way to make it work correctly with Group I found is described here: https://movingparts.io/variadic-views-in-swiftui

Maksim Gayduk
  • 1,051
  • 6
  • 13