Leo
Leo

Reputation: 1768

How to Have a Deletable List of TextFields in SwiftUI

I try to have a list of editable strings and deletable strings that come from a string array.

struct ShortcutSettingView: View {
    
    @State var keywords: [String] = ["keyword 1", "keyword 2", "keyword 3"]
    
    var body: some View {
        Form {
            List {
                ForEach(keywords, id: \.self) { keyword in
                    TextField("", text: $keywords[keywords.firstIndex(of: keyword)!)
                }
                .onDelete(perform: deleteKeywords)
            }
        }
        .navigationTitle(Text("Generic Shortcut"))
    }
    
    private func deleteKeywords(_ offsets: IndexSet) {
        keywords.remove(atOffsets: offsets)
    }
    
}

I tried the following but I run into the following two issues:

  1. The keyboard disappears after one letter typed into any of the TextFields (I assume this is caused by a refresh of the view)
  2. I get an Index out of range error during the deletion process

I could fix #2 with the following update

TextField("", text: Binding<String>(get: {keywords[keywords.firstIndex(of: keyword)!] ?? "<error>"}, set: {keywords[keywords.firstIndex(of: keyword)] = $0}))

But the textfields are still not usable as intended because of the disappearing keyboard issue (#1).

Upvotes: 0

Views: 217

Answers (1)

Titouan
Titouan

Reputation: 581

Why

Each time the user edit a TextField, you modify the content of the keywords variable. Because your text field are created depending on that list, they hence get refreshed. They are all deleted and recreated again so the user can't continue to edit.

Your second problem is due to the fact that your keyword array is edited by the user. You thus need to manage delete entries.

Solution

The solution is to not create your text field directly based on what's inside the list, but based on the length of the list. Replace your for each like so and your first problem is solved :

ForEach(0..<keywords.count, id: \.self) { index in
    TextField("", text: $keywords[index])
}

Then to solve your second problem we have to change the part of the text field which retrieve the content. We make it use proxy binding :

TextField("", text: Binding(
            get: { keywords[index] },
            set: { keywords[index] = $0 }))

Your final code should look like :

ForEach(0..<keywords.count, id: \.self) { index in
    TextField("", text: Binding(
                get: { keywords[index] },
                set: { keywords[index] = $0 }))
}
.onDelete(perform: deleteKeywords)

Upvotes: 2

Related Questions