Search code examples
iosswiftuiswiftui-navigationview

SwiftUI Complex Navigation Data Model Design


In iOS 16, SwiftUI introduced the new navigation API, requiring that navigation state should be driven by data. Therefore, one need to design the navigation data model before adopting the new navigation API.

I encountered a case where I find it rather difficult to design a data model for SwiftUI Navigation APIs in iOS 16. To clearly state the case, I would provide the following senario.

The app contains two major sections: Fruit Store and Recipes. In Fruit Store, user may browse and purchase different kinds of fruit; In Recipes, user may browse online recipes to make some drinks.

Both section has some rather complicated navigation system. In Fruit Store section, user start from the home page and view some fruit detail page, where he may again navigate to some fruit category page, etc. So is the case in Recipes section. In a word, neither of the section's navigation system can be categorized by a single type.

I wish to use a three column design, i.e., NavigationSplitView, as the following image shows.

App Design

I've read Apple's NavigationCookbook demo, but it didn't solve my problem. It's detail page has only one type, so using a single value to control the detail page would be an elegant solution. However that doesn't apply in my case, as my detail page has multiple types.

I wonder if there is a simple solution to solve this problem, which would be very useful for people adopting SwiftUI in large scale projects.


Solution

  • This seems pretty straight forward. Using the initialiser

    NavigationSplitView(sidebar: () -> Sidebar, content: () -> Content, detail: () -> Detail)
    

    you just need to keep track of the sidebar selection. Using NavigationLinks in the content, the NavigationSplitView handles showing the correct detail views.

    struct ContentView: View {
            
        @State private var sidebarSelection: SidebarType?
        
        var body: some View {
            NavigationSplitView {
                List(SidebarType.allCases, id: \.self, selection: $sidebarSelection) { type in
                    Text(type.name)
                }
            } content: {
                switch sidebarSelection {
                case .recipes:
                    List(Recipe.allCases) { recipe in
                        NavigationLink(recipe.name) {
                            Text("Selected: \(recipe.name)")
                        }
                    }
                case .fruits:
                    List(Fruit.allCases) { fruit in
                        NavigationLink(fruit.name) {
                            Text("Selected: \(fruit.name)")
                        }
                    }
                default:
                    Text("Select something")
                }
            } detail: {
                switch sidebarSelection {
                case .recipes:
                    Text("Select a recipe")
                case .fruits:
                    Text("Select a fruit")
                default:
                    EmptyView()
                }
            }
        }
    }
    
    struct Recipe: CaseIterable, Identifiable, Hashable {
        let id = UUID()
        let name: String
        static let allCases: [Recipe] = (1...9).map { Recipe(name: "Recipe \($0)")}
    }
    
    struct Fruit: CaseIterable, Identifiable, Hashable {
        let id = UUID()
        let name: String
        static let allCases: [Fruit] = (1...9).map { Fruit(name: "Fruit \($0)")}
    }
    
    enum SidebarType: CaseIterable, Identifiable {
        var id: Self { self }
        case recipes, fruits
        var name: String {
            switch self {
            case .recipes: return "Recipes"
            case .fruits: return "Fruit Store"
            }
        }
    }