Home > Blockchain >  SwiftUI throwing 'Fatal error: Index out of range' when adding element for app with no exp
SwiftUI throwing 'Fatal error: Index out of range' when adding element for app with no exp

Time:07-11

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:

  • If I unroll the ForEach($checks) the crash doesn't occur (but I need to keep the ForEach so I can list all the checks)
  • If I don't take a binding to the CheckContribution (ForEach($check.contributions) { $contribution in then the crash doesn't occur (but I need the binding so subviews can modify the CheckContribution
  • If I don't set the selectedContributor then the crash doesn't occur (but I need the selectedContributor in the real app for navigation purposes)

CodePudding user response:

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")
        }
    }
}

CodePudding user response:

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.

  • Related