ewang
ewang

Reputation: 506

Create an arbitrary number of state variables in SwiftUI

I'm trying to create a survey using SwiftUI, where the survey can have an arbitrary number of questions.

enter image description here

I'm trying to capture what values the user inputted through state variables such as:

@State var answer: String = ""

ForEach(survey) { surveyQuestion in
  Text(surveyQuestion.question)

  TextField(surveyQuestion.placeholder, text: $answer)
}

However, since I don't know how many questions are going to be in the survey beforehand, I don't know how many of these state variables to store the answers to make. I could create the variables on the fly inside the ForEach loop but then the variables would be out of scope when I actually go to submit the survey (since the submitting would happen outside of the ForEach loop).

How do I create an arbitrary number of state variables to capture the user's answers to the survey?

EDIT: I had tried making my answers variable a dictionary, where the keys are the IDs to the questions. My code looked like:

@State var answers: [String:String] = [:]

ForEach(survey) { surveyQuestion in
  Text(surveyQuestion.question)

  TextField(surveyQuestion.placeholder, text: $answers[surveyQuestion.id!])
}

However, I kept getting the error:

Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding<String>'

So then I tried replacing $answers[surveyQuestion.id!] with $(answers[surveyQuestion.id!]!) but then the system gets confused and responds with:

'$' is not an identifier; use backticks to escape it

I had also tried adjusting my question model so that there's a field for an answer in the same struct. My code looked like this:

TextField(surveyQuestion.placeholder, text: $surveyQuestion.answer)

I kept getting the error:

Cannot find '$surveyQuestion' in scope

Upvotes: 2

Views: 684

Answers (2)

jnpdx
jnpdx

Reputation: 52565

Using the strategy in the edit that you added, with the Dictionary, you could provide a custom Binding, like this:

func bindingForID(id: String) -> Binding<String> {
    .init {
        answers[id] ?? ""
    } set: { newValue in
        answers[id] = newValue
    }
}

And you could use it like this:

TextField(surveyQuestion.placeholder, text: bindingForID(id: surveyQuestion.id))

In terms of adding this data to Firestore, you could trigger Firestore updates in the set closure from the custom binding. However, I'd probably recommend moving this logic to a @Published property on a view model (ObservableObject) where you could use Combine to do things like Debouncing before you send the data to Firestore (probably a little beyond the scope of this question).

Upvotes: 1

hayesk
hayesk

Reputation: 594

Create a struct that holds id, question, and answer. Your @State var should be an array of those.

struct QA: Identifiable {
id: String
question: String
answer: String
}

…

@State private var myQAs: [QA] = myQAs() // or populate them in init, onAppear(if asynchronous) or however you see fit

…
ForEach(myQAs) { qa in
  Text(qa.question)

  TextField(placeholder, text: $qa.answer)
}

Upvotes: 0

Related Questions