How to loop over viewbuilder content subviews in SwiftUI
Asked Answered
E

4

19

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?

Enterovirus answered 7/10, 2020 at 6:36 Comment(3)
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 ViewBuilderLeong
do you know how to do it? I’m trying to use @_functionBuilder: @_functionBuilder struct UIViewFunctionBuilder { static func buildBlock<V: View>(_ views: [V]) -> some View { return ForEach(views) { view in view Divider() } } } But V should conform to IdentifiableEnterovirus
There are some online blogs. 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 questionLeong
Z
9

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

Zetes answered 24/4, 2021 at 14:9 Comment(4)
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.Dotdotage
@LucasC.Feijo Check the repo. If the feature doesn’t exist there, create an issue with more detail and I’ll be sure to have a lookZetes
Isn't marking the content closure @escaping wrong here - it's being used in the function and not being saved to be evaluated laterDesignate
@Designate Looks like it, though I highly recommend using the Swift package instead.Zetes
E
2

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

Enterovirus answered 8/10, 2020 at 7:31 Comment(2)
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?Haughay
@Haughay nope i haven’t :( let me know if you doEnterovirus
D
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

Drippy answered 26/7, 2023 at 13:45 Comment(0)
E
0

It is possible to iterate over the subviews of a view by using https://developer.apple.com/documentation/swiftui/foreach/init(subviews:content:).

This is currently in the iOS 18 beta.

Eureetloir answered 24/7 at 20:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.