Reputation: 738
I tried as much as I could before asking the next "Index out of Range" question, because generally I understand why an index out of range issue occurs, but this specific issues makes me crazy:
struct Parent: Identifiable {
let id = UUID()
let name: String
var children: [Child]?
}
struct Child: Identifiable {
let id = UUID()
let name: String
var puppets: [Puppet]?
}
struct Puppet: Identifiable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
@Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)]),
Parent(name: "FooBar", children: nil)
]
}
}
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents.indices, id: \.self) { parentIndex in
NavigationLink (destination: ChildrenView(parentIndex: parentIndex).environmentObject(self.appState)) {
Text(self.appState.parents[parentIndex].name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
@EnvironmentObject var appState: AppState
var parentIndex: Int
var body: some View {
let children = appState.parents[parentIndex].children
return VStack {
List {
if (children?.indices != nil) {
ForEach (children!.indices, id: \.self) { childIndex in
NavigationLink (destination: PuppetsView(parentIndex: self.parentIndex, childIndex: childIndex).environmentObject(self.appState)) {
Text(children![childIndex].name)
}
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var children = self.appState.parents[self.parentIndex].children
if (children != nil) {
children?.append(Child(name: "Teest"))
} else {
children = [Child(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children!.remove(atOffsets: indexSet)
}
}
}
struct PuppetsView: View {
@EnvironmentObject var appState: AppState
var parentIndex: Int
var childIndex: Int
var body: some View {
let child = appState.parents[parentIndex].children?[childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach (child!.puppets!.indices, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
Button(action: {
var puppets = self.appState.parents[self.parentIndex].children![self.childIndex].puppets
if (puppets != nil) {
puppets!.append(Puppet(name: "Teest"))
} else {
puppets = [Puppet(name: "Teest")]
}
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
private func deleteItem(at indexSet: IndexSet) {
if (self.appState.parents[self.parentIndex].children != nil) {
self.appState.parents[self.parentIndex].children![self.childIndex].puppets!.remove(atOffsets: indexSet)
}
}
}
I can remove both children of Foo and FooBar without issues, but when I remove the Puppet of child bar first, then the app crashes like shown in the comments.
I understand that the childIndex doesn't exist anymore, but I don't understand why the view gets built again when there is no child with puppets.
Upvotes: 3
Views: 343
Reputation: 54516
The problem with your optional chaining is that this line produces the result of type Child
and not Child?
:
appState.parents[parentIndex].children?[childIndex]
And if it's not an optional you can't call puppets
on children?[childIndex]
without checking if childIndex
is valid:
// this will crash when childIndex is out of range
appState.parents[parentIndex].children?[childIndex].puppets?.indices
I recommend to use safeIndex
subscript for accessing possible empty elements:
var body: some View {
let child = appState.parents[safeIndex: parentIndex]?.children?[safeIndex: childIndex]
return VStack {
List {
if (child != nil && child!.puppets?.indices != nil) {
ForEach ((appState.parents[parentIndex].children?[childIndex].puppets!.indices)!, id: \.self) { puppetIndex in
Text(self.appState.parents[self.parentIndex].children![self.childIndex].puppets![puppetIndex].name)
}
.onDelete(perform: deleteItem)
}
}
...
}
To do this you would need an Array
extension which allows to access array elements in a safe way (ie. return nil
instead of throwing an error):
extension Array {
public subscript(safeIndex index: Int) -> Element? {
guard index >= 0, index < endIndex else {
return nil
}
return self[index]
}
}
Note: You'd need to do the same for the ParentView
, so in overall Paulw11's answer is cleaner.
Upvotes: 0
Reputation: 114875
All the referencing of array indices looks pretty awful to me. Using array indices also requires that you pass the various objects down to the subviews.
To address this I started by changing your models - Make them classes rather than structs so you can make them @ObservableObject
. They also need to be Hashable
and Equatable
.
I also added add
and remove
functions to the model objects so that you don't need to worry about indices when adding/removing children/puppets. The remove
methods use an array extension that removes an Identifiable
object without needing to know the index.
Finally, I changed the children
and puppets
arrays to be non-optional. There is little semantic difference between a nil
optional and an empty non-optional array, but the latter is much easier to deal with.
class Parent: ObservableObject, Hashable {
static func == (lhs: Parent, rhs: Parent) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
@Published var children: [Child]
init(name: String, children: [Child]? = nil) {
self.name = name
self.children = children ?? []
}
func remove(child: Child) {
self.children.remove(child)
}
func add(child: Child) {
self.children.append(child)
}
}
class Child: ObservableObject, Identifiable, Hashable {
static func == (lhs: Child, rhs: Child) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
@Published var puppets: [Puppet]
init(name: String, puppets:[Puppet]? = nil) {
self.name = name
self.puppets = puppets ?? []
}
func remove(puppet: Puppet) {
self.puppets.remove(puppet)
}
func add(puppet: Puppet) {
self.puppets.append(puppet)
}
}
struct Puppet: Identifiable, Hashable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
@Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)])
]
}
}
extension Array where Element: Identifiable {
mutating func remove(_ object: Element) {
if let index = self.firstIndex(where: { $0.id == object.id}) {
self.remove(at: index)
}
}
}
Having sorted out the model, the views then only need to know about their specific item:
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.parents, id: \.self) { parent in
NavigationLink (destination: ChildrenView(parent: parent)) {
Text(parent.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.appState.parents.append(Parent(name: "Test", children: nil))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Parents"))
}
}
private func deleteItem(at indexSet: IndexSet) {
self.appState.parents.remove(atOffsets: indexSet)
}
}
struct ChildrenView: View {
@ObservedObject var parent: Parent
var body: some View {
VStack {
List {
ForEach (self.parent.children, id: \.self) { child in
NavigationLink (destination: PuppetsView(child:child)) {
Text(child.name)
}
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.parent.add(child: Child(name: "Test"))
}) {
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Children"))
}
private func deleteItem(at indexSet: IndexSet) {
let children = Array(indexSet).map { self.parent.children[$0]}
for child in children {
self.parent.remove(child: child)
}
}
}
struct PuppetsView: View {
@ObservedObject var child: Child
var body: some View {
VStack {
List {
ForEach (child.puppets, id: \.self) { puppet in
Text(puppet.name)
}
.onDelete(perform: deleteItem)
}
Button(action: {
self.child.add(puppet:Puppet(name: "Test"))
})
{
Text("Add")
}
.padding(.bottom, 15)
}
.navigationBarTitle(Text("Puppets"))
}
func deleteItem(at indexSet: IndexSet) {
let puppets = Array(indexSet).map { self.child.puppets[$0] }
for puppet in puppets {
self.child.remove(puppet:puppet)
}
}
}
Upvotes: 2