Неустранимая ошибка: индекс вне диапазона при удалении связанного объекта в представлении

У меня возникли проблемы с предотвращением ошибок индекса вне диапазона при изменении массива, от которого дочернее представление зависит от привязанного объекта.

У меня есть родительский вид WorkoutList. WorkoutList имеет объект EnvironmentObject ActiveWorkoutStore. ActiveWorkoutStore - это объект ObservableObject, содержащий массив объектов Workout. У меня есть список активных тренировок, получаемых из ActiveWorkoutStore. Я использую цикл ForEach для работы с индексами этих активных тренировок и передачи привязки объекта к дочернему представлению с именем EditWorkout в качестве пункта назначения для NavigationLink. EditWorkout имеет кнопку для завершения тренировки, которая удаляет ее из массива тренировок ActiveWorkoutStore и добавляет в WorkoutHistoryStore. У меня возникают проблемы, когда я удаляю этот объект из массива activeWorkouts ActiveWorkoutStore, что немедленно вызывает ошибку индекса вне допустимого диапазона. Я подозреваю, что это связано с тем, что активное представление полагается на связанный объект, который я только что удалил. Я'Я пробовал несколько вариантов этого, включая передачу тренировки в EditWorkout, а затем использование его идентификатора для ссылки на тренировку в ActiveWorkoutStore для выполнения моих операций, но столкнулся с аналогичными проблемами. Я видел много примеров в Интернете, которые следуют этому шаблону использования ForEach для итерации по индексам, и я отразил его, насколько я могу судить, но я подозреваю, что могу упустить нюанс в подходе.

Ниже я приложил образцы кода. Дайте мне знать, если у вас есть какие-либо вопросы или есть что-то еще, что я должен добавить! Заранее спасибо за помощь!

WorkoutList (просмотр для родителей)

import SwiftUI

struct WorkoutList: View {
    @EnvironmentObject var activeWorkoutsStore: ActiveWorkoutStore
    @State private var addExercise = false
    @State private var workoutInProgress = false

    var newWorkoutButton: some View {
        Button(action: {
            self.activeWorkoutsStore.newActiveWorkout()
        }) {
            Text("New Workout")
            Image(systemName: "plus.circle")
        }
    }

    var body: some View {
        NavigationView {
            Group {
                if activeWorkoutsStore.activeWorkouts.isEmpty {
                    Text("No active workouts")
                } else {
                    List {
                        ForEach(activeWorkoutsStore.activeWorkouts.indices.reversed(), id: \.self) { activeWorkoutIndex in
                            NavigationLink(destination: EditWorkout(activeWorkout: self.$activeWorkoutsStore.activeWorkouts[activeWorkoutIndex])) {
                                Text(self.activeWorkoutsStore.activeWorkouts[activeWorkoutIndex].id.uuidString)
                            }
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Active Workouts"))
            .navigationBarItems(trailing: newWorkoutButton)
        }
    }
}

EditWorkout (Детский вид)

//
//  EditWorkout.swift
//  workout-planner
//
//  Created by Dominic Minischetti III on 11/2/19.
//  Copyright © 2019 Dominic Minischetti. All rights reserved.
//

import SwiftUI

struct EditWorkout: View {
    @EnvironmentObject var workoutHistoryStore: WorkoutHistoryStore
    @EnvironmentObject var activeWorkoutStore: ActiveWorkoutStore
    @EnvironmentObject var exerciseStore: ExerciseStore
    @Environment(\.presentationMode) var presentationMode
    @State private var addExercise = false
    @Binding var activeWorkout: Workout
    
    var currentDayOfWeek: String {
        let weekdayIndex = Calendar.current.component(.weekday, from: Date())
        return Calendar.current.weekdaySymbols[weekdayIndex - 1]
    }

    var chooseExercisesButton: some View {
        Button (action: {
            self.addExercise = true
        }) {
            HStack {
                Image(systemName: "plus.square")
                Text("Choose Exercises")
            }
        }
        .sheet(isPresented: self.$addExercise) {
            AddWorkoutExercise(exercises: self.$activeWorkout.exercises)
                .environmentObject(self.exerciseStore)

        }
    }
    
    var saveButton: some View {
        Button(action: {
            self.workoutHistoryStore.addWorkout(workout: self.$activeWorkout.wrappedValue)
            self.activeWorkoutStore.removeActiveWorkout(workout: self.$activeWorkout.wrappedValue)
            self.presentationMode.wrappedValue.dismiss()
        }) {
            Text("Finish Workout")
        }
        .disabled(self.$activeWorkout.wrappedValue.exercises.isEmpty)
    }

    var body: some View {
        Form {
            Section(footer: Text("Choose which exercises are part of this workout")) {
                chooseExercisesButton
            }
            Section(header: Text("Exercises")) {
                if $activeWorkout.wrappedValue.exercises.isEmpty {
                    Text("No exercises")
                } else {
                    ForEach(activeWorkout.exercises.indices, id: \.self) { exerciseIndex in
                        NavigationLink(destination: EditWorkoutExercise(exercise: self.$activeWorkout.exercises[exerciseIndex])) {
                            VStack(alignment: .leading) {
                                Text(self.activeWorkout.exercises[exerciseIndex].name)
                                Text("\(self.activeWorkout.exercises[exerciseIndex].sets.count) Set\(self.activeWorkout.exercises[exerciseIndex].sets.count == 1 ? "" : "s")")
                                    .font(.footnote)
                                    .opacity(0.5)
                            }
                        }
                    }
                    saveButton
                }
            }
        }
        .navigationBarTitle(Text("Edit Workout"), displayMode: .inline )
    }
}

ActiveWorkoutStore

import Foundation
import Combine

class ActiveWorkoutStore: ObservableObject {
    @Published var activeWorkouts: [Workout] = []
    
    func newActiveWorkout() {
        activeWorkouts.append(Workout())
    }
    
    func saveActiveWorkout(workout: Workout) {
        let workoutIndex = activeWorkouts.firstIndex(where: { $0.id == workout.id })!
        
        activeWorkouts[workoutIndex] = workout
    }
    
    func removeActiveWorkout(workout: Workout) {
        if let workoutIndex = activeWorkouts.firstIndex(where: { $0.id == workout.id }) {
            activeWorkouts.remove(at: workoutIndex)
        }
    }
}

Разрабатывать

import SwiftUI

struct Workout: Hashable, Codable, Identifiable {
    var id = UUID()
    var date = Date()
    var exercises: [WorkoutExercise] = []
}

1 ответ

Решение

ForEach<Range> является контейнером постоянного диапазона (обратите внимание на описание конструктора ниже), его нельзя изменять после построения.

extension ForEach where Data == Range<Int>, ID == Int, Content : View {

    /// Creates an instance that computes views on demand over a *constant*
    /// range.
    ///
    /// This instance only reads the initial value of `data` and so it does not
    /// need to identify views across updates.
    ///
    /// To compute views on demand over a dynamic range use
    /// `ForEach(_:id:content:)`.
    public init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}

Если вы хотите изменить контейнер, вы должны использовать ForEach(activeWorkout.exercises)

Другие вопросы по тегам