3

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

I were able to fix the issue by adding an if else check into the binding wrapper but this feels wrong and like a bad workaround.

   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?

1
  • I didn't see any where you have a condition that checks wether the Array in your model have objects or not. So, may you are trying to remove an element from empty array. Commented Jul 5, 2022 at 16:40

1 Answer 1

4

Best would be to pass the ContentItem itself down to your ContentItemView. In order to do so see the following commented code.

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

Remarks:

  • You got your naming wrong. Uppercased for classes and structs and lowercased for properties.
  • As your ContentItem allready contains an id you can simply conform to Identifiable protocol and omit the , id: \.id argument in your ForEach loop.
Sign up to request clarification or add additional context in comments.

1 Comment

Many thanks! I didn't know it was possible to use binding with forEach in such a simple way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.