1
votes

I've built a horizontal scrolling ForEach UI within my ContentView component that displays an array of custom objects (struct form). When I try to delete an item, I get an "Fatal Error: Index out of range" error.

The issue is when I delete an item, the actual array itself updates, but specific AssetView (component below) component is not updating and so it ultimately iterates to an index that no longer exists. Any idea what the issue can be? Below is my code:

ContentView

struct ContentView: View {
    
    @ObservedObject var assetStore: AssetStore    
    var body: some View {
              ScrollView (.horizontal) {
                      ForEach(assetStore.assets.indices, id: \.self) { index in
                           AssetView(
                               asset: $assetStore.assets[index],
                               assetStore: assetStore,
                               smallSize: geo.size.height <= 667
                           )
                            .padding(.bottom)
                       }
              }
    }
}

AssetView

struct AssetView: View {
    @Binding var asset: Asset
    @ObservedObject var assetStore: AssetStore
    var smallSize: Bool
    @State var editAsset: Bool = false
    
    var body: some View {
        VStack(alignment: .center, spacing: smallSize ? -10 : 0) {
                HStack {
                    TagText(tagName: asset.name)
                        .onTapGesture {
                            editAsset.toggle()
                        }
                    Spacer()
                    DisplayCurrentValues(
                        forCurrentValueText: asset.getCurrentValueString,
                        forCurrentValueLabel: "Current Value"
                    )
                    .onTapGesture {
                        editAsset.toggle()
                    }
                }
            DisplayStepper(asset: $asset, title: "YoY Growth", type: .growth)
            DisplayStepper(asset: $asset, title: "Recurring Deposit", type: .recurring)
            }
        .sheet(isPresented: $editAsset, content: {
            EditAsset(assetStore: assetStore, currentValue: String(asset.currentValue), name: asset.name, asset: $asset)
        })
    }
}

AssetStore

This is where I read/write all of the asset objects to my App's Documents folder.

class AssetStore: ObservableObject {
  let assetsJSONURL = URL(fileURLWithPath: "Assets",
                         relativeTo: FileManager.documentsDirectoryURL).appendingPathExtension("json")
  
    @Published var assets: [Asset] = [] {
        didSet {
          saveJSONAssets()
        }
    }
    
    init() {
        print(assetsJSONURL)
        loadJSONAssets()
    }
  
  private func loadJSONAssets() {
    guard FileManager.default.fileExists(atPath: assetsJSONURL.path) else {
        return
    }
    
    let decoder = JSONDecoder()
    
    do {
      let assetsData = try Data(contentsOf: assetsJSONURL)
      assets = try decoder.decode([Asset].self, from: assetsData)
    } catch let error {
      print(error)
    }
  }
  
  private func saveJSONAssets() {
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    do {
      let assetsData = try encoder.encode(assets)
      
      try assetsData.write(to: assetsJSONURL, options: .atomicWrite)
    } catch let error {
      print(error)
    }
  }

    public func deleteAsset(atIndex id: Asset) {
        let index = assets.firstIndex(where: { $0.id == id.id})
        assets.remove(at: index!)
        
    }
  
}

Asset Object

struct Asset: Identifiable, Codable, Hashable {
    // currently set for 10 years
    let id = UUID()

    enum Frequency: String, Codable, CaseIterable {
        case month = "Month"
        case year = "Year"
        case none = "None"
    }
    
    let years: Int
    var name: String
    var currentValue: Int
    var growth: Int
    var recurringDeposit: Int
    var recurringFrequency: Frequency

    enum CodingKeys: CodingKey {
        case id
        case years
        case name
        case currentValue
        case growth
        case recurringDeposit
        case recurringFrequency
    }
    
    init(
        name: String = "AssetName",
        currentValue: Int = 1000,
        growth: Int = 10,
        years: Int = 10,
        recurringDeposit: Int = 100,
        recurringFrequency: Frequency = .month
    ) {
        self.name = name
        self.currentValue = currentValue
        self.growth = growth
        self.recurringDeposit = recurringDeposit
        self.recurringFrequency = recurringFrequency
        self.years = years
    }
}
1
Does this answer your question stackoverflow.com/a/61706902/12299030? - Asperi
No unfortunately this doesn't work. I still end up getting the same exact error. - Deep

1 Answers

2
votes

I think it's because of your ForEach relying on the indices, rather than the Assets themselves. But, if you get rid of the indices, you'll have to write a new binding for the Asset. Here's what I think it could look like:

ForEach(assetStore.assets, id: \.id) { asset in
                           AssetView(
                               asset: assetStore.bindingForId(id: asset.id),
                               assetStore: assetStore,
                               smallSize: geo.size.height <= 667
                           )
                            .padding(.bottom)
}

And then in your AssetStore:

func bindingForId(id: UUID) -> Binding<Asset> {
        Binding<Asset> { () -> Asset in
            self.assets.first(where: { $0.id == id }) ?? Asset()
        } set: { (newValue) in
            self.assets = self.assets.map { asset in
                if asset.id == id {
                    return newValue
                } else {
                    return asset
                }
            }
        }
    }