3

I've seen several posts about this, but so far none of the solutions seem to be working for me.

I'm trying to create an array of Identifiable items using ForEach -- with both a Text() and Toggle() view inside. The array is stored in a @Published property of an @ObservableObject.

I'm currently looping through the indices to create the toggle bindings (as suggested in other posts).

Everything appears to be working, until I try to delete a row.

(Specifically the last row - which triggers a "Fatal error: Index out of range" every time.)

Any help would be greatly appreciated!

struct Rule: Identifiable {
  let id: String
  var displayName: String
  var isEnabled: Bool
}

class UserData: ObservableObject {
  @Published var rules: [Rule] = []
}

struct RuleListView: View {
  @ObservableObject var userData: UserData

  var body: some View {
    List {
      ForEach(userData.rules.indices, id: \.self) { index in
        HStack {
          Toggle(
            isOn: self.$userData.rules[index].isEnabled
          ) { Text("Enabled") }
          Text(self.userData.rules[index].displayName)
        }
      }
      .onDelete(perform: delete)
    }
  }

  func delete(at offsets: IndexSet) {
    userData.rules.remove(atOffsets: offsets)
  }
}

bmtul
  • 33
  • 5
  • https://medium.com/better-programming/ios-13-be-dynamic-with-diffabledatasource-56ed938a0325 – Adrian Dec 14 '19 at 14:21

1 Answers1

3

It seems you have complicated your code:

class UserData: ObservableObject {
  @Published var rules: [Rule] = []
}

Will notice when new element is added to rules array, you could have done that just by declaring:

@State var rules = [Rule]()

You probably want to know when isEnabled in Rule class changes. Right now it is not happening. For that to ObservableObject must be the Rule class.

Keeping that in mind, if you change your code to:

import SwiftUI

class Rule: ObservableObject, Identifiable {
  let id: String
  var displayName: String
  @Published var isEnabled: Bool

  init(id: String, displayName: String, isEnabled: Bool) {
    self.id = id
    self.displayName = displayName
    self.isEnabled = isEnabled
  }
}

struct ContentView: View {
  // for demonstration purpose, you may just declare an empty array here
  @State var rules: [Rule] = [
    Rule(id: "0", displayName: "a", isEnabled: true),
    Rule(id: "1", displayName: "b", isEnabled: true),
    Rule(id: "2", displayName: "c", isEnabled: true)
  ]

  var body: some View {
    VStack {
      List {
        ForEach(rules) { rule in
          Row(rule: rule)
        }
        .onDelete(perform: delete)
      }
    }
  }

  func delete(at offsets: IndexSet) {
    rules.remove(atOffsets: offsets)
  }
}

struct Row: View {
  @ObservedObject var rule: Rule

  var body: some View {
    HStack {
      Toggle(isOn: self.$rule.isEnabled)
      { Text("Enabled") }

      Text(rule.displayName)
        .foregroundColor(rule.isEnabled ? Color.green : Color.red)
    }
  }
}

It will notice when new element is added to rules array, and also will notice when isEnabled changes. This also solves your problem with crashing.

Nalov
  • 578
  • 5
  • 9