Search code examples
listswiftuiindices

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

Solution

  • @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)