ForEach over a List of (String, View) pairs

I'm in the process of making my first app (ever) using SwiftUI


I've made a fairly simple custom view for my main navigation links


struct MainMenuCapsule: View {
    let label: String
    private let dest: NavDestination
    
    init(_ label:String, destination: NavDestination) {
        self.label = label
        self.dest = destination
    }
    
    var body: some View {
        
            NavigationLink(label, destination: dest)
                .frame(width: 250, height: 100)
                .background(Capsule().fill(Color.newPrimaryColor))
                .foregroundColor(.black)
                .font(.system(size: 28, weight: .light))
                .padding()
    }
}


For my home screen, I want to use something clean like a ForEach iterating over a list of (label: String, dest: View) pairs to generate the list of capsule-links. (I briefly tried using List but the automatic NavLink formating didn't fit my design and I struggled to edit it - comments on using that approach are also welcomed.)


I've been googling and playing with things for a few hours it seems, and it doesn't seem possible (with the original issue being "Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional" - if I do that, it allows the array's declaration but issue ensue with trying to use it) but I wanted to check here before resorting to an explicit call for each MainMenuCapsule instance


struct ContentView: View {
    let menuItems = ["Recommended", "Browse", "Guidelines"]
// The style of array I want to use
//    let menuItems = [("Recommended", Dots(2)),
//                     ("Browse", Placeholder()),
//                     ("Guidelines", Placeholder())]
    
    var body: some View {
        VStack {
            // Nav Icons
            HStack {
                Image(systemName: "info.circle.fill")
                Spacer()
                Image(systemName: "gear")
            }
            .padding(.horizontal)
            
            NavigationView {
                VStack(spacing: 30) {
                    // Title and welcome message
                    HStack {
                        VStack(alignment: .leading) {
                            Text("Welcome")
                                .font(.largeTitle)
                            Text("Let's learn some maths!")
                                .font(.system(size: 20, weight: .light))
                        }
                        .padding(.horizontal, 30)
                        Spacer()
                    }
                    
                    // Main menu
                    ForEach(0..<menuitems.count) {i="" in<br="">                        MainMenuCapsule(self.menuItems[i], destination: Placeholder())
                    }
                    Spacer()
                }
            }
        }
    }
}



PS Any comments on bad swift code style are also welcomed, I take pride in trying to follow standards and doing things in the "best" way, but I've got a lot to learn;)

Answered by OOPer in 413280022

As you found, you cannot create an Array of different Views practical enough to use it.


One possible way to handle this situation might be a type-erasure.

You can create an Array of a single type `AnyView`.

struct ContentView: View {
    let menuItems: [(label: String, dest: AnyView)] = [
        ("Recommended", AnyView(Dots(2))),
        ("Browse", AnyView(Placeholder())),
        ("Guidelines", AnyView(Placeholder()))
    ]
    
    var body: some View {
        VStack {
            // Nav Icons
            //...
            
            NavigationView {
                VStack(spacing: 30) {
                    // Title and welcome message
                    //...
                    
                    // Main menu
                    ForEach(0..<menuItems.count) {i in
                        MainMenuCapsule(self.menuItems[i].label, destination: self.menuItems[i].dest)
                    }
                    Spacer()
                }
            }
        }
    }
}


The type `NavDestination` is sort of a mystery that I'm not sure you can make it work, but I believe it's worth trying.

Accepted Answer

As you found, you cannot create an Array of different Views practical enough to use it.


One possible way to handle this situation might be a type-erasure.

You can create an Array of a single type `AnyView`.

struct ContentView: View {
    let menuItems: [(label: String, dest: AnyView)] = [
        ("Recommended", AnyView(Dots(2))),
        ("Browse", AnyView(Placeholder())),
        ("Guidelines", AnyView(Placeholder()))
    ]
    
    var body: some View {
        VStack {
            // Nav Icons
            //...
            
            NavigationView {
                VStack(spacing: 30) {
                    // Title and welcome message
                    //...
                    
                    // Main menu
                    ForEach(0..<menuItems.count) {i in
                        MainMenuCapsule(self.menuItems[i].label, destination: self.menuItems[i].dest)
                    }
                    Spacer()
                }
            }
        }
    }
}


The type `NavDestination` is sort of a mystery that I'm not sure you can make it work, but I believe it's worth trying.

Hey, thanks, encapsulating each View inside the array in an "AnyView()" seems to have worked without any side effects!


The "NavDestination" was a generic I made to allow View passthrough to MainMenuCapsule - when I copied the code onto here it looks like some items slipped through a bit skewy

// original post
struct MainMenuCapsule: View {
// actual code
struct MainMenuCapsule<NavDestination: View>: View {

Thanks for reporting. The code editor of the forums has a severe bug that some parts enclosed in `<` and `>` may be lost.

It's a pity that Apple keeps this bug for so long time, that we experience difficulties discussing on generic related codes.


Anyway, SwiftUI is sort of developing still now and there may not be a standard or "the best" way confirmed. When you find a better solution for this situation or some faults using AnyView, please do not hesitate to share your experience.

Yeah I figured it was from the angled brackets.


A development related to this post - I had a similar situation but where I actually wanted the List style over ForEach. However this has issues as the Array of pairs doesn't conform to Hashable.


As a solution to this and to generally improve code structure I decided to make a stuct to represent the pair. After some playing and research I got the following to work successfully.


struct NavLinkPair<NavDestination: View> {
    let label: String
    let dest: NavDestination
    
    init(_ label:String, _ destination: NavDestination) { // dest param won't use _ once I get it working btw
        self.label = label
        self.dest = destination
    }
}

struct MainMenuCapsule<Content: View>: View {
    private let navPair: NavLinkPair<Content>
    
    init(_ navPair: NavLinkPair<Content>) {
        self.navPair = navPair
    }
    // body ...


However, when replacing the old "let menuItems: [(label: String, dest: AnyView)] = ..." with the use of the new struct it produced to original heterogeneous errors. As you said in your first reply "As you found, you cannot create an Array of different Views practical enough to use it."


Am I just wasting my time pursuing things like this?


Both

let test = [NavLinkPair("Label", Text(""))]
let test2 = [NavLinkPair("Label", Image(somePath))]

work fine, but as you'd expect, trying to combine them fails.

When you declare `test` as `[NavLinkPair("Label", Text(""))]`, it becomes `Array<NavLinkPair<Text>>`.

`test2` becomes `Array<NavLinkPair<Image>>`.

`NavLinkPair<Text>` and `NavLinkPair<Image>` are different types, so you cannot make an Array containing both other than `[Any]`.


So, the issue is nearly the same as an Array of (String, View), you may apply the same technique.

    let menuItems: [NavLinkPair<AnyView>] = [
        NavLinkPair("Recommended", AnyView(Dots(2))),
        NavLinkPair("Browse", AnyView(Placeholder())),
        NavLinkPair("Guidelines", AnyView(Placeholder()))
    ]


Or else, you can make your `NavLinkPair` non-generic.

struct NavLinkPair {
    let label: String
    let dest: AnyView
    
    init<NavDestination: View>(_ label:String, _ destination: NavDestination) {
        self.label = label
        self.dest = AnyView(destination)
    }
}

And create an Array of it.

    let menuItems: [NavLinkPair] = [
        NavLinkPair("Recommended", Dots(2)),
        NavLinkPair("Browse", Placeholder()),
        NavLinkPair("Guidelines", Placeholder())
    ]

Great answer and explanation, top marks!;) I didn’t really understand the issue originally as I saw ‘Text()’ and ‘Image()’ (and practically everything else in SwiftUI) as of the type ‘View’ since that’s what they return but I guess it’s a little more complicated than that. Come morning I’ll try your suggestion of replacing the generic type with AnyView since it looks more accurate to what I’m trying to do? Honestly never used generics before this, but saw it as a solution to a similar issue/post involving View pass through.

ForEach over a List of (String, View) pairs
 
 
Q