@State in component view causing weird UI state retention

Viewed 38

I am experimenting a bit around SwiftUI and trying to understand SwiftUI's @State behavior. I found myself preparing a component that has a @State of its own. Something like the below code snippet:

Code

I am displaying 4 WrappedTextField views in a VStack, data populated from an @Published list in the view model. There is a delete button(x) attached with all the views. On pressing the delete button, I am removing the data at that index from the data source.

struct WrappedTextField: View {
    @State var text: String
    
    var body: some View {
        Text(text)
    }
}

class ExperimentViewModel: ObservableObject {
    @Published var dataSource = ["0", "1", "2", "3"]
    
    func remove(at index: Int) {
        guard index < dataSource.count else {
            return
        }
        
        dataSource.remove(at: index)
    }
}

struct ExperimentView: View {
    @ObservedObject var viewModel: ExperimentViewModel = ExperimentViewModel()
    
    var body: some View {
        VStack {
            ForEach(0..<viewModel.dataSource.count, id: \.self) { index in
                HStack {
                    WrappedTextField(text: viewModel.dataSource[index])
                    Button(action: {
                        viewModel.remove(at: index)
                    }, label: {
                        Text("x").foregroundColor(.red)
                    })
                }
            }
        }
    }
}

Expected Behaviour

Ideally, the view should reflect the alteration and remove the WrappedTextField at that particular index.

Behaviour

As shown below, on deleting an index in the data source, it deletes it but the WrappedTextField still retains the previous values.

enter image description here

My understanding

My understanding is that somehow the @State of the particular WrappedTextField has not been updated after the delete.

Approaches that work.

If we remove the @State inside the WrappedTextField, the code will work as expected.

Question

I am still trying to figure out what is the exact reason for this behavior. What is the reason that @State is getting retained with previous values even though the data source has been changed?

1 Answers

It is because State preserves value with which it was initialised, until it is changed explicitly by changing state property within view (that is why State should be used only as internal state of view).

In your case you don't need state wrapper. Use just

struct WrappedTextField: View {
    var text: String               // << here !!
    
    var body: some View {
        Text(text)
    }
}
Related