Search code examples
iosswiftuiswiftui-navigationsplitview

NavigationSplitView doesn't navigate to detail on iPhone (but does on iPad)


I'm trying to understand if there is a way to use NavigationSplitView where the content view is not a List. I want to have a custom content view without a List. The problem, however, is that if I don't use a List, the detail view is not presented on iPhone.

Here's my starting point code - which works fine, i.e. the detail view slides in on the iPhone.

import SwiftUI

struct ContentView: View {
    
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?

    var body: some View {

        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                
                NavigationLink(value: category) {
                    
                    Text(category.rawValue)
                }
            }
        } content: {

            if let selectedCategory {
                
                List(Recipe.allCases, selection: $selectedRecipe) { recipe in

                    NavigationLink(value: recipe) {
                        
                        Text(recipe.rawValue)
                    }
                }
            } else {
                Text("Nothing selected")
            }
        } detail: {
            if let selectedRecipe {
                Text(selectedRecipe.rawValue)
            } else {
                Text("Nothing selected")
            }
        }
    }
}

enum Category: String, CaseIterable, Identifiable {
    var id: String {
        self.rawValue
    }
    
    case main
    case settings
}

enum Recipe: String, CaseIterable, Identifiable {
    var id: String {
        self.rawValue
    }
    
    case omelet
    case cereal
}

Now, here's a modification that allows me to remove the unneeded (in my case) NavigationLink. This code also works fine. The button (when tapped) can update the selectedRecipe which causes the detail view to slide in on the iPhone.

...
                List(Recipe.allCases, selection: $selectedRecipe) { recipe in

//                    NavigationLink(value: recipe) {
//                        
//                        Text(recipe.rawValue)
//                    }
                    
                    Button {
                        self.selectedRecipe = recipe
                    } label: {
                        Text(recipe.rawValue)
                    }
                }
...

But since I don't need either a List or a NavigationLink in my custom content view, I tried this (simplified code). This works on the iPad, where the detail area displays the selectedRecipe, but on an iPhone, the detail view doesn't slide in and replace the content view.

...
                Button {
                    self.selectedRecipe = Recipe.omelet
                } label: {
                    Text(Recipe.omelet.rawValue)
                }
...

Anyway, I'm just confused about whatever magic is happening that requires a List with a selection binding to make setting the self.selectedRecipe actually cause the detail view to appear on an iPhone.

  • iOS 17 or newer
  • Xcode 15.3
  • macOS 14.4.1 "Sonoma"

Any guidance appreciated.


Solution

  • NavigationSplitView cannot be "driven" by anything other than List. See also this discussion on Apple Developer Forums.

    That said, I've found that adding a navigationDestination to the sidebar/content column does cause a navigation to the detail column. For example:

    } content: {
        if let selectedCategory {
            Button {
                self.selectedRecipe = Recipe.omelet
            } label: {
                Text(Recipe.omelet.rawValue)
            }
            .navigationDestination(item: $selectedRecipe) {
                Text($0.rawValue)
            }
        } else {
            Text("Nothing selected")
        }
    } detail: { 
        // the detail parameter here is only used for the case when nothing is selected
        // the actual detail view goes in the navigationDestination above
        Text("Nothing selected")
    }
    

    I don't think this is an intended usage of navigationDestination though, as its documentation does not mention that it can be used in a NavigationSplitView. The fact that this works could just be a coincidence, due to some implementation detail.

    Another workaround is to just hide a List with opacity(0)

    } content: {
        if let selectedCategory {
            ZStack {
                List(Recipe.allCases, selection: $selectedRecipe) { _ in
                    Text("")
                }
                .opacity(0)
                Button {
                    self.selectedRecipe = Recipe.omelet
                } label: {
                    Text(Recipe.omelet.rawValue)
                }
            }
        } else {
            Text("Nothing selected")
        }
    }