How to manually remove an item from List in SwiftUI? - swiftui

I try to remove some item from List but get an error Thread 1: Fatal error: Index out of range. I know about onDelete but on macOS it's not clear how to call it with mouse
#State var wishList = [
"Item 1",
"Item 2"
]
List {
ForEach(wishList.indices) { index in
Button(action: {
}) {
HStack {
Text(self.wishList[index]) //Thread 1: Fatal error: Index out of range.
Button(action: {
RunLoop.main.perform {
self.wishList.remove(at: index)
}
}) {
Image(systemName: "minus.circle").foregroundColor(.red)
}
}
}
}
}
Fix:
I added id:\.self to fix my code.
#State var wishList = [
"Item 1",
"Item 2"
]
List {
ForEach(wishList.indices, id:\.self) { index in
Button(action: {
}) {
HStack {
Text(self.wishList[index]) //Thread 1: Fatal error: Index out of range.
Button(action: {
RunLoop.main.perform {
self.wishList.remove(at: index)
}
}) {
Image(systemName: "minus.circle").foregroundColor(.red)
}
}
}
}
}

The cause is given to you in an error:
count (1) != its initial count (2). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
Instead, use an Identifiable version and operate directly on the content:
List {
ForEach(wishList, id: \.self) { content in
HStack {
Text(verbatim: content)
Button(action: {
guard let index = self.wishList.firstIndex(of: content) else { return }
self.wishList.remove(at: index)
}) {
Image(systemName: "minus.circle").foregroundColor(.red)
}
}
}
}
EDIT: Here's a simpler version:
List(0..<wishList.count, id: \.self) { index in
HStack {
Text(verbatim: self.wishList[index])
Button(action: {
self.wishList.remove(at: index)
}) {
Image(systemName: "minus.circle").foregroundColor(.red)
}
}
}

Related

SwiftUI DataStructure for ForEach.. (how to delete)

I'm working on an app which show images when tapping buttons.
I have a 4 buttons, and Each buttons has a action that add a image.
The problem I have is that when I remove by ontapGesture, The deleting order is not my want.
Ex) button 1 -> button 2 -> button 3 -> button 4 : Now I can see 4 images in vstack, plus, minus, multiply, divide.
But If I tap button 1 now, the plus image still there.
I think the problem arises because of array. What dataStructure should I use instead?
`
import SwiftUI
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
array.append(0)
} label: {
Text("Button 0")
}
Button {
array.append(1)
} label: {
Text("Button 1")
}
Button {
array.append(2)
} label: {
Text("Button 2")
}
Button {
array.append(3)
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[index]
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
`
I found out when I tap button 1 again, it delete the data in array[0].
But there is still array[0], because array always has index.
I understand! but I can not come up with data structure for images.
Using your current setup, you could try this approach, using the content of the array array[index] for the index into systemDictionary, and to limit the set of images to 4 only, use if !array.contains(0) { array.append(0) } as shown in the code:
struct ContentView: View {
#State var array: [Int] = []
let systemDictionary: [Int : Image] = [
0 : Image(systemName: "plus.circle"),
1 : Image(systemName: "minus.circle"),
2 : Image(systemName: "multiply.circle"),
3 : Image(systemName: "divide.circle")
]
var body: some View {
VStack {
HStack {
Button {
if !array.contains(0) { array.append(0) } // <-- here
} label: {
Text("Button 0")
}
Button {
if !array.contains(1) { array.append(1) }
} label: {
Text("Button 1")
}
Button {
if !array.contains(2) { array.append(2) }
} label: {
Text("Button 2")
}
Button {
if !array.contains(3) { array.append(3) }
} label: {
Text("Button 3")
}
}
ForEach(array.indices, id: \.self) { index in
systemDictionary[array[index]] // <-- here
.font(.system(size: 30))
.padding()
.onTapGesture {
array.remove(at: index)
}
}
}
}
}
EDIT-1: using a specific structure for the images:
struct Picture: Identifiable {
let id = UUID()
var image: Image
var label: String
var isVisible: Bool = false
}
struct ContentView: View {
#State var pictures: [Picture] = [
Picture(image: Image(systemName: "plus.circle"), label: "Button 0"),
Picture(image: Image(systemName: "minus.circle"), label: "Button 1"),
Picture(image: Image(systemName: "multiply.circle"), label: "Button 2"),
Picture(image: Image(systemName: "divide.circle"), label: "Button 3")
]
var body: some View {
VStack {
HStack {
ForEach($pictures) { $picture in
Button {
picture.isVisible = true
} label: {
Text(picture.label)
}
}
}
ForEach($pictures) { $picture in
if picture.isVisible {
picture.image
.font(.system(size: 30))
.padding()
.onTapGesture {
picture.isVisible = false
}
}
}
}
}
}

How to pass the int representing the index of the swiped item to the function of the swipeActions button?

I have a ForEach building a list and have a swipeActions running a custom function. I can get the index value of the swiped item and output it with the list, but how do you pass it into the function of the swipeActions button?
Error is on the line in the button calling the cleanUp function - "Cannot convert value of type '(UnsafePointer?, Int32) -..."
import SwiftUI
struct Match: Hashable, Codable, Identifiable {
var homeTeam: String
var id = UUID()}
var savedMatches = [
Match(homeTeam: "discourge")]
struct ContentView: View {
var body: some View {
VStack{
List{
ForEach(Array(zip(savedMatches.indices, savedMatches)), id: \.0) { index, match in
Text("\(index): \(match.homeTeam)") //correctly outputs '0: discourge'
}.swipeActions{
Button(role: .destructive, action: {
cleanUp(item: index) //when '0: discourge' is swiped, would like this to call cleanUp(item: 0)
},
label: {
Label("Clean Up", systemImage: "gear")
})
}
}
}
}
}
func cleanUp (item: Int) {
print("the item swiped has an index of \(item)")
}
Move your .swipeActions{...} to the Text(...), like this:
struct ContentViews: View {
var body: some View {
VStack{
List{
ForEach(Array(zip(savedMatches.indices, savedMatches)), id: \.0) { index, match in
Text("\(index): \(match.homeTeam)")
.swipeActions { // <--- here
Button(role: .destructive, action: {
cleanUp(item: index) //when '0: discourge' is swiped, would like this to call cleanUp(item: 0)
},
label: {
Label("Clean Up", systemImage: "gear")
})
}
}
}
}
}
}

SwiftUI : How I can use confirmationDialog for each row of a list?

import SwiftUI
struct MemoListView: View {
let folder : FolderModel
#State private var showActionSheet : Bool = false
#EnvironmentObject var vm : FolderListViewModel
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
vm.deleteMemo(folder: folder, memo: memo)
}, label: {
Text("Delete")
.foregroundColor(.red)
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Hi!
Please check my code above.
I want to apply actionSheet(confirmationDialog) to each of row in list but I think the list can't recognize which row is selected.
If I tried to delete row3, it just delete only row1
I don't know how I can handle this situation.
Thanks!
As per your comments to the original post, .confirmationDialog only has an isPresented binding available, rather than an object-based one as with sheet, fullScreenCover, etc.
One workaround I've used quite successfully is to move the confirmationDialog, and the boolean state flag that drives it, into a child view. This could be MemoRowView itself, or you could keep that view as the presentation component only and wrap it in an interactive view that called MemoRowView and added the interactive elements, e.g.
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder)
}
.listStyle(.plain)
This does mean injecting quite a lot of domain knowledge into the list item, so it'd need its own #EnvironmentObject reference, etc., but that could be lived with.
Alternatively, you could keep the code that deletes the object in the parent view, and just keep the confirmation in the child view:
struct MemoListItem: View {
var memo: MemoModel
var folder: FolderModel
var onDelete: () -> Void
#State private var showDeleteConfirmation: Bool = false
init(memo: MemoModel, folder: FolderModel, onDelete: #escaping () -> Void) {
self.memo = memo
self.folder = folder
self.onDelete = onDelete
}
var body: some View {
// rest of row setup omitted for brevity
.confirmationDialog(Text("Option"), isPresented: $showDeleteConfirmation) {
Button("Delete", role: .destructive, action: onDelete)
}
}
}
// MemoListView
List {
ForEach(folder.memo) { memo in
MemoListItem(memo: memo, folder: self.folder, onDelete: {
vm.deleteMemo(folder: folder, memo: memo)
})
}
}
An explanation of what I wrote in my comment :
struct MemoListView: View {
#EnvironmentObject var vm : FolderListViewModel
// Updated folder so as it seems it is extracted from your
// view model
var folder : FolderModel {
vm.folder
}
#State private var showActionSheet : Bool = false
// This is the way to know which memo to delete
#State var memoToDelete: Memo?
var body: some View {
ZStack {
if folder.memo.count == 0 {
NoMemoView()
} else {
List {
ForEach(folder.memo) { memo in
MemoRowView(memo: memo, folder: self.folder)
.onLongPressGesture {
// Save the memo on the current row
memoToDelete = memo
self.showActionSheet.toggle()
}
.confirmationDialog(Text("Option"), isPresented: $showActionSheet) {
Button(role : .destructive, action: {
// Delete the saved memo
if let memo = memoToDelete {
vm.deleteMemo(folder: folder, memo: memo)
}
}, label: {
// Here to show the memo id before delete
if let memo = memoToDelete {
Text("Delete \(memo.id)")
.foregroundColor(.red)
}
})
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
}
}
.navigationTitle("Memos in '\(folder.folderName)'")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
NavigationLink(destination: {
NewMemoView(folder: self.folder)
}, label: {
Image(systemName: "plus")
})
NavigationLink(destination: {
}, label: {
Image(systemName: "gear")
})
}
}
}
}
}
Note : I changed from the comment and kept the dialog on each row.
It is not the best way to do it.

SwiftUI error: Generic parameter 'Label' could not be inferred Explicitly specify the generic arguments to fix this issue `

I can't find the issue for this error. I don't think the compiler is giving a meaningful error message.
Why self.book.map {} does not work here?
var body: some View {
List {
ForEach(cards) { card in
if (!self.showMarkedOnly || card.marked) {
ZStack() {
CardView(card: card).frame(maxWidth: 400)
.contextMenu() {
if self.editMode {
Button(action: {}) { HStack { Image(systemName: "square.and.pencil"); Text("Edit") } }
Button(action: {}) { HStack { Image(systemName: "trash"); Text("Delete") } }
// self.book.map { Button(action: { self.onRemoveCard(card, fromBook: $0) }) { HStack { Image(systemName: "folder.badge.minus"); Text("Remove from current Book") } } } // ERROR happens here
if self.book != nil {
Button(action: { self.onRemoveCard(card, fromBook: self.book!) }) { HStack { Image(systemName: "folder.badge.minus"); Text("Remove from current Book") } }
} // <------- this works!!! but I prefer to not use "self.book!"
} else {} }
}
}
}
}
}
This is a case of nesting closures interfering with $0. Try this:
self.book.map { book in Button(action: { self.onRemoveCard(card, fromBook: book) }) { ... }

Why won't my bound [String] not change my multi-select list SwiftUI

I am trying to create a multi-select list:
#Binding var selection:[String]
List {
ForEach(self.items, id: \.self) { item in
MultipleSelectionRow(title: item, isSelected: self.selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item }) <=== NO AFFECT
}
else {
self.selection.append(item). <=== NO AFFECT
}
self.queryCallback()
}
}//ForEach
.listRowBackground(Color("TPDarkGrey"))
}//list
I have a row which is a button that calls the above action
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text(self.title)
Spacer()
if self.isSelected {
Image(systemName: "checkmark")
}
}
.font(.system(size: 14))
}
}
}
Why does it not append or remote the item in the bound array? It seems to change on the second time through the view
I managed to produce an example from your code that works:
I don't know how the rest of your code is setup so I cannot hint you to anything unfortunately.
struct MultipleSelectionRow: View {
var title: String
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text(self.title)
Spacer()
if self.isSelected {
Image(systemName: "checkmark")
}
}
.font(.system(size: 14))
}
}
}
struct ContentView: View {
#State var selection:[String] = []
#State var items:[String] = ["Hello", "my", "friend", "did", "I", "solve", "your", "question", "?"]
var body: some View {
List {
ForEach(self.items, id: \.self) { item in
MultipleSelectionRow(title: item, isSelected: self.selection.contains(item)) {
if self.selection.contains(item) {
self.selection.removeAll(where: { $0 == item })
}
else {
self.selection.append(item)
}
}
}
.listRowBackground(Color("TPDarkGrey"))
}
}
}
I hope this helps to clarify things.