Search code examples
iosarraysswiftswiftuiviewbuilder

Is there any way to create/extract an array of Views using @ViewBuilder in SwiftUI


I'm trying to create a simple struct that accepts an array of Views and returns an ordinary VStack containing those Views except they all are stacked diagonally.

Code:

struct LeaningTower<Content: View>: View {
    var views: [Content]
    var body: some View {
        VStack {
            ForEach(0..<views.count) { index in
                self.views[index]
                    .offset(x: CGFloat(index * 30))
            }
        }
    }
}

Now this works great but I always get annoyed whenever I have to call it:

LeaningTower(views: [Text("Something 1"), Text("Something 2"), Text("Something 3")])

Listing the views in an array like that seems extremely odd to me so I was wondering if there was a way I could use @ViewBuilder to call my LeaningTower struct like this:

LeaningTower {  // Same way how you would create a regular VStack
    Text("Something 1")
    Text("Something 2")
    Text("Something 3")
    // And then have all of these Text's in my 'views' array
}

If there's a way to use @ViewBuilder to create/extract an array of Views please let me know.

(Even if it isn't possible using @ViewBuilder literally anything that will make it look neater will help out a lot)


Solution

  • It's rare that you need to extract views from an array. If you are just looking to pass @ViewBuilder content into a view, you can simply do the following:

    struct ContentView: View {
        var body: some View {
            VStackReplica {
                Text("1st")
                Text("2nd")
                Text("3rd")
            }
        }
    }
    
    struct VStackReplica<Content: View>: View {
        @ViewBuilder let content: () -> Content
    
        var body: some View {
            VStack(content: content)
        }
    }
    

    If this isn't sufficient for your use-case, then see below.


    Recommended: use my ViewExtractor package as it's much more robust than the code here.

    I have got a generic version working, so there is no need to make multiple initializers for different lengths of tuples. In addition, the views can be anything you want (you are not restricted for every View to be the same type).

    You can find a Swift Package I made for this at GeorgeElsham/ViewExtractor. That contains more than what's in this answer, because this answer is just a simplified & basic version. Since the code is slightly different to this answer, so read the README.md first for an example.

    Back to the answer, example usage:

    struct ContentView: View {
        
        var body: some View {
            LeaningTower {
                Text("Something 1")
                Text("Something 2")
                Text("Something 3")
                Image(systemName: "circle")
            }
        }
    }
    

    Definition of your view:

    struct LeaningTower: View {
        private let views: [AnyView]
        
        init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
            views = content().getViews
        }
        
        var body: some View {
            VStack {
                ForEach(views.indices) { index in
                    views[index]
                        .offset(x: CGFloat(index * 30))
                }
            }
        }
    }
    

    TupleView extension (AKA where all the magic happens):

    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