skip to Main Content

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)

3

Answers


  1. 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.

    Login or Signup to reply.
  2. 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")
            }
        }
    }
    
    Login or Signup to reply.
  3. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search