Новичок SwiftUI: Привязка, переключение и фатальная ошибка: Индекс вне диапазона

#swift #swiftui

Вопрос:

Очень новичок в SwiftUI amp; Swift и пытаюсь разобраться в привязках.

В частности, у меня есть цикл ForEach, для Tasks которого отображается новое TaskRow значение Tasks . В этой строке задачи есть два действия:

  • Кнопка для удаления задачи, отображаемая в соответствующей строке
  • Переключатель для изменения состояния задачи с «завершено» на «не завершено»

Без переключателя всякий раз, когда я удаляю задачу, представление обновляется просто отлично. Но при наличии переключателя удаление задачи вызывает:

 Thread 1: Fatal error: Index out of range
 

ошибка.

Я предполагаю, что это связано с $isSet привязкой к данным self.$, но я не знаю, как еще передать эти данные таким образом, чтобы переключатель обновился. Код ниже, мысли глубоко ценятся:

Список задач.свифт

 import SwiftUI

struct TaskList: View {
    @ObservedObject private var data = TaskData()
    @State private var showFinished: Bool = false

    
    var visibleTasks: [Task] {
        data.tasks.filter { task in
            showFinished || !task.finished
        }
    }
    
    var body: some View {
     
        VStack {
            Toggle(isOn: $showFinished) {
                Text("Show Finished")
            }
            
            ForEach(data.tasks.indices, id: .self) { index in
                    TaskRow(
                            isSet: self.$data.tasks[index].finished,
                            task: self.data.tasks[index],
                            onChange: { self.data.save() },
                            onDelete: { self.deleteTask(task_index: index)})
                
                    Divider()
                }
            AddTask(data: data)
        
        }
        .padding()
        .onAppear {
            data.load()
        }
        
    }
    
    func deleteTask(task_index: Int) {
        self.data.tasks.remove(at: self.data.tasks[task_index].id)
    }
}
 

ТаскРоу.свифт

импорт SwiftUI

 struct TaskRow: View {
    @Binding var isSet: Bool
    @State var task: Task
    var onChange: () -> ()
    var onDelete: () -> ()
  
    var body: some View {
        HStack() {
            Toggle("", isOn: $isSet)
                .onChange(of: isSet) { _isOn in
                   onChange()
            }
            Text(task.name)
            Spacer()
            Button(action: onDelete) {
                Image(systemName: "minus.circle")
            }
        }
        .scaledToFit()
        
    }
}
 

Комментарии:

1. Вам следует посмотреть видео демистификации SwiftUI с WWDC 2021. У тебя есть несколько маленьких ошибок.

2. Привет, ПланкТон, не могли бы вы, пожалуйста, сделать данные не приватным var, а var? Затем проверьте, не обновляются ли по-прежнему данные.задачи.индексы.

3. @MacUserT почему ты думаешь, что это может что-то изменить?

4. Забудь об этом, это не так. Тем не менее, наблюдаемый объект-это var, а не частный var.

5. @MacUserT он может быть легко приватным, если вам не нужно создавать дополнительный параметр инициализатора для этого var.

Ответ №1:

В iOS 15 вы можете сделать следующее:

 ForEach(Array($data.tasks.enumerated()), id: .element.wrappedValue) { (index, $task) in
    TaskRow(
        isSet: $task.finished,
        task: task,
        onChange: { self.data.save() },
        onDelete: { self.deleteTask(task_index: index) }
    )

    Divider()
}
 

Для iOS 14 вы получаете предупреждение ниже (и другие).:

Соответствие «Привязки» к «Последовательности» доступно только в iOS 15.0 или новее

Ответ №2:

Это происходит, когда вы перечисляете по indexes , а затем получаете привязки по индексу внутри ForEach . Это небезопасно из-за неправильных идентификаторов во время обновления состояния. Я создан ForEachIndexed для таких случаев:

 struct ForEachIndexed<Data: RandomAccessCollection, RowContent: View, ID: Hashable>: View, DynamicViewContent where Data.Index: Hashable {
    var data: [(Data.Index, Data.Element)] {
        forEach.data
    }

    let forEach: ForEach<[(Data.Index, Data.Element)], ID, RowContent>

    init(_ data: Binding<Data>,
         @ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
    ) where
    Data.Element: Identifiable,
    Data.Element.ID == ID,
    Data: MutableCollection {
        forEach = ForEach(
            Array(zip(data.wrappedValue.indices, data.wrappedValue)),
            id: .1.id
        ) { i, _ in
            rowContent(i, Binding(get: {
                data.wrappedValue[i]
            }, set: {
                data.wrappedValue[i] = $0
            }))
        }
    }

    var body: some View {
        forEach
    }
}
 

Использование:

 ForEachIndexed($data.tasks) { index, taskBinding in
    TaskRow(
        isSet: taskBinding.finished,
        task: taskBinding.wrappedValue,
        onChange: { self.data.save() },
        onDelete: { self.deleteTask(task_index: index)})

    Divider()
}