I have an observable object that stores an array of contentItems (struct). My "root" view owns the observable object and subviews are generated with ForEach. Each subview has a textfield that should modify its content stored in the array. This works fine until I use the delete button to remove one item of the array. This causes the view to crash. The error message says: "Fatal error: Unexpectedly found nil while unwrapping an Optional value". Of corse it can't find the index because it doesn't exist any more. Why is the subview still in the render loop??
For better understanding I simplified my code. This code runs on iOS/iPadOS


Simplified code:
import SwiftUI

class obs: ObservableObject {
    
    @Published var contentArray : [contentItem] = []
    
    func removeItem(id: UUID) {
        contentArray.remove(at: contentArray.firstIndex(where: {  $0.id == id })!)
    }
}

struct contentItem  {
    var id : UUID = UUID()
    var str : String = ""
}

struct ForEachViewWithObservableObjetTest: View {
    
    @StateObject var model : obs = obs()
    
    var body: some View {
        VStack{
            Button("add") { model.contentArray.append(contentItem()) }
                .padding(.vertical)
            
            ScrollView{
                ForEach(model.contentArray, id: \.id) { content in
                    contentItemView(id: content.id, model: model)
                }
            }
        }
    }
}

struct contentItemView : View {
    
    var id : UUID
    @ObservedObject var model : obs
    
    var body: some View {
        
        HStack{
            
            TextField("Placeholder", text: $model.contentArray[ model.contentArray.firstIndex(where: { $0.id == id })! ].str)
                .fixedSize()
                .padding(3)
                .background(.teal)
                .foregroundColor(.white)
                .cornerRadius(7)
            
            Spacer()
            
            Image(systemName: "xmark.circle")
                .font(.system(size: 22))
                .foregroundColor(.red)
                 
                // tap to crash - I guess
                .onTapGesture { model.removeItem(id: id) }
            
        }.padding()
         .padding(.horizontal, 100)
    }
}

我可以通过在绑定包装器中添加if-else判断来解决这个问题,但这感觉是错误的,而且是一个糟糕的解决方法.

   TextField("Placeholder", text:
                        Binding<String>(
                            get: {
                                if let index = model.contentArray.firstIndex(where: { $0.id == id }) {
                                      return model.contentArray[index].str
                                }
                                else { return "Placeholder" }
                
                            }, set: { newValue in
                                if let index = model.contentArray.firstIndex(where: { $0.id == id }) {
                                      model.contentArray[index].str = newValue
                                }
                                else { }
                            }))

With this method I noticed that while deleting the subview, the textfield in the subview refreshes and thus causes the crash.

How can I fix this issue properly?

推荐答案

最好是将ContentItem本身传递到ContentItemView.为此,请参阅以下注释代码.

class Obs: ObservableObject {
    
    @Published var contentArray : [ContentItem] = []
    
    func removeItem(id: UUID) {
        contentArray.remove(at: contentArray.firstIndex(where: {  $0.id == id })!)
    }
}

struct ContentItem: Identifiable {
    var id : UUID = UUID()
    var str : String = ""
}

struct ForEachViewWithObservableObjetTest: View {
    
    @StateObject var model: Obs = Obs()
    
    var body: some View {
        VStack{
            Button("add") { model.contentArray.append(ContentItem()) }
                .padding(.vertical)
            
            ScrollView{
                
                //iterate over the contentarray
                // but with the binding
                ForEach($model.contentArray) { $content in
                    //pass the content binding on to the subview
                    ContentItemView(content: $content, model: model)
                }
            }
        }
    }
}

struct ContentItemView : View {
    //Add the contentitem as binding
    @Binding var content: ContentItem
    @ObservedObject var model: Obs
    
    var body: some View {
        
        HStack{
            //use the content binding
            TextField("Placeholder", text: $content.str)
                .fixedSize()
                .padding(3)
                .background(.teal)
                .foregroundColor(.white)
                .cornerRadius(7)
            
            Spacer()
            
            Image(systemName: "xmark.circle")
                .font(.system(size: 22))
                .foregroundColor(.red)
                 
                // tap to crash - I guess
                // pass the id of the contentitem on to the viewmodel
                .onTapGesture { model.removeItem(id: content.id) }
            
        }.padding()
         .padding(.horizontal, 100)
    }
}

comments :

  • 你的名字写错了.类和 struct 用大写字母表示,属性用小写字母表示.
  • 由于您的ContentItem allready包含id,您只需遵循Identifiable协议,并在ForEach循环中省略, id: \.id参数.

Swift相关问答推荐

同步问题,发送Api Call之前未设置idToken

如何取消正在视图修改器中运行的任务

如何分解阿拉伯字母?

NSWindow中以编程方式创建的TextField快捷方式与SwiftUI

SWIFT闭包使用的是陈旧的值,即使它是S@转义

如何将函数参数(字符串文字)标记为可本地化?

Swift 扩展:通过方法重载增强现有类

表视图插座单元测试失败

在 RealmSwift 中表示范围值?

macOS 的窗口框架中使用的真实类型和真实类型是什么?

如何将视图添加到多行文本视图的末尾?

快速递归:函数与闭包

如何从数据中读取以空结尾的字符串?

在 Vapor 4 中使用协议的通用流利查询

如何在 Swift 中将 XPC 自定义类列入白名单?

在 Swift 中从 HTML 生成带有图像的 PDF 而不显示打印界面

否定 #available 语句

无法分配给属性:self 是不可变的,我知道如何修复但需要理解

playground 导入:没有这样的模块Foo

类型myViewController不符合 Swift 中的协议 UIPIckerDataSource