在 SwiftUI 中符合 RandomAccessCollection 时,下标中的索引超出范围

Posted

技术标签:

【中文标题】在 SwiftUI 中符合 RandomAccessCollection 时,下标中的索引超出范围【英文标题】:Index out of range in subscript when conforming to RandomAccessCollection in SwiftUI 【发布时间】:2020-09-15 07:25:20 【问题描述】:

我创建了一个类并向ObservableObjectRandomAccessCollection 确认它,认为这个类是 Array 的自定义实现,但我不需要它是一个结构。我需要它作为一个类。

我尝试通过调用名为 append 的自定义函数将项目添加到类的内容中,并且效果很好,但是当我要删除时,由于某种原因它在 subscript 处崩溃我试图跟踪它,似乎就像每次我删除它时都会循环遍历包括已删除项目在内的所有内容,然后它会崩溃。所以我不太确定发生了什么。这是一段带有可重现代码的代码(只需复制并粘贴即可)

如果我将代码更改为 struct 它可以 100% 正常工作,但我需要它作为一个类。

Test.swift

import Foundation

public class Test<Element : Hashable>: ObservableObject
    @Published fileprivate var contents:[Element] = []
    
    public var count:Int 
        return self.contents.count
    
    
    init() 
        
    


public extension Test 
    func append(_ newElement: Element) 
        self.contents.append(newElement)
    
    
    func remove(_ at: Int) 
        self.contents.remove(at: at)
    


extension Test : Collection, RandomAccessCollection 
    public typealias Index = Int
    public typealias Indices = CountableRange<Int>
    
    public var startIndex: Int 
        return self.contents.startIndex
    
    
    public var endIndex: Int 
        return self.contents.endIndex
    
    
    public subscript(position: Int) -> Element 
        get 
            return self.contents[position] // Crash occurs here after trying to remove
        
    
    public func index(after i: Int) -> Int 
        return self.contents.index(after: i)
    
    
    public func index(before i: Int) -> Int 
        return self.contents.index(before: i)
    

ContentView.swift

import SwiftUI

struct ContentView: View 
    @ObservedObject var test: Test<String> = Test<String>()
    @State var input: String = ""
    
    var body: some View 
        VStack 
            HStack 
                TextField("New Item", text: self.$input)
                
                Button("Add", action: 
                    if(!self.input.isEmpty) 
                        self.test.append(self.input)
                    
                )
            
            
            Button("Remove", action: 
                self.test.remove(0)
            ).disabled(self.test.count <= 0)
            
            List 
                ForEach(self.test, id:\.self)  n in
                    Text(n)
                
            
        
    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【问题讨论】:

【参考方案1】:

ForEach 看不到数据的变化,因为self.test 是一个引用。

这是可能的解决方法(并且仍然有 Test 的 contents 私有)。使用 Xcode 11.7 / ios 13.7 测试

    List 
        ForEach(Array(self.test), id:\.self)  n in
            Text(n)
        
    

【讨论】:

我可以看到列表随着添加代码和不添加代码而发生变化;但是,即使在进行了您提出的更改后,当我尝试删除时,我仍然会遇到 Index out of range 的问题。 感谢您的回复。这确实解决了它,但我知道这是一种解决方法。我也明白ForEach 没有看到变化,但我无法理解的是为什么当我执行add 而不是remove 时它能够看到变化?另外,您是否认为目前没有黑客攻击就无法做到这一点?还是我目前在上课时所要求的是不可能的?因为上面的代码与struct 一起工作100%,但我知道结构是值类型。感谢您的帮助。【参考方案2】:

由于您超出了数组边界,您可以将集合的返回值设为可选,或者您可以在 init 中提供一个默认值,当数组为空时可以返回该值。

@ObservedObject var test: Test<String> = Test<String>(emptyValue: "")

public class Test<Element : Hashable>: ObservableObject
    private let empty: Element
    @Published fileprivate var contents:[Element] = []

    public var count:Int 
        return self.contents.count
    
    init(emptyValue: Element) 
        empty = emptyValue
    


extension Test 
    func append(_ newElement: Element) 
        self.contents.append(newElement)
    

    func remove(_ at: Int) 
        self.contents.remove(at: at)
    


extension Test : Collection, RandomAccessCollection 
    public typealias Index = Int
    public typealias Indices = CountableRange<Int>

    public var startIndex: Int 
        return self.contents.startIndex
    

    public var endIndex: Int 
        return self.contents.endIndex
    

    public subscript(position: Int) -> Element 
        get 
            position < self.contents.count ? self.contents[position] : empty
        
    
    public func index(after i: Int) -> Int 
        return self.contents.index(after: i)
    

    public func index(before i: Int) -> Int 
        return self.contents.index(before: i)
    

【讨论】:

以上是关于在 SwiftUI 中符合 RandomAccessCollection 时,下标中的索引超出范围的主要内容,如果未能解决你的问题,请参考以下文章

在不符合 MapAnnotationProtocol 的 SwiftUI 2 中添加 MapAnnotations

ForEach 在符合 Identifiable 协议后无法在 SwiftUI 中工作

SwiftUI:类型不符合协议“UIViewRepresentable”

通用结构 'ObservedObject' 要求 'Video' 符合 SwiftUI 中的 'ObservableObject'

SwiftUI - 类型“服务”不符合协议“可解码”

SwiftUI - 如何在我的视图中调用函数?