Выбор в SwiftUI NavigationView теряется при изменении порядка списка

Это тестовая модель данных:

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()
  }
}

Это мое представление содержимого с NavigationLink и подробное представление с кнопкой, которая меняет порядок элементов:

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()
          }
        }
      }
    }
  }
}

Теперь, если я нажму на Reverse, кажется, что NavigationView или List теряет свой выбор, всплывает представление и снова нажимает его:

NavigationView теряет выбор

Это ожидаемое поведение или ошибка в SwiftUI? Есть ли обходной путь? Я ожидал, что подробный вид просто останется без перезагрузки.


person lassej    schedule 05.07.2020    source источник
comment
Вы изменяете опубликованное значение в наблюдаемом объекте. Ваш вид будет перезагружен, что приведет к вышеуказанному поведению.   -  person davidev    schedule 05.07.2020
comment
Эффект, которого я пытаюсь достичь, такой же, как, например, приложение Notes в macOS: главное представление - это список, упорядоченный по дате последнего изменения. Затем, когда я щелкаю старую заметку и меняю ее, выбранная заметка перемещается в верхнюю часть списка. Как вы думаете, этого нельзя добиться с помощью SwiftUI?   -  person lassej    schedule 05.07.2020


Ответы (1)


Вам необходимо указать явный id для вашего ForEach цикла.

Если вы используете статический ForEach (без параметра id), ваше представление перестраивается, потому что данные (storage.items) изменены.

Попробуйте следующее:

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")
        }
    }
}

Однако этот метод работает только в том случае, если сохраняется исходное положение выбранного элемента.

В этом примере выполнение update() на подробном экране для элемента 1 не выводит NavigationLink.

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

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

Вот обходной путь, чтобы заставить его работать (используйте пустой 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")
        }
    }
}
person pawello2222    schedule 05.07.2020
comment
У меня это работает в Xcode 11.5, iOS 13. Вы также извлекли destinationView? - person pawello2222; 05.07.2020
comment
Да, я скопировал, Xcode 11.5, iOS 13 (новейшая версия). DetailView по-прежнему перемещается. - person lassej; 05.07.2020
comment
pawello2222, просто чтобы подтвердить вашу реализацию: вы нажимаете элемент списка, а затем нажимаете Reverse, и подробный вид остается наверху, не двигаясь? А затем при нажатии кнопки «Назад» элементы списка меняются местами? - person lassej; 05.07.2020
comment
@lassej На самом деле у меня это работало, пока я не вставил свой код в новый проект (думаю, что-то было ошибочно). Я обновил свой ответ, чтобы включить обходной путь. Сообщите мне, если это то, что вы ищете. - person pawello2222; 05.07.2020
comment
Спасибо за вашу помощь с этим. Ваш обходной путь решает проблему с нестабильным представлением подробностей, но теперь представление списка не показывает, какой элемент выбран. На iPhone это, вероятно, не такая уж большая проблема, но на iPad или на Mac я хочу, чтобы пользователь мог видеть, какой элемент списка выбран в данный момент, если представление отображается в разделенном виде. - person lassej; 05.07.2020
comment
Поскольку перемещаемый контроллер представления больше не отображается, выбор снимается на iPhone. По крайней мере, в UIKit. Попробуйте обойтись в симуляторе iPad, так как поведение выбора в идиомах пользовательского интерфейса iPhone и iPad отличается. - person Scott Ahten; 28.12.2020