54

I can do a static List like

List {
   View1()
   View2()
}

But how do i make a dynamic list of elements from an array? I tried the following but got error:

🛑 Closure containing control flow statement cannot be used with function builder 'ViewBuilder'

    let elements: [Any] = [View1.self, View2.self]
    
    List {
       ForEach(0..<elements.count) { index in
          if let _ = elements[index] as? View1 {
             View1()
          } else {
             View2()
          }
    }
}

Is there any work around for this? What I am trying to accomplish is a List contaning dynamic set of elements that are not statically entered.

0

10 Answers 10

56

Looks like the answer was related to wrapping my view inside of AnyView

struct ContentView : View {
    var myTypes: [Any] = [View1.self, View2.self]
    var body: some View {
        List {
            ForEach(0..<myTypes.count) { index in
                self.buildView(types: self.myTypes, index: index)
            }
        }
    }
    
    func buildView(types: [Any], index: Int) -> AnyView {
        switch types[index].self {
           case is View1.Type: return AnyView( View1() )
           case is View2.Type: return AnyView( View2() )
           default: return AnyView(EmptyView())
        }
    }
}

With this, i can now get view-data from a server and compose them. Also, they are only instanced when needed.

Sign up to request clarification or add additional context in comments.

Comments

7

if/let flow control statement cannot be used in a @ViewBuilder block.

Flow control statements inside those special blocks are translated to structs.

e.g.

if (someBool) {
    View1()
} else {
    View2()
}

is translated to a ConditionalValue<View1, View2>.

Not all flow control statements are available inside those blocks, i.e. switch, but this may change in the future.

More about this in the function builder evolution proposal.


In your specific example you can rewrite the code as follows:

struct ContentView : View {

    let elements: [Any] = [View1.self, View2.self]

    var body: some View {
        List {
            ForEach(0..<elements.count) { index in
                if self.elements[index] is View1 {
                    View1()
                } else {
                    View2()
                }
            }
        }
    }
}

2 Comments

Why not just self.elements[index] is View1?
It will build, but it wont work. using is will always make the control flow go to the else part of the If statement. is cannot work because types are inside of the array (not instances).
5

You can use dynamic list of subviews, but you need to be careful with the types and the instantiation. For reference, this is a demo a dynamic 'hamburger' here, github/swiftui_hamburger.

// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
    @Binding var currentPage: Int
    var pageArray: [AnyView]

    var body: AnyView {
        return pageArray[currentPage]
    }
}

// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
    @State var currentPage: Int = 0

    let page0 = AnyView(
        NavigationView {
            VStack {
                Text("Page Menu").color(.black)

                List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
                    Text(row)
                }.navigationBarTitle(Text("A Page"), displayMode: .large)
            }
        }
    )

    let page1 = AnyView(
        NavigationView {
            VStack {
                Text("Another Page Menu").color(.black)

                List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
                    Text(row)
                }.navigationBarTitle(Text("A Second Page"), displayMode: .large)
            }
        }
    )

    var body: some View {
        let pageArray: [AnyView] = [page0, page1]

        return Pages(currentPage: self.$currentPage, pageArray: pageArray)

    }
}

1 Comment

That List syntax no longer works seems you now need to have List { ForEach(["1", "2", "3", "4", "5"], id: \.self) { row in
5

You can do this by polymorphism:

struct View1: View {
    var body: some View {
        Text("View1")
    }
}

struct View2: View {
    var body: some View {
        Text("View2")
    }
}

class ViewBase: Identifiable {
    func showView() -> AnyView {
        AnyView(EmptyView())
    }
}

class AnyView1: ViewBase {
    override func showView() -> AnyView {
        AnyView(View1())
    }
}

class AnyView2: ViewBase {
    override func showView() -> AnyView {
        AnyView(View2())
    }
}

struct ContentView: View {
    let views: [ViewBase] = [
        AnyView1(),
        AnyView2()
    ]

    var body: some View {
        List(self.views) { view in
            view.showView()
        }
    }
}

1 Comment

AnyView is a type-erased view: "An AnyView allows changing the type of view used in a given view hierarchy. Whenever the type of view used with an AnyView changes, the old hierarchy is destroyed and a new hierarchy is created for the new type."
3

I found a little easier way than the answers above.

Create your custom view.

Make sure that your view is Identifiable

(It tells SwiftUI it can distinguish between views inside the ForEach by looking at their id property)

For example, lets say you are just adding images to a HStack, you could create a custom SwiftUI View like:

struct MyImageView: View, Identifiable {
    // Conform to Identifiable:
    var id = UUID()
    // Name of the image:
    var imageName: String

    var body: some View {
        Image(imageName)
            .resizable()
            .frame(width: 50, height: 50)
    }
}

Then in your HStack:

// Images:
HStack(spacing: 10) {
    ForEach(images, id: \.self) { imageName in
        MyImageView(imageName: imageName)
    }
    Spacer()
}

1 Comment

In my case, its asking to confirm to Hashable, not Identifiable.
2

SwiftUI 2

You can now use control flow statements directly in @ViewBuilder blocks, which means the following code is perfectly valid:

struct ContentView: View {
    let elements: [Any] = [View1.self, View2.self]

    var body: some View {
        List {
            ForEach(0 ..< elements.count) { index in
                if let _ = elements[index] as? View1 {
                    View1()
                } else {
                    View2()
                }
            }
        }
    }
}

SwiftUI 1

In addition to the accepted answer you can use @ViewBuilder and avoid AnyView completely:

@ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
    switch types[index].self {
    case is View1.Type: View1()
    case is View2.Type: View2()
    default: EmptyView()
    }
}

Comments

1

Swift 5

this seems to work for me.

struct AMZ1: View {
    var body: some View {         
       Text("Text")     
    }
 }

struct PageView: View {
    
   let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
    
   var body: some View {
     TabView {
       ForEach(0..<elements.count) { index in
         if self.elements[index] is AMZ1 {
             AMZ1()
           } else if self.elements[index] is AMZ2 {
             AMZ2()
           } else {
             AMZ3()
      }
   }
}

Comments

0

The problem with Paulo's example is you now have any type for your array. It's better to create a protocol for that type.

Here is my example that creates a list of folders and documents:

import SwiftUI

struct ContentView: View {
    @State var listOfItems: [MultiTypeProtocol] = []
    
    func createArray() -> Array<MultiTypeProtocol> {
        let t1 = CardType(name: "name3")
        let t2 = FolderType(name: "name1")
        let t3 = FolderType(name: "name4")
        let t4 = CardType(name: "name2")
        return [t1, t2, t3, t4]
    }
    
    var body: some View {
        VStack {
            List(listOfItems, id: \.id) {obj in
                switch obj.self {
                case is CardType: AnyView(DocumentView(name: obj.name))
                   case is FolderType: AnyView(FolderView(name: obj.name))
                   default:  AnyView(EmptyView())
                }
            }
        }
        .padding()
        .onAppear {
            listOfItems = createArray().shuffled()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct CardType: Identifiable, MultiTypeProtocol {
    var id: String = UUID().uuidString
    var name: String
}
struct FolderType: Identifiable, MultiTypeProtocol {
    var id: String = UUID().uuidString
    var name: String
}

protocol MultiTypeProtocol {
    var id: String { get set }
    var name: String { get set }
}

struct DocumentView: View {
    var name: String = "Untitled"
    var body: some View {
        HStack{
            Image(systemName: "doc")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text(name)
        }
    }
}

struct FolderView: View {
    var name: String = "Untitled"
    var body: some View {
        VStack {
            DisclosureGroup(
                content: {Text(name)},
                label: {
                    Image(systemName: "folder")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    Text(name)
                }
            )
        }
    }
}

So you create keep adding more objects to the list as long as they conform to MultiTypeProtocol.

Comments

-1
import SwiftUI

struct ContentView: View {
    
    var animationList: [Any] = [
        AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
    ]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<animationList.count) { index in
                    NavigationLink(
                        destination: animationIndex(types: animationList, index: index),
                        label: {
                            listTitle(index: index)
                        })
                }
                
            }
            .navigationBarTitle("Animations")
        }
    }
    
    @ViewBuilder
    func listTitle(index: Int) -> some View {
        switch index {
        case 0:
            Text("AnimationDemo").font(.title2).bold()
        case 1:
            Text("WithAnimationDemo").font(.title2).bold()
        case 2:
            Text("TransitionDemo").font(.title2).bold()
        default:
            EmptyView()
        }
    }
    
    @ViewBuilder
    func animationIndex(types: [Any], index: Int) -> some View {
        switch types[index].self {
        case is AnimationDemo.Type:
            AnimationDemo()
        case is WithAnimationDemo.Type:
            WithAnimationDemo()
        case is TransitionDemo.Type:
            TransitionDemo()
        default:
            EmptyView()
        }
    }
}

enter image description here

2 Comments

Please add some explanation to your answer such that others can learn from it
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
-1

Your original code works nowadays. the only issue is that you have to compare types with Type like if elements[index] is View1.Type.

But generally speaking, you can have craft conditional View type using ViewBuilder:

@ViewBuilder
func viewForIndex(_ index: Int) -> some View {
    switch index {
    case 0: View1()
    case 1: View2()
    default: Text("Unsupported index: \(index)")
    }
}

This helps you craft your conditional view which is NOT different views but acts as you wanted.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.