I'm trying (and failing!) to implement an expandable list of questions and answers in SwiftUI.
struct FAQ: Equatable, Identifiable {
static func ==(lhs: FAQ, rhs: FAQ) -> Bool {
return lhs.id == rhs.id
}
let id = UUID()
let question: String
let answers: [String]
var isExpanded: Bool = false
}
struct ContentView: View {
@State private (set) var faqs: [FAQ] = [
FAQ(question: "What is the fastest animal on land?", answers: ["The cheetah"]),
FAQ(question: "What colours are in a rainbox?", answers: ["Red", "Orange", "Yellow", "Blue", "Indigo", "Violet"])
]
var body: some View {
List {
ForEach(faqs) { faq in
Section(header: Text(faq.question)
.onTapGesture {
if let index = self.faqs.firstIndex(of: faq) {
self.faqs[index].isExpanded.toggle()
}
}
) {
if faq.isExpanded {
ForEach(faq.answers, id: \.self) {
Text("• " + $0).font(.body)
}
}
}
}
}
}
}
Tapping any question successfully expands the answers into view, but tapping the same header again doesn't contract the answers, nor does tapping a second question expand those answers.

With some judiciously placed prints, I can see that isExpanded is toggled to true on the first question tapped, but then won't toggle back to false.
Could someone explain what I'm doing wrong?
The issue is with your @State var faq: [FAQ] line. The @State property wrapper allows your view to watch for changes in your array, but changing a property of one of the array's elements does not count as a change in the array.
Instead of using a state variable, you probably want to create a small view model, like so:
class ViewModel: ObservableObject {
@Published var faqs: [FAQ] = [
FAQ(question: "What is the fastest animal on land?", answers: ["The cheetah"]),
FAQ(question: "What colours are in a rainbox?", answers: ["Red", "Orange", "Yellow", "Blue", "Indigo", "Violet"])
]
}
and update your ContentView:
struct ContentView: View {
@ObservedObject var model = ViewModel()
var body: some View {
List {
ForEach(model.faqs.indices) { index in
Section(header: Text(self.model.faqs[index].question)
.onTapGesture {
self.model.faqs[index].isExpanded.toggle()
}
) {
if self.model.faqs[index].isExpanded {
ForEach(self.model.faqs[index].answers, id: \.self) {
Text("• " + $0).font(.body)
}
}
}
}
}
}
}
The @ObservedObject property wrapper in your ContentView now watches for any changes its ViewModel object announces, and the @Published wrapper tells the ViewModel class to announce anything that happens to the array, including changes to its elements.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With