2

I cant seem to understand why this can throw an error? The first for each loop is working perfectly, 2nd is throwing an error. I need to use index of the array to get custom colors for my code. Otherwise i would go with normal ForEach.

    @ViewBuilder
    private func chartLegendView() -> some View {
        if !viewModel.topTransactions.isEmpty {
            VStack {
                ForEach(viewModel.topTransactions, id: \.self) { transaction in
                    Text(transaction.description)
                }
                ForEach(viewModel.topTransactions.indices, id: \.self) { index in
                    Text(viewModel.topTransactions[index].description)
                }
            }
        }
    }

enter image description here

I have tried all the logical options, but nothing works.

UPDATE 1: This is what I am trying to achieve. Im getting an array of transactions from SwiftData. I am creating a custom object from that array of transactions (grouping by category and some other calculations) and displaying them in a SwiftUI Chart. What I have is that my Chart legend and Chart colors are custom.

I have predefined 10 colors to be used on the chart in an array. That's why I use an index to grab a color from each item In the chart.

My user can choose "Income, Expense, or date" to filter the data coming from SwiftData and regenerate the UI. I do agree that the problem looks like a model/fetching issue.

this is how I have "solved" the problem, but its wrong. I still have crashes when filters change.

@ViewBuilder
private func chartLegendView() -> some View {
    VStack {
        ForEach(Array(viewModel.topTransactions.prefix(analyticsTransactionsNumber).enumerated()), id: \.element) { index, transaction in
            TopExpenseRow(topExpense: transaction, color: categoryColors[index])

        }
    }
}

struct TopTransaction: Comparable, Hashable, Identifiable {
let id = UUID()
let total: Double
let percentOfTotal: Double
let category: Category?

var description: String {
    "\(name) \(percentOfTotal.noDecimalString)%"
}

var name: String {
    category?.name ?? "Unknown"
}

static func < (lhs: TopTransaction, rhs: TopTransaction) -> Bool {
    return lhs.total > rhs.total
}

func hash(into hasher: inout Hasher) {
    hasher.combine(id)
    hasher.combine(name)
    hasher.combine(total)
    hasher.combine(percentOfTotal)
}

}

enter image description here

3
  • Index is unsafe in a ForEach you shouldn’t use it. But enumerated seems a bit more stable Commented Nov 12, 2023 at 11:03
  • stackoverflow.com/questions/72222014/… Commented Nov 12, 2023 at 11:05
  • Don't use index especially with an unrelated array there is no way to tell SwiftUI there is a change prepare your data before you get to the ForEach Commented Nov 13, 2023 at 22:48

2 Answers 2

1

Your @ViewBuilder function looks fine, the issue is in your data model. I can't assume the solution without looking into your model structure but here is a sample of forEach similar to yours that works with and without indices.

struct Thing: Hashable {
  var name: String
  init(_ name: String) {
    self.name = name
  }
}

struct Test: View {
  @State var data = [Thing("A"), Thing("B"), Thing("C"), Thing("D"), Thing("E"), Thing("F")]
  @ViewBuilder
  private func chartLegendView() -> some View {
    VStack {
      ForEach(data, id: \.self) { thing in
        Text(thing.name)
      }
      ForEach(data.indices, id: \.self) { index in
        Text(data[index].name)
      }
    }
  }
  var body: some View {
    chartLegendView()
  }
}

enter image description here

Unless the data source is static it's best not to use index as it holds strong a reference to the collection.

enter image description here

Sign up to request clarification or add additional context in comments.

4 Comments

Index is unsafe “Demystify SwiftUI” attributes this error to that
That's true, unless data source is static it's best not to use index.
And in your case since the array is in a State it isn’t static
I have updated the question with extra info. I do agree that the issue is more of a model thing. I am guessing the best option would be adding the color directly to my custom TopTransaction object and use normal ForEachLoop
-2

Lots of mistakes:

  1. Remove @ViewBuilder, that is what View and body is for, so make a new custom ChartLegendView.
  2. Remove if, this generates a _ConditionalView which is unnecessary in your code.
  3. Remove id: \.self, you need real ids with ForEach, or better to make your model struct conform to identifiable.
  4. Don't use indices or accessing an array by index inside a ForEach's closure. Indices are not unique across changes, e.g. a transaction at index 0 will get id 0 which is the same as any other transaction at index 0 so it couldn't detect the deletion of a transaction at index 0 when there are multiple transactions, you need to use real ids.

So it should look like this:

struct ChartLegendView: View {

    let transactions: [Transaction]
    var body: View {    
        ForEach(transactions) { transaction in
             Text(transaction.description)
        }
    }
}

struct Transaction: Identifiable {
    var id: String {
        return a unique combination of vars, e.g. user + transactionID
    }
...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.