使用自定义 RandomAccessCollection 从 SwiftUI 中的列表中删除项目时发生崩溃

问题描述 投票:0回答:2

基本方法

我目前正在尝试清理我的 Core Data/SwiftUI 代码,其中一部分是将代码从我的视图移出并移入我的 ViewModel。由于

@FetchRequest
似乎在虚拟机中不起作用,我尝试创建类似的在虚拟机中工作的东西(基本上是
NSFetchedResultsController
的包装器):

class FetchedData<ResultType: NSManagedObject>: NSFetchedResultsController<NSFetchRequestResult>, NSFetchedResultsControllerDelegate, ObservableObject {
    
    @Published var value: [ResultType] = []
    
    convenience init(entity: ResultType.Type) {
         let request = NSFetchRequest<NSFetchRequestResult>()
         request.entity = ResultType.entity()
         request.sortDescriptors = []

         super.init(
            fetchRequest: request,
            managedObjectContext: PersistenceController.shared.context,
            sectionNameKeyPath: nil, 
            cacheName: nil
         )

         self.delegate = self
        
         do {
            try self.performFetch()
            value = (self.fetchedObjects as? [ResultType]) ?? []
         } catch {
            print("Could not perform fetch")
         }
     }
    
    internal func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let newValue = controller.fetchedObjects as? [ResultType] else {
            return
        }
        
        value = newValue
    }
}

到目前为止一切顺利,我可以按如下方式使用它:

struct ContentView: View {

    class ViewModel: ObservableObject {
        private var cancellables: [AnyCancellable] = []
        var data = FetchedData(entity: Element.self)
        
        init() {
            data.$value.sink { _ in
                self.objectWillChange.send()
            }
            .store(in: &cancellables)
        }
    }

    @StateObject var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.data.value, id: \.self) { element in
                // show UI for element
            }
        }
    }
}

将 FetchedData 设为集合

现在我想稍微简化一下它的用法,并将

FetchedData
制作为一个集合,这样我就可以直接使用它,而不必使用
value
。我添加了这个:

extension FetchedData: RandomAccessCollection {

    typealias Element = ResultType
    typealias Index = Int
    typealias Indices = Range<Int>
    typealias Iterator = IndexingIterator<[Element]>

    subscript(position: Int) -> ResultType {
        return value[position]
    }

    var startIndex: Int {
        return value.startIndex
    }

    var endIndex: Int {
        return value.endIndex
    }

    __consuming func makeIterator() -> IndexingIterator<[Element]> {
        return value.makeIterator()
    }
}

然后将我的视图更改为:

struct ContentView: View {

    class ViewModel: ObservableObject {
        let data = LiveData(entity: Element.self)
    }

    @StateObject var viewModel = ViewModel()

    var body: some View {
        Group {
            List {
                ForEach(viewModel.data, id: \.self) { element in
                    // show UI for element
                }
            }
        }
    }
}

问题

它...有点管用。它将显示并更新我的数据,我可以添加

Element
,视图就会更新得很好。当我尝试使用以下方法删除数据时出现问题:

.onDelete { indexSet in
    indexSet.forEach { index in
        let element = viewModel.data[index]
        viewContext.delete(element)
    }
                            
    try? viewContext.save()
}

我的应用程序将崩溃,并在扩展程序的

Fatal error: Index out of range
方法中出现
subscript
异常。

例如,如果我的集合中有 5 个元素并删除其中一个,首先会发生删除,然后 SwiftUI 显然会从

viewModel.data
查询索引 4(该元素已不存在,因为只剩下 4 个元素)。奇怪的是,SwiftUI 似乎在此之前查询了
endIndex
,它返回了正确的值,所以我不知道为什么它会查询它知道超出范围的东西。另外,似乎出于某种原因,删除最后一个元素似乎效果很好。

我真的无法理解这一点。因为它使用

viewModel.data.values
而不是仅仅
viewModel.data
来工作,我想我的
extension FetchedData: RandomAccessCollection
有问题,但我不明白这里的问题。

如有任何帮助,我们将不胜感激!

swift core-data swiftui combine observableobject
2个回答
0
投票

不确定您是否仍然遇到这个问题,但我遇到了同样的事情。我解决这个问题的唯一方法是在 ForEach 中使用索引

ForEach(viewModel.data.indices, id: \.self) { idx in
    // show UI for element
    // using viewModel.data[idx] e.g.
    Text(viewModel.data[idx].description)
}

可悲的是,我无法解释为什么这有效,但依赖底层集合却行不通。


0
投票

我也有同样的问题。我无法发表评论,因为我的声誉太低。我已经尝试了一切。我相当确定这是 SwiftUI 框架中的一个错误。

© www.soinside.com 2019 - 2024. All rights reserved.