How to loop over viewbuilder content subviews in SwiftUI

5.1k views Asked by At

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?

3

There are 3 answers

2
Eddy On

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

4
George On

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

0
Maksim Gayduk On

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