Удаление элементов списка из списка SwiftUI
У SwiftUI есть довольно досадное ограничение, из-за которого сложно создать List
или ForEach
при получении привязки к каждому элементу для перехода к дочерним представлениям.
Наиболее часто предлагаемый подход, который я видел, - это перебрать индексы и получить привязку с помощью $arr[index]
(на самом деле, нечто подобное было предложено Apple, когда они удалилиBinding
соответствие Collection
):
@State var arr: [Bool] = [true, true, false]
var body: some View {
List(arr.indices, id: \.self) { index in
Toggle(isOn: self.$arr[index], label: { Text("\(idx)") } )
}
}
Это работает до тех пор, пока размер массива не изменится, а затем произойдет сбой с ошибкой времени выполнения индекса вне диапазона.
Вот пример, который вылетит:
class ViewModel: ObservableObject {
@Published var arr: [Bool] = [true, true, false]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.arr = []
}
}
}
struct ContentView: View {
@ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { idx in
Toggle(isOn: self.$vm.arr[idx], label: { Text("\(idx)") } )
}
}
}
Как правильно обрабатывать удаление из списка, сохраняя при этом возможность изменять его элементы с помощью привязки?
4 ответа
Используя идеи @pawello2222 и @Asperi, я придумал подход, который, как мне кажется, работает хорошо, но без излишней мерзости (все еще хакерский).
Я хотел сделать подход более общим, чем просто для упрощенного примера в вопросе, а также не тот, который нарушает разделение проблем.
Итак, я создал новое представление оболочки, которое создает привязку к элементу массива внутри себя (что, похоже, исправляет порядок недействительности / обновления состояния в соответствии с наблюдением @pawello2222) и передает привязку в качестве параметра для закрытия содержимого.
Изначально я ожидал, что мне потребуется выполнить проверку безопасности индекса, но оказалось, что для этой проблемы это не требовалось.
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
Использование:
@ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { index in
Safe(self.$vm.arr, index: index) { binding in
Toggle("", isOn: binding)
Divider()
Text(binding.wrappedValue ? "on" : "off")
}
}
}
Похоже, твой Toggle
обновляется до List
(возможно, ошибка, исправленная в SwiftUI 2.0).
Вы можете извлечь свою строку в другое представление и проверить, существует ли еще индекс.
struct ContentView: View {
@ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { index in
ToggleView(vm: self.vm, index: index)
}
}
}
struct ToggleView: View {
@ObservedObject var vm: ViewModel
let index: Int
@ViewBuilder
var body: some View {
if index < vm.arr.count {
Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
}
}
}
Таким образом ToggleView
будет обновляться послеList
.
Если вы сделаете то же самое, но внутри ContentView
он все равно выйдет из строя:
ContentView {
...
@ViewBuilder
func toggleView(forIndex index: Int) -> some View {
if index < vm.arr.count {
Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
}
}
}
SwiftUI 2.0
По результатам тестирования с Xcode 12 / iOS 14 - сбой не воспроизводится
SwiftUI 1.0+
Сбой происходит из-за болтающихся привязок к удаленным элементам (предположительно, из-за неправильного порядка аннулирования / обновления). Вот безопасный обходной путь. Протестировано с Xcode 11.4 / iOS 13.4
struct ContentView: View {
@ObservedObject var vm: ToggleViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self, rowContent: row(for:))
}
// helper function to have possibility to generate & inject proxy binding
private func row(for idx: Int) -> some View {
let isOn = Binding(
get: {
// safe getter with bounds validation
idx < self.vm.arr.count ? self.vm.arr[idx] : false
},
set: { self.vm.arr[idx] = $0 }
)
return Toggle(isOn: isOn, label: { Text("\(idx)") } )
}
}
Если кому-то интересно, я объединил Safe Solution от New dev с
ForEach
:
struct ForEachSafe<T: RandomAccessCollection & MutableCollection, C: View>: View where T.Index: Hashable {
private let bindingArray: Binding<T>
private let array: T
private let content: (Binding<T.Element>) -> C
init(_ bindingArray: Binding<T>, _ array: T, @ViewBuilder content: @escaping (Binding<T.Element>) -> C) {
self.bindingArray = bindingArray
self.array = array
self.content = content
}
var body: some View {
ForEach(array.indices, id: \.self) { index in
Safe(bindingArray, index: index) {
content($0)
}
}
}
}