When using index in a ForEach loop in SwiftUI, you might encounter an "index out of range" error.

I encountered a problem while using ScrollView in SwiftUI. When I perform a refresh, the app crashes. I access the array using an index in a ForEach loop. This is done to create new data from the array in a commonly used view. The function to create data is adopted from a protocol in the view model. I access it by index because the type of the array is not specified; each view using it may have a different data type. Below is an example code. Is it possible to access data from the array using an index? Every time I refresh, I get an "index out of range" error.

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        ScrollView {
            if !viewModel.testValues.isEmpty {
                LazyVStack(spacing: 20) {
                    ForEach(Array(zip(viewModel.testValues.indices, viewModel.testValues)), id:\.1) { index, data in
                        test(index: index, data: data, viewModel: viewModel)
                            .onAppear {
                                if !viewModel.isLoading, viewModel.testValues.count - 2 == index {
                                    viewModel.fetch()
                                }
                            }
                    }
                }
            } else {
                Text("tesetsetsetsettse")
            }
        }
        .onAppear {
            viewModel.fetch()
        }
        .refreshable {
            viewModel.refresh()
        }
    }
}

struct test: View {
    let index: Int
    let data: String
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack(spacing: 8) {
            test1(index: index, data: data, viewModel: viewModel)
            Text("------------------------")
        }
    }
}


struct test1: View {
    let index: Int
    let data: String
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            Text(viewModel.testValues[index])
                .font(.system(size: 12))
                .foregroundStyle(Color.red)
                .padding(.horizontal, 40)
                .padding(.vertical, 50)
                .background {
                    RoundedRectangle(cornerRadius: 20)
                        .fill(Color.blue)
                }
        }
    }
}

class ViewModel: ObservableObject {
    @Published var isLoading = false
    @Published var testValues: [String] = []
    
    func fetch() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.testValues += [
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
                UUID().uuidString,
            ]
        }
    }
    
    func refresh() {
        testValues = []
        fetch()
    }
}

Replies

I believe you need to use

\.self

as the id in the for each loop. That way it will check for changes in the loop range.

The way I do it is by using enumerated and .offset:

                ForEach(Array(data.enumerated()), id: \.offset) { (index, d) in

Why do you zip ?

Try to replace:

                    ForEach(Array(zip(viewModel.testValues.indices, viewModel.testValues)), id:\.1) { index, data in

by

                    ForEach(Array(viewModel.testValues.enumerated()), id:\.offset) { index, data in

@Claude31 , @jlilest Thank you for your interest. I've tried various "id" values, but couldn't resolve the issue. Whether it's through indices, enumerate, or direct indexing, the problem persists when accessing the array. I don't want to remove the index, but should I eventually make the choice to do so?

So, when you try:

 ForEach(Array(viewModel.testValues.enumerated()), id:\.offset) { index, data in

What is the index value that causes crash ?

Could you also add a print and show what you get:

                            .onAppear {
                                print("index", index)  // <<-- ADD THIS
                                if !viewModel.isLoading, viewModel.testValues.count - 2 == index {
                                    viewModel.fetch()
                                    print("fetch. index", index, viewModel.testValues.count)  // <<-- ADD THIS                                }
                            }