Home > database >  Array of different Views in SwiftUI iterable in a list as NavigationLink
Array of different Views in SwiftUI iterable in a list as NavigationLink

Time:06-13

I am making an app with various screens, and I want to collect all the screens that all conform to View in an array of views. On the app's main screen, I want to show a list of all the screen titles with a NavigationLink to all of them. The problem I am having is that if I try to create a custom view struct and have a property that initialises to a given screen and returns it. I keep running into issues and the compiler forces me to change the variable to accept any View rather than just View or the erased type some View.

struct Screen: View, Identifiable {
    var id: String {
        return title
    }
    
    let title: String
    let destination: any View
    
    var body: some View {
        NavigationLink(destination: destination) { // Type 'any View' cannot conform to 'View'
            Text(title)
        }
    }
}

This works:

struct Screen<Content: View>: View, Identifiable {
    var id: String {
        return title
    }
    
    let title: String
    let destination: Content
    
    var body: some View {
        NavigationLink(destination: destination) {
            Text(title)
        }
    }
}

But the issue with this approach is that I cannot put different screens into an Array which can be fixed as follows:

struct AllScreens {
    var screens: [any View] = []
    
    init(){
        let testScreen = Screen(title: "Charties", destination: SwiftChartsScreen())
        let testScreen2 = Screen(title: "Test", destination: NavStackScreen())
        screens = [testScreen, testScreen2]
    }
}

But when I try to access the screens in a List it cannot infer what view it is without typecasting. The end result I am trying to achieve is being able to pass in an array of screens and get their title displayed in a list like below. Screenshot

At the moment I can only achieve this by hardcoding the lists elements, which works.

import SwiftUI

struct MainScreen: View {
    let screens = AppScreens.allCases
    let allScreens = AllScreens()
    var body: some View {
        NavigationStack() {
            List() {
                AppScreens.chartsScreen
                AppScreens.navStackScreen
            }
            .navigationTitle("WWDC 22")
        }
    }
}

CodePudding user response:

SwiftUI is data-driven reactive framework and Swift is strict typed language, so instead of trying to put different View types (due to generics) into one array (requires same type), we can make data responsible for providing corresponding view (that now with help of ViewBuilder is very easy).

So here is an approach. Tested with Xcode 13 / iOS 15 (NavigationView or NavigationStack - it is not important)

enum AllScreens: CaseIterable, Identifiable {   // << type !!

    case charts, navStack   // << known variants

    var id: Self { self }

    @ViewBuilder var view: some View { // corresponding view !!
        switch self {
        case .charts:
            Screen(title: "Charties", destination: SwiftChartsScreen())
        case .navStack:
            Screen(title: "Test", destination: NavStackScreen())
        }
    }
}

usage is obvious:

struct MainScreen: View {
    var body: some View {
        NavigationStack { // or `NavigationView` for backward compatibility
            List(AllScreens.allCases) {
                $0.view                 // << data knows its presenter
            }
            .navigationTitle("WWDC 22")
        }
    }
}

Test module on GitHub

  • Related