Jared Khan
Jared Khan

Reputation: 344

SwiftUI throwing 'Fatal error: Index out of range' when adding element for app with no explicit indexing

Why, in the following app when clicking through to 'Nice Restaurant' and trying to add a contributor, does the app crash with the error: Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range?

The error, in the Xcode debugger, has no obviously useful stack trace and points straight at the '@main' line.

There are no explicit array indices used in the code nor any uses of members like .first.

I'm using Xcode Version 13.4.1 (13F100) I'm using simulator: iPhone 13 iOS 15.5 (19F70)

import SwiftUI

struct CheckContribution: Identifiable {
    let id: UUID = UUID()
    var name: String = ""
}

struct Check: Identifiable {
    var id: UUID = UUID()
    var title: String
    var contributions: [CheckContribution]
}

let exampleCheck = {
    return Check(
        title: "Nice Restaurant",
        contributions: [
            CheckContribution(name: "Bob"),
            CheckContribution(name: "Alice"),
        ]
    )
}()

struct CheckView: View {
    @Binding var check: Check
    @State private var selectedContributor: CheckContribution.ID? = nil
    
    func addContributor() {
        let newContribution = CheckContribution()
        check.contributions.append(newContribution)
        selectedContributor = newContribution.id
    }
    
    var body: some View {
        List {
            ForEach($check.contributions) { $contribution in
                TextField("Name", text: $contribution.name)
            }
            Button(action: addContributor) {
                Text("Add Contributor")
            }
        }
    }
}


@main
struct CheckSplitterApp: App {
    @State private var checks: [Check] = [exampleCheck]
    var body: some Scene {
        WindowGroup {
            NavigationView {
                List {
                    ForEach($checks) { $check in
                        NavigationLink(destination: {
                            CheckView(check: $check)
                        }) {
                            Text(check.title).font(.headline)
                        }
                    }
                }
            }
        }
    }
}

I've noticed that:

Upvotes: 3

Views: 1400

Answers (3)

Sergei Volkov
Sergei Volkov

Reputation: 1144

I had the same error but with tabview. Moreover, the fall was only on iOS 15, but on iOS 16 it worked perfectly and there were no falls.

I tried both through indexes, and through checking for finding the index inside the range, but nothing helped.

The solution was found in the process of debugging: I noticed that it was falling even before the predstavlenie appeared (it worked Appear). I did a simple check to see if the data array is empty

if !dataArray.isEmpty {
    TabView(selection: $selection) {
        ForEach(dataArray, id: \.self) { item in
            ...
        } 
    }
}

And it worked - there were no more crashes on iOS 15. Apparently there was some problem with the processing of empty arrays before iOS 16.

Upvotes: 2

alobaili
alobaili

Reputation: 904

The cleanest way I could find that actually works is to further separate the nested ForEach into a subview and bind the contributors array to it.

struct CheckView: View {
    @Binding var check: Check
    @State private var selectedContributor: CheckContribution.ID? = nil

    func addContributor() {
        let newContribution = CheckContribution()
        check.contributions.append(newContribution)
        selectedContributor = newContribution.id
    }

    var body: some View {
        List {
            ContributionsView(contributions: $check.contributions)

            Button(action: addContributor) {
                Text("Add Contributor")
            }

            // Test that changing other properties still works.
            Button("Change title", action: changeTitle)
        }
        .navigationTitle(check.title)
    }

    func changeTitle() {
        check.title = "\(Int.random(in: 1...100))"
    }
}

struct ContributionsView: View {
    @Binding var contributions: [CheckContribution]

    var body: some View {
        ForEach($contributions) { $contribution in
            TextField("Name", text: $contribution.name)
        }
    }
}

I'm still not sure about the internals of SwiftUI, and why it works this way. I hope it helps. And maybe another more experienced user can provide a clear explanation to this.

Upvotes: 1

If you really want the Button to be in the List, then you could try this approach using a separate view, works well for me:

struct CheckView: View {
    @Binding var check: Check
    @State private var selectedContributor: CheckContribution.ID? = nil
    
    var body: some View {
        List {
            ForEach($check.contributions) { $contribution in
                TextField("Name", text: $contribution.name)
            }
            AddButtonView(check: $check)  // <-- here
        }
    }
}

struct AddButtonView: View {
    @Binding var check: Check
    
    func addContributor() {
        let newContribution = CheckContribution(name: "new contribution")
        check.contributions.append(newContribution)
    }
    
    var body: some View {
        Button(action: addContributor) {
            Text("Add Contributor")
        }
    }
}

Upvotes: 0

Related Questions