SwiftUI:从 ForEach 中删除项目导致索引超出范围

Posted

技术标签:

【中文标题】SwiftUI:从 ForEach 中删除项目导致索引超出范围【英文标题】:SwiftUI: Deleting an item from a ForEach results in Index Out of Range 【发布时间】:2021-02-18 04:41:41 【问题描述】:

我在我的 ContentView 组件中构建了一个水平滚动的 ForEach UI,它显示了一组自定义对象(结构形式)。当我尝试删除项目时,我收到“致命错误:索引超出范围”错误。

问题是当我删除一个项目时,实际的数组本身会更新,但特定的 AssetView(下面的组件)组件没有更新,因此它最终会迭代到不再存在的索引。知道问题可能是什么吗?以下是我的代码:

内容视图

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)
                       
              
    

资产视图

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)
        )
    

资产商店

这是我将所有资产对象读/写到我的应用程序的 Documents 文件夹的地方。

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!)
        
    
  

资产对象

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
    

【问题讨论】:

这是否回答了您的问题***.com/a/61706902/12299030? 不,不幸的是,这不起作用。我最终还是得到了同样的错误。 【参考方案1】:

我认为这是因为您的 ForEach 依赖于索引,而不是 Assets 本身。但是,如果您摆脱了indices,您将不得不为Asset 编写一个新的绑定。我认为它可能是这样的:

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

然后在你的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
                
            
        
    

【讨论】:

我注意到 Binding 给了我一个错误:Cannot find Binding in scope。这是因为 AssetStore 是一个类(不是结构)吗? 更正 - 这行得通!我只需要import SwiftUI。我只导入了Combine 和Foundation。谢谢!

以上是关于SwiftUI:从 ForEach 中删除项目导致索引超出范围的主要内容,如果未能解决你的问题,请参考以下文章

如果列表行在 SwiftUI 中包含文本字段,则无法从 foreach 中删除元素

在 ForEach 循环中删除项目会导致致命错误:索引超出范围

从 SwiftUI 中的列表中删除绑定

SwiftUI:删除列表中的动画?

在 SwiftUI 中删除整个列表

SwiftUI:删除行时使用 Array/Index 的 ForEach 崩溃