Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to loop over viewbuilder content subviews in SwiftUI

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?

like image 537
Eddy Avatar asked Oct 07 '20 06:10

Eddy


People also ask

What is a TupleView?

TupleView is a concrete View type uses to store multiple View values.

How does ForEach work in SwiftUI?

ForEach in SwiftUI is a view struct in its own right, which means you can return it directly from your view body if you want. You provide it an array of items, and you may also need to tell SwiftUI how it can identify each of your items uniquely so it knows how to update them when values change.

What is viewbuilder in SwiftUI?

A property wrapper that lets you build views declaratively. ViewBuilder is used extensively in SwiftUI to let you create new on-screen views by just listing them out in a trailing closure. It's a property wrapper applied to function parameter. Usually, it's just working behind the scenes, so you don't have to worry about it.

How to implement a view in a method in SwiftUI?

Thankfully, there is an alternative, more immediate and quick to implement, which is to implement parts of a view in methods. That is feasible thanks to a specific attribute in SwiftUI, called ViewBuilder, which according to official documentation, it is: A custom parameter attribute that constructs views from closures.

How to combine two views into one in Swift?

The swift compiler will try to find the static buildBlock method declared in @ViewBuilder struct that has two views as parameters. Let’s take a look at @ViewBuilder struct declaration to find that method. As you can see, @ViewBuilder has a static buildBlock method that accepts two views, combine them and return TupleView.

Why does this for loop not work in SwiftUI?

This example prints out the value of i five times and is valid Swift and can be run in a Swift Playground. Unfortunately, this type of For loop will not work in SwiftUI because ViewBuilder cannot handle the control flow statements.


2 Answers

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

like image 174
George Avatar answered Oct 11 '22 20:10

George


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

like image 21
Eddy Avatar answered Oct 11 '22 19:10

Eddy