【问题标题】:'Fatal error: index out of range' when deleting bound object in view删除视图中的绑定对象时出现“致命错误:索引超出范围”
【发布时间】:2019-11-21 21:34:02
【问题描述】:

在修改子视图依赖于绑定对象的数组时,我在避免索引超出范围错误时遇到了一些麻烦。

我有一个名为 WorkoutList 的父视图。 WorkoutList 有一个 ActiveWorkoutStore 的 EnvironmentObject。 ActiveWorkoutStore 是一个 ObservableObject,它有一个 Workout 对象数组。我有一个从 ActiveWorkoutStore 中检索到的活动锻炼列表。我正在使用 ForEach 循环来处理这些活动锻炼的索引,并将对象绑定传递给名为 EditWorkout 的子视图作为 NavigationLink 的目标。 EditWorkout 有一个完成锻炼的按钮,该按钮将其从 ActiveWorkoutStore 的锻炼数组中删除并将其添加到 WorkoutHistoryStore。当我从 ActiveWorkoutStore 的 activeWorkouts 数组中删除这个对象时,我遇到了麻烦,立即导致索引超出范围错误。我怀疑这是因为活动视图依赖于我刚刚删除的绑定对象。我已经尝试了几种排列方式,包括将锻炼传递给 EditWorkout,然后使用它的 id 来引用 ActiveWorkoutStore 中的锻炼来执行我的操作,但遇到了类似的麻烦。我在网上看到了很多遵循这种利用 ForEach 迭代索引的模式的示例,并且我已经尽我所能地对其进行了镜像,但我怀疑我可能遗漏了该方法的细微差别。

我在下面附上了代码示例。让我知道您是否有任何问题或是否还有其他我应该包括的内容!提前感谢您的帮助!

锻炼列表(父视图)

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] = []
}

【问题讨论】:

  • WorkoutExercise、EditWorkoutExercise、WorkoutHistoryStore、ExerciseStore 中有什么内容?也许最好清理这段代码,只留下重要的部分?
  • 作为一般规则,从列表中删除内容风险更大,因为您最终可能会遇到您所描述的问题。更好的方法可能是有一个锻炼列表,然后对于每个锻炼都有一个锻炼状态变量(可能是一个枚举 WorkoutState),它可以是“活动”或“完成”。然后,任何需要来自该数组的数据的视图都可以在锻炼状态变量上使用过滤器。
  • 您可以在视图中添加if activeWorkout.exercises.indices.count > 0 { //Render List } else {// render something else} 以检查是否有要渲染的内容,同时保持@Published 变量简单... Workout 对象内的数组未发布!!!
  • @АлександрГрабовский 是的,抱歉……更多的上下文总是有帮助的!我想我已经找到了答案,并且非常喜欢 Adahus 推荐的方法,所以我将继续前进。 :)
  • @Adahus 我喜欢这个概念。打算做出这样的改变!谢谢!

标签: swift swiftui


【解决方案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)

【讨论】:

  • 非常感谢。我没有注意到这一点,它真的清楚了我遇到这个问题的原因。
  • 当我将它添加到我的代码中时,我收到“Initializer requires a body”错误。知道为什么会发生这种情况吗?
猜你喜欢
  • 1970-01-01
  • 2018-08-06
  • 1970-01-01
  • 2020-04-22
  • 2017-04-03
  • 1970-01-01
  • 1970-01-01
  • 2020-01-16
  • 1970-01-01
相关资源
最近更新 更多