strangetimes
strangetimes

Reputation: 5183

Binding in SwiftUI fails to work inside of a loop

For my understanding, I've written the following code which expands / collapses a section inside of a list.

struct WORKING_CollapsableListView: View {
  @State var sectionExpansionStates = [true, true, true]

  var body: some View {
    VStack {
      List {
          Section(header: CollapsableSectionHeader(expansionState: self.$sectionExpansionStates[0])) {
            if self.sectionExpansionStates[0] {
              ForEach(0..<10) { item in
                Text("\(item) is \(self.sectionExpansionStates[0] ? "Expanded" : "Collapsed")")
                  .frame(height: self.sectionExpansionStates[0] ? 10 : 10)
              }
            }
          }

        Section(header: CollapsableSectionHeader(expansionState: self.$sectionExpansionStates[1])) {
          if self.sectionExpansionStates[1] {
            ForEach(0..<10) { item in
              Text("\(item) is \(self.sectionExpansionStates[1] ? "Expanded" : "Collapsed")")
                .frame(height: self.sectionExpansionStates[1] ? 10 : 10)
            }
          }
        }
        Section(header: CollapsableSectionHeader(expansionState: self.$sectionExpansionStates[2])) {
          if self.sectionExpansionStates[2] {
            ForEach(0..<10) { item in
              Text("\(item) is \(self.sectionExpansionStates[2] ? "Expanded" : "Collapsed")")
                .frame(height: self.sectionExpansionStates[2] ? 10 : 10)
            }
          }
        }
      }
    }
  }
}

struct CollapsableSectionHeader: View {
  @Binding var expansionState: Bool

  var body: some View {
    Button(action: {
      self.expansionState.toggle()
    }) {
      Text("HEADER: \(expansionState ? "Expanded" : "Collapsed")")
        .bold()
    }
  }
}

This works as expected. However the following code does NOT work. All I've done is replaced the multiple sections with a ForEach. This code should be identical in its behavior, but nothing happens when I tap on the section headers. What am I missing? It's as though the binding isn't working.

struct NOT_WORKING_CollapsableListView: View {
  @State var sectionExpansionStates = [true, true, true]

  var body: some View {
    VStack {
      List {
        ForEach(0 ..< 3) { section in
          Section(header: CollapsableSectionHeader(expansionState: self.$sectionExpansionStates[section])) {
            if self.sectionExpansionStates[section] {
              ForEach(0..<10) { item in
                Text("\(item) is \(self.sectionExpansionStates[section] ? "Expanded" : "Collapsed")")
                  .frame(height: self.sectionExpansionStates[section] ? 10 : 10)
              }
            }
          }
        }
      }
    }
  }
}

Upvotes: 1

Views: 130

Answers (1)

Asperi
Asperi

Reputation: 257493

It is due to statically_ranged_ForEach... as I experienced here on SO it is most confused concept in SwiftUI.. anyway - the solution is to use dynamic container of explicit models for sections.

Here is simplified working demo of your code (but the idea should be easily adoptable to your not provided components).

Tested with Xcode 11.4 / iOS 13.4

demo

// simple demo model for sections
struct SectionModel: Identifiable {
    let id: Int
    var expanded = true
}

struct TestCollapsableListView: View {
  // dynamic container with model, state is triggered
  @State var sections = [SectionModel(id: 0), SectionModel(id: 1), SectionModel(id: 2)]

  var body: some View {
    VStack {
      List {
        ForEach(sections) { section in
          Section(header: Button("Section \(section.id)") { self.sections[section.id].expanded.toggle() }) {
            if section.expanded {
              ForEach(0..<10) { item in
                Text("\(item) is \(section.expanded ? "Expanded" : "Collapsed")")
                  .frame(height: section.expanded ? 10 : 10)
              }
            }
          }
        }
      }
    }
  }
}

Upvotes: 2

Related Questions