lassej
lassej

Reputation: 6494

Selection in SwiftUI NavigationView lost if List order changes

This is the test data model:

class Item: Identifiable {
  let name: String

  init( n: Int) {
    self.name = "\(n)"
  }
}

class Storage: ObservableObject {
  @Published var items = [Item( n: 1), Item( n: 2)]

  func reverse() {
    items = self.items.reversed()
  }
}

This is my content view, with a NavigationLink and a detail view with a button that reverses the item order:

struct ContentView: View {

  @ObservedObject
  var storage = Storage()

  var body: some View {
    NavigationView {
      List {
        ForEach( storage.items) { item in
          NavigationLink( destination: Button( action: {
            self.storage.reverse()
          }) {
            Text("Reverse")
          }) {
            Text( item.name).padding()
          }
        }
      }
    }
  }
}

Now if I tap on Reverse the NavigationView or List seems to lose its selection, pops the view, and pushes it again:

NavigationView losing selection

Is this expected behaviour or a bug in SwiftUI? Is there a workaround? I would expect that the detail view simply stays as it is, without reloading.

Upvotes: 5

Views: 1380

Answers (1)

pawello2222
pawello2222

Reputation: 54426

You need to specify an explicit id for your ForEach loop.

If you use a static ForEach (without the id parameter) your view is rebuilt because the data (storage.items) is changed.

Try the following:

struct ContentView: View {
    @ObservedObject
    var storage = Storage()

    var body: some View {
        NavigationView {
            List {
                ForEach(storage.items, id:\.name) { item in // <- add `id` parameter
                    NavigationLink(destination: self.destinationView) {
                        Text(item.name).padding()
                    }
                }
            }
        }
    }
    
    var destinationView: some View {
        Button(action: {
            self.storage.reverse()
        }) {
            Text("Reverse")
        }
    }
}

This method, however, only works if the original position of selected item is maintained.

In this example performing the update() from the detail screen for item 1 will not pop the NavigationLink.

class Storage: ObservableObject {
    @Published var items = [Item(n: 1), Item(n: 2)]

    func update() {
        items = [Item(n: 1), Item(n: 3)]
    }
}

Here is a workaround to make it work (use an empty NavigationLink):

struct ContentView: View {
    @ObservedObject var storage = Storage()
    @State var isLinkActive = false

    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(storage.items, id:\.name) { item in
                        Button(action: {
                            self.isLinkActive = true
                        }) {
                            Text(item.name).padding()
                        }
                    }
                }
                NavigationLink(destination: self.destinationView, isActive: $isLinkActive) {
                    EmptyView()
                }
            }
        }
    }

    var destinationView: some View {
        Button(action: {
            self.storage.reverse()
        }) {
            Text("Reverse")
        }
    }
}

Upvotes: 4

Related Questions