SwiftUI – Add a row of multiple Textfields/Views on button click

I am using SwiftUI to build a feature where a user can click on a button to create rows and each row has multiple views (Textfields, Radio buttons…) [see image below]
I created a swift class DynamicSkills where I handle the creation of the items and the "Add" button, but when I click "Add" the App crashes completely, below is my code so far and an image showing how it should be, I just started with SwiftUI and I am not fully grasping the concept of Binding<>.

What should I do exactly to get it up and running?

enter image description here

//DynamicSkills.swift
struct SkillListEditor: View {

    @Binding var evalList: [String]
    @Binding var skillList: [String]

    func getBindingS(forIndex index: Int) -> Binding<String> {
        return Binding<String>(get: { skillList[index] },
                           set: { skillList[index] = $0 })
    }

    func getBindingE(forIndex index: Int) -> Binding<String> {
        return Binding<String>(get: { evalList[index] },
                           set: { evalList[index] = $0 })
    }


    var body: some View {
        ForEach(0..<skillList.count, id: \.self) { index in
            ListItem(skillEvaluation: getBindingE(forIndex: index), text: getBindingS(forIndex: index)) { self.skillList.remove(at: index) }
        }
        AddButton{ self.skillList.append("") }
    }
}

fileprivate struct ListItem: View {

    @Binding var skillEvaluation: String
    @Binding var text: String
    var removeAction: () -> Void

    var body: some View {
        VStack(alignment: .leading){
            Spacer().frame(height: 8)
            HStack{
                Button(action: removeAction) {
                    Image(systemName: "minus.circle.fill")
                        .foregroundColor(.red)
                        .padding(.horizontal)
                }
                TextField("Skill", text: $text)
                    .padding(.all)
                    .background(Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
                    .cornerRadius(6)
            }
            HStack{
                Text("Evaluation").font(.callout)
                Spacer()
            }
            HStack{
                RadioButtonField(id: "1", label: "1", isMarked: $skillEvaluation.wrappedValue == "1" ? true : false) { selected in
                    self.skillEvaluation = selected
                }
                Spacer()
                RadioButtonField(id: "2", label: "2", isMarked: $skillEvaluation.wrappedValue == "2" ? true : false) { selected in
                    self.skillEvaluation = selected
                }
                Spacer()
                RadioButtonField(id: "3", label: "3", isMarked: $skillEvaluation.wrappedValue == "3" ? true : false) { selected in
                    self.skillEvaluation = selected
                }
                Spacer()
                RadioButtonField(id: "4", label: "4", isMarked: $skillEvaluation.wrappedValue == "4" ? true : false) { selected in
                    self.skillEvaluation = selected
                }
                Spacer()
                RadioButtonField(id: "5", label: "5", isMarked: $skillEvaluation.wrappedValue == "5" ? true : false) { selected in
                    self.skillEvaluation = selected
                }
            }
        }
    }
}

fileprivate struct AddButton: View {

    var addAction: () -> Void

    var body: some View {
        HStack{
            Spacer()
            Button(action: addAction) {
                    Text("add skill").padding(.horizontal, 12).padding(.vertical, 6).background(.white).foregroundColor(Color("PassioGreen")).overlay(RoundedRectangle(cornerRadius: 25).stroke(Color("PassioGreen"), lineWidth: 2))
                }
        }.padding(.top, 14)
    }
} 

And in my main screen in ContentView I am showing it this way:

//ContentView.swift

@State var skills = [String]()
@State var skillsEvals = [String]()

struct ContentView: View {
    NavigationView{
        VStack{
            HStack{
            Text("Skills").font(.headline)
            Spacer()
        }
        HStack{
            Text("Add up to 5 skills").font(.callout)
            Spacer()
        }
        SkillListEditor(evalList: $skillsEvals, skillList: $skills)
        }
    }
}

Update: this is the custom radio button class CustomRadio.swift

//Custom Radio button used:
struct RadioButtonField: View {
    let id: String
    let label: String
    let isMarked: Bool
    let callback: (String)->()

    init(
        id: String,
        label:String,
        isMarked: Bool = false,
        callback: @escaping (String)->()
    ) {
        self.id = id
        self.label = label
        self.isMarked = isMarked
        self.callback = callback
    }

    var body: some View {
        Button(action:{
            self.callback(self.id)
        }) {
            VStack(alignment: .center) {
                Text(label).foregroundColor(self.isMarked ? .white : .black).bold().padding(.horizontal, 8).padding(.vertical, 8)
            }
        }
        .padding()
        .background(self.isMarked ? Color("PassioGreen") : Color(red: 239.0/255.0, green: 243.0/255.0, blue: 244.0/255.0, opacity: 1.0))
        .cornerRadius(10)
    }
}

>Solution :

Your AddButton action appends an item to skillList, but does not append an item to evalList. So when ListItem uses its skillEvaluation binding, the binding’s getter tries to access an element of evalList that doesn’t exist.

Try this:

AddButton {
    self.evalList.append("")
    self.skillList.append("")
}

Leave a Reply