Ошибка выбора средства выбора SwiftUI и поле поиска, приводящие к исчезновению элементов списка

В этом коде при выборе элементов в средстве выбора возникают две проблемы:

Эта ошибка возникает при выборе элемента в первом средстве выбора. Выбор кажется недействительным, и я получаю указанную выше ошибку как для родительских, так и для дочерних элементов.

Проблема с полем поиска: когда я использую поле поиска, все элементы в списке исчезают, хотя совпадения должны быть. Может ли кто-нибудь помочь мне решить эти проблемы? Вот соответствующий фрагмент кода:

import SwiftUI

struct Parent: Hashable {
    let title: String
    let children: [Child]
}

struct Child: Hashable {
    let title: String
}

extension Parent {
    static var mock: [Parent] = [
        .init(title: "Parent 1", children: [.init(title: "Child 1"), .init(title: "Child 2")]),
        .init(title: "Parent 2", children: [.init(title: "Child 3"), .init(title: "Child 4"), .init(title: "Child 5")])
    ]
}

@MainActor
@Observable
final class TestViewModel {
    var parents: [Parent] = Parent.mock
    var selectedParent: Parent
    var searchParentText = ""
    var searchParents: [Parent] {
        if searchParentText.isEmpty {
            return parents
        } else {
            return parents.filter { $0.title.contains(searchParentText) }
        }
    }
    
    var children: [Child] {
        selectedParent.children
    }
    var selectedChild: Child
    var searchChildText = ""
    var searchChildren: [Child] {
        if searchChildText.isEmpty {
            return children
        } else {
            return children.filter { $0.title.contains(searchChildText) }
        }
    }
    
    init() {
        self._selectedParent = .mock[0]
        self._selectedChild = Parent.mock[0].children[0]
    }
    
    func updateSelectedItems() {
        self.selectedChild = self.selectedParent.children[0]
    }
}

@MainActor
struct TestSwiftUIView: View {
    @State var vm = TestViewModel()
    
    var body: some View {
        NavigationStack {
            Form {
                Picker("Parents",
                       selection: $vm.selectedParent) {
                    ForEach(vm.searchParents, id: \.self) { parent in
                        Text(parent.title)
                            .tag(parent.title)
                            .searchable(text: $vm.searchParentText)
                    }
                }
                       .pickerStyle(.navigationLink)
                
                Picker("Parents",
                       selection: $vm.selectedChild) {
                    ForEach(vm.searchChildren, id: \.self) { child in
                        Text(child.title)
                            .tag(child.title)
                            .searchable(text: $vm.searchChildText)
                    }
                }
                       .pickerStyle(.navigationLink)
            }
            .onChange(of: vm.selectedParent) {
                vm.updateSelectedItems()
            }
        }
    }
}

Что может быть причиной этих проблем и как их исправить? Или есть лучший способ добиться двух сборщиков по отношению к родительскому/дочернему элементу, категории/подкатегории и т.д.


50
1

Ответ:

Решено

Во-первых, у вас несоответствие типов. Первый сборщик привязан к $vm.selectedParent, который имеет тип Parent. Теги имеют значение parent.title, которое относится к типу String. Теги должны иметь тот же тип, что и связанная переменная. То же самое относится и ко второму сборщику.

В действительности, тот способ, который у вас есть сейчас, действительно может работать, по крайней мере, для дочернего элемента, поскольку значения тегов сравниваются с выбираемыми элементами по их хеш-значению. Тем не менее, я бы посоветовал вам изменить теги на следующие:

Text(parent.title)
    .tag(parent)

//...

Text(child.title)
    .tag(child)

Это изменение не решит проблему, о которой вы сообщили. Причина, по которой это происходит, заключается в том, что второй сборщик содержит дочерние элементы выбранного родителя. Когда вы меняете выбор с Родителя 1 на Родителя 2, второй элемент выбора автоматически заполняется дочерними элементами Родителя 2. Однако ранее выбранный дочерний элемент принадлежал Родителю 1. Этот дочерний элемент больше не находится среди дочерних элементов Родителя. 2, поэтому вы получаете ошибку.

Когда выбор родителя изменяется, вы вызываете updateSelectedItems, чтобы выбрать первого дочернего элемента вновь выбранного родителя. К сожалению, это происходит слишком поздно. Средство выбора дочерних элементов динамически заполняется заново при изменении родительского выбора, и когда это происходит, оно не может сохранить предыдущий дочерний выбор.

Один из способов исправить это — обновить модель представления:

  • добавить переменную для хранения ранее выбранного родителя
  • добавьте наблюдателей-сеттеров в selectedParent и обновите новую переменную в willSet и didSet:
// TestViewModel

private var previouslySelectedParent: Parent?

var selectedParent: Parent {
    willSet {
        if newValue != selectedParent {
            previouslySelectedParent = selectedParent
        }
    }
    didSet {
        selectedChild = selectedParent.children[0]
        previouslySelectedParent = nil
    }
}

Затем обновите вычисляемое свойство, которое доставляет дочерние элементы:

var children: [Child] {
    var combinedChildren = selectedParent.children
    if let previouslySelectedParent, previouslySelectedParent != selectedParent {
        combinedChildren += previouslySelectedParent.children
    }
    return combinedChildren
}

Наконец, функция didSet на selectedParent теперь выполняет работу updateSelectedItems, поэтому вы можете удалить эту функцию и удалить обратный вызов .onChange:

//    func updateSelectedItems() {
//        self.selectedChild = self.selectedParent.children[0]
//    }
// TestSwiftUIView

Form {
    // ...
}
// .onChange(of: vm.selectedParent) {
//      vm.updateSelectedItems()
// }