foolbear
foolbear

Reputation: 964

Got "Fatal error: Index out of range" : show index in list item for swiftui

Updated: error: Type '_' has no member '1', if put a if closure(if !self.showMarkedOnly || name.marked {}) inside list-foreach, why?

Code version 4:

struct Name: Identifiable, Hashable {
    var id: String = UUID().uuidString
    var name: String
    var marked: Bool
    init(_ name: String, marked: Bool = false) { self.name = name; self.marked = marked }
}

struct TestView: View {
    @State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3", marked: true), Name("test4"), Name("test5", marked: true), Name("test6"), Name("test7"), Name("test8")]
    @State private var showMarkedOnly = false

    var body: some View {
        VStack{
            Toggle(isOn: $showMarkedOnly) { Text("show marked only") }
            List {
                ForEach(Array(zip(0..., list)), id: \.1.id) { index, name in
//                    if !self.showMarkedOnly || name.marked {
                        HStack {
                            Text("\(index)").foregroundColor(name.marked ? .red : .gray)
                            Spacer()
                            Text("\(name.name)")
                        }
                        .background(Color.gray.opacity(0.001))
                        .onTapGesture {
                            self.list.remove(at: index)
                        }
//                    }
                }
            }
        }
    }
}

=========

Updated: I found the issue of Code version 2, I must provide a id for ForEach. And Code version 2 updated.

I found a graceful way to show index, It avoids self.list[index]. But I found an error "Type '_' has no member '1'" occurred in some complex code.

Code version 3:

var body: some View {
        List {
            ForEach(Array(zip(0..., list)), id: \.1.id) { index, name in // a error occurs in some complex code: "Type '_' has no member '1'"
                HStack {
                    Text("\(index)")
                    Spacer()
                    Text("\(name.name)")
                }
                .background(Color.gray.opacity(0.001))
                .onTapGesture {
                    self.list.remove(at: index)
                }
            }
        }
    }

I show a list, and remove the item I tapped. It's Code version 1, it works fine. When I add index into the list item using indices(Code veriosn 2), got "Fatal error: Index out of range" after tapping.

Code version 1:

struct Name: Identifiable, Hashable {
    var id: String = UUID().uuidString
    var name: String
    init(_ name: String) { self.name = name }
}

struct TestView: View {
    @State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3"), Name("test4"), Name("test5"), Name("test6"), Name("test7"), Name("test8")]

    var body: some View {
        List {
            ForEach(list) { name in
                HStack {
                    Text("\(0)")
                    Spacer()
                    Text("\(name.name)")
                }
                .background(Color.gray.opacity(0.001))
                .onTapGesture {
                    self.list = self.list.filter { $0 != name }
                }
            }
        }
    }
}

Code version 2:

struct TestView: View {
    @State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3"), Name("test4"), Name("test5"), Name("test6"), Name("test7"), Name("test8")]

    var body: some View {
        List {
            //ForEach(list.indices) { index in
            ForEach(list.indices, id: \.self) { index in
                HStack {
                    Text("\(index)")
                    Spacer()
                    Text("\(self.list[index].name)")
                }
                .background(Color.gray.opacity(0.001))
                .onTapGesture {
                    self.list.remove(at: index)
                }
            }
        }
    }
}

Upvotes: 2

Views: 5255

Answers (3)

user3441734
user3441734

Reputation: 17544

@State is property wrapper, which will force the View in which it is defined to recalculate its body.

In your case, if you remove the item at index,

HStack {
    Text("\(index)")
    Spacer()
    Text("\(self.list[index].name)")
}
.background(Color.gray.opacity(0.001))
.onTapGesture {
     self.list.remove(at: index)
 }

the Text inside HStack

Text("\(self.list[index].name)")

crash, just because list[index] doesn't exist any more.

Using

ForEach(list.indices, id:\.self) { index in ... }

instead of

ForEach(list.indices) { index in ... }

will force SwiftUI to recreate TestView (see the id:\.self in ForEach constructor)

SwiftUI will make fresh copy of TestView while using fresh value of property wrapped in @State property wrapper.

UPDATE

Please, don't update your question ...

Your last code version 4 is total mess, so I rewrote it to something you able to copy - paste - run

import SwiftUI

struct Name: Identifiable, Hashable {
    var id: String = UUID().uuidString
    var name: String
    var marked: Bool
    init(_ name: String, marked: Bool = false) { self.name = name; self.marked = marked }
}

struct ContentView: View {
    @State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3", marked: true), Name("test4"), Name("test5", marked: true), Name("test6"), Name("test7"), Name("test8")]
    @State private var showMarkedOnly = false

    var body: some View {
        VStack{
            Toggle(isOn: $showMarkedOnly) {
                Text("show marked only")
            }.padding(.horizontal)
            List {
                ForEach(Array(zip(0..., list)).filter({!self.showMarkedOnly || $0.1.marked}), id: \.1.id) { index, name in
                    HStack {
                        Text("\(index)").foregroundColor(name.marked ? .red : .gray)
                        Spacer()
                        Text("\(name.name)")
                    }
                    .background(Color.gray.opacity(0.001))
                    .onTapGesture {
                        self.list.remove(at: index)
                    }
                }
            }
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

it should looks like enter image description here

UPDATE based on discussion

ForEach different versions of constructors use internally different functionality of ViewBuilder

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view
    /// that is visible only when the `if` condition evaluates `true`.
    public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View

    /// Provides support for "if" statements in multi-statement closures, producing
    /// ConditionalContent for the "then" branch.
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View

    /// Provides support for "if-else" statements in multi-statement closures, producing
    /// ConditionalContent for the "else" branch.
    public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}

This is about "implementation details" and hopefully it will be documented in next release. SwiftUI is still in very early stage of development, we have to be careful.

Lets try to force SwiftUI to follow our own way! First separate RowView

struct RowView: View {
    var showMarkedOnly: Bool
    var index: Int
    var name: Name
    //@ViewBuilder
    var body: some View {
        if !self.showMarkedOnly || name.marked {
            HStack {
                Text(verbatim: index.description).foregroundColor(name.marked ? .red : .gray)
                Spacer()
                Text(verbatim: name.name)
            }
            .background(Color.gray.opacity(0.001))

        }
    }
}

Compiler complains with

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type

Uncomment the line to wrap our body

struct RowView: View {
    var showMarkedOnly: Bool
    var index: Int
    var name: Name
    @ViewBuilder
    var body: some View {
        if !self.showMarkedOnly || name.marked {
            HStack {
                Text(verbatim: index.description).foregroundColor(name.marked ? .red : .gray)
                Spacer()
                Text(verbatim: name.name)
            }
            .background(Color.gray.opacity(0.001))

        }
    }
}

Now we can use the code the way you like :-)

struct ContentView: View {
    @State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3", marked: true), Name("test4"), Name("test5", marked: true), Name("test6"), Name("test7"), Name("test8")]
    @State private var showMarkedOnly = false

    var body: some View {
        VStack{
            Toggle(isOn: $showMarkedOnly) {
                Text("show marked only")
            }.padding(.horizontal)
            List {
                ForEach(Array(zip(0..., list)), id: \.1.id) { (index, name) in
                    RowView(showMarkedOnly: self.showMarkedOnly, index: index, name: name).onTapGesture {
                    self.list.remove(at: index)
                }
                }
            }
        }
    }
}

The final result uses now buildIf<Content> construct and all code works again (the result looks exactly the same as shown above)

Upvotes: 9

Procrastin8
Procrastin8

Reputation: 4503

It's because indices is not your state, the array is. So you update the array but @State isn't actually smart enough to update computed or downstream properties of the value it wraps. If you want the index, make that the source of identifiers in your ForEach:

ForEach(0..<list.count, id: \.self) {
    // the rest should be ok here
}

Upvotes: 3

Aleksey Potapov
Aleksey Potapov

Reputation: 3783

That is because SwiftUI reloads its content basing on original data provided. You could solve it by modifying your

ForEach(list) { item in
    HStack {
        Text("\(item.id)")
        Spacer()
        Text("\(item.name)")
    }
    .onTapGesture {
        guard let itemIndex = self.list.firstIndex(of: item) else { return }
        self.list.remove(at:itemIndex)
    }
}

Otherwise, you could solve your issue using built-in .onDelete() method (you delete items by swiping them):

var body: some View {
    List {
        ForEach(list) { item in
            HStack {
                Text("\(item.id)")
                Spacer()
                Text("\(item.name)")
            }
            .background(Color.gray.opacity(0.001))
        }.onDelete(perform: delete)
    }
}

func delete(at offsets: IndexSet) {
    list.remove(atOffsets: offsets)
}

Upvotes: 0

Related Questions