Reputation: 964
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
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()
}
}
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
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
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