Why can’t I use @Binding to manage NavigationList selection state?

Consider this view:

Code Block swift
import SwiftUI
struct ContentView: View {
@State var selection: ListObject?
var objects: [ListObject]
var body: some View {
NavigationView {
List {
ForEach(objects) { object in
NavigationLink(
destination:
ListObjectView(object: object), tag: object, selection: $selection,
label: {
Text(object.text)
})
}
}.listStyle(SidebarListStyle())
.toolbar {
ToolbarItem {
Button("Push item") {
selection = objects.first
}
}
}
Text("Empty view, pick something.").padding()
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
selection = objects.first
}
}
}
}


This is a simple list, where I can programmatically control the selection, and push another view into the navigation stack. I demonstrate two ways of doing this here, both with a timer and then with a button. Everything works as expected.

Now, when I change the @State variable to @Binding, and pass it in from outside, this suddenly stops working. No matter what I do, I can no longer programmatically change the selection. What’s more, the list UI itself stops working: when I tap on a list item, nothing happens, and no detail view appears.

Why do @State and @Binding behave differently here? Why does navigation link selection work with one, but not the other?

Accepted Reply

The behavior of this has improved in recent versions. Now everything works as expected.

Replies


Why do @State and @Binding behave differently here? Why does navigation link selection work with one, but not the other?

That depends on the outside, and how the @State var is declared. Please show enough code.
Here is the non-working version with binding.

Main app:

Code Block swift
@main
struct ListSelectionApp: App {
@StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView(selection: $viewModel.listSelection, objects: [ListObject(text: "first"), ListObject(text:"second"), ListObject(text: "third")])
}
}
}


ViewModel:

Code Block swift
import Foundation
struct ListObject: Identifiable, Hashable {
let id = UUID()
let text: String
}
class ViewModel: ObservableObject {
@Published var listSelection: ListObject?
{
didSet {
print("did set list selection to \(String(describing: listSelection))")
}
}
}


ContentView:

Code Block swift
import SwiftUI
struct ContentView: View {
@Binding var selection: ListObject?
var objects: [ListObject]
var body: some View {
NavigationView {
List {
ForEach(objects) { object in
NavigationLink(
destination:
ListObjectView(object: object), tag: object, selection: $selection,
label: {
Text(object.text)
})
}
}.listStyle(SidebarListStyle())
.toolbar {
ToolbarItem {
Button("Push item") {
selection = objects.first
}
}
}
Text("Empty view, pick something.").padding()
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
selection = objects.first
}
}
}
}


When the timer fires or when tapping the button, didSet of the ViewModel property is called as expected, but the UI doesn’t update.
As far as I have tried till now, @Binding does not directly trigger UI updates.
You may need @State, @ObservableObject or @StateObject somewhere in the view hierarchy.

For example:
Code Block
@main
struct ListSelectionApp: App {
@StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView(viewModel: viewModel, objects: [ListObject(text: "first"), ListObject(text:"second"), ListObject(text: "third")])
}
}
}


Code Block
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var objects: [ListObject]
var body: some View {
NavigationView {
List {
ForEach(objects) { object in
NavigationLink(
destination:
ListObjectView(object: object), tag: object, selection: $viewModel.listSelection,
label: {
Text(object.text)
})
}
}.listStyle(SidebarListStyle())
.toolbar {
ToolbarItem {
Button("Push item") {
viewModel.listSelection = objects.first
}
}
}
Text("Empty view, pick something.").padding()
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
viewModel.listSelection = objects.first
}
}
}
}


Interesting. I tried to run exactly this, with the viewModel being an ObservedObject that gets passed in. When tapping on the list items, the UI works but appears glitchy, there are some weird flashing artifacts. Updating the selection with timer or external button press does not work.

The behavior of this has improved in recent versions. Now everything works as expected.