Search code examples
swiftuiswiftui-navigationlinkswiftui-navigationsplitview

How to run `task` or `onAppear` on NavigationSplitView detail for every selection change?


I'm trying to use NavigationSplitView with a DetailView that has a task or onAppear in it, but it seems it only runs once.

enum MenuItem: String, Hashable, Identifiable, CaseIterable {
    case menu1
    case menu2
    case menu3
    case menu4
    case menu5
    
    var id: String { rawValue }
}

struct ContentView: View {
    @State var selection: MenuItem?
    
    var body: some View {
        NavigationSplitView {
            List(MenuItem.allCases, selection: $selection) { item in
                NavigationLink(value: item) {
                    Text(item.rawValue)
                }
            }
        } detail: {
            if let selection {
                DetailView(menuItem: selection)
            } else {
                Text("Default")
            }
        }
    }
}

struct DetailView: View {
    let menuItem: MenuItem
    @State var name = "Name"
    
    var body: some View {
        VStack {
            Text(menuItem.id)
            Text(name)
        }
        .task {
            // This should be an async setup code
            // but for the sake of simplicity
            // I just made it like this
            name = menuItem.id
        }
    }
}

Initial application load enter image description here

Initial menu selection enter image description here

2nd to 5th menu selection enter image description here

I know I can use onChange(of: selection) as a workaround, and then have my setup code there. But is there any other way to make task or onAppear work inside my DetailView?


Solution

  • Basing the View's id on the selection is not a good idea. It will force an entire body rebuild every time the selection changes, which will result in sluggish performance as the view hierarchy grows.

    Instead, you can use the alternate form of task(id:priority:_:) to initiate the task when the selection value changes, like so:

    struct ContentView: View {
        @State var selection: MenuItem?
        
        var body: some View {
            NavigationSplitView {
               …
            } detail: {
               …
            }
            .task(id: selection, priority: .userInitiated) { sel in
                print("selection changed:", sel)
            }
        }
    }