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.