更新和删除列表中的项目,SwiftUI
Updating and deleting items in List, SwiftUI
在待办事项样式列表中,我在更新列表时遇到了 2 个问题。
删除列表中的项目时,它会飞到随机的地方。 (故障)。我想这与使用独特的identifier
有关
勾选列表中的项目时,不会更改部分。视图仅在添加新项目后更新。
- 此外,也许您知道如何在没有更多项目时隐藏部分,并在添加任务时重新出现。
ListView
struct ContentView: View {
@ObservedObject var listViewModel = ListViewModel()
@State var newItem = ""
var body: some View {
VStack {
Form {
// To-do section
Section(header: Text("New")) {
ForEach(listViewModel.itemCellViewModels) { itemCellViewModel in
if itemCellViewModel.item.accomplished == false {
ItemCellView(itemCellViewModel: itemCellViewModel)
}
}.onDelete(perform: { indexSet in
//remove item from the shopping list
self.listViewModel.itemCellViewModels.remove(atOffsets: indexSet)
print(indexSet)
})
.onDelete(perform: listViewModel.removeRows)
}
// Accomplished section
Section(header: Text("Done")) {
ForEach(listViewModel.itemCellViewModels.indices, id: \.self) { index in
if listViewModel.itemCellViewModels[index].item.accomplished == true {
ItemCellView(itemCellViewModel: listViewModel.itemCellViewModels[index])
}
}.onDelete(perform: listViewModel.removeRows)
}
}
TextField("Enter item here", text: $newItem) { _ in
} onCommit: {
self.listViewModel.addItem(item: Item(productName: newItem, accomplished: false))
newItem = ""
}
.autocapitalization(.none)
.padding()
.border(Color.blue)
.padding()
}
// update ViewModel with placeholder data
.onAppear(perform: {self.listViewModel.itemCellViewModels.append(contentsOf: placeholderItems)})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
var placeholderItems = [
ItemViewModel(item: Item(productName: "1", accomplished: false)),
ItemViewModel(item: Item(productName: "2", accomplished: false)),
ItemViewModel(item: Item(productName: "3", accomplished: false)),
ItemViewModel(item: Item(productName: "4", accomplished: false)),
ItemViewModel(item: Item(productName: "5", accomplished: false)),
ItemViewModel(item: Item(productName: "6", accomplished: true)),
ItemViewModel(item: Item(productName: "7", accomplished: true)),
ItemViewModel(item: Item(productName: "8", accomplished: true)),
ItemViewModel(item: Item(productName: "9", accomplished: true)),
ItemViewModel(item: Item(productName: "10", accomplished: true))
]
/// The rest of the code
// Reusable View for Cells
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
import Foundation
import Combine
class ItemViewModel: ObservableObject, Identifiable {
@Published var item: Item
var id = UUID()
@Published var items: [Item] = []
@Published var completionStateIconName = ""
private var cancellables = Set<AnyCancellable>()
init(item: Item) {
self.item = item
$item
.map { item in
item.accomplished ? "checkmark.square" : "square"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables) // <- for memory management purposes
}
}
class ListViewModel: ObservableObject {
@Published var itemCellViewModels = [ItemViewModel]()
private var cancellables = Set<AnyCancellable>()
func addItem(item: Item) {
let itemVM = ItemViewModel(item: item)
self.itemCellViewModels.append(itemVM)
}
func removeRows(at offsets: IndexSet) {
itemCellViewModels.remove(atOffsets: offsets)
}
}
// Model
struct Item: Identifiable {
var id = UUID()
var productName: String
var accomplished: Bool
}
单元格的可重用视图
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
ItemViewModel
import Foundation
import Combine
class ItemViewModel: ObservableObject, Identifiable {
@Published var item: Item
var id = UUID()
@Published var items: [Item] = []
@Published var completionStateIconName = ""
private var cancellables = Set<AnyCancellable>()
init(item: Item) {
self.item = item
$item
.map { item in
item.accomplished ? "checkmark.square" : "square"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables) // <- for memory management purposes
}
}
ListViewModel
import Foundation
import Combine
class ListViewModel: ObservableObject {
@Published var itemCellViewModels = [ItemViewModel]()
private var cancellables = Set<AnyCancellable>()
func addItem(item: Item) {
let itemVM = ItemViewModel(item: item)
self.itemCellViewModels.append(itemVM)
}
func removeRows(at offsets: IndexSet) {
itemCellViewModels.remove(atOffsets: offsets)
}
}
型号
struct Item: Identifiable {
var id = UUID()
var productName: String
var accomplished: Bool
}
首先,你加错了ForEach
。
因为您只需要添加那些已完成或未完成的对象。您需要使用过滤器而不是迭代所有对象并检查 ForEach 内部的条件。通过删除它,您的第一个问题解决了动画,您还可以添加一个空条件,这样当您完成的数组为空时,该部分会自动将其从视图中删除。
作为下面的演示,我们为两个部分使用了不同的数组,因此部分行的索引将发生变化,并且您的数据源是单个数组。为此,您需要通过 id 从数组中删除项目。
struct ContentView: View {
@ObservedObject var listViewModel = ListViewModel()
var arrNotAccomplished: [ItemViewModel] {
return listViewModel.itemCellViewModels.filter({![=10=].item.accomplished})
}
var arrAccomplished: [ItemViewModel] {
return listViewModel.itemCellViewModels.filter({[=10=].item.accomplished})
}
@State var newItem = ""
var body: some View {
VStack {
Form {
// To-do section
if !arrNotAccomplished.isEmpty {
Section(header: Text("New")) {
ForEach(arrNotAccomplished) { itemCellViewModel in
ItemCellView(itemCellViewModel: itemCellViewModel) { (_) -> (Void) in
self.listViewModel.objectWillChange.send()
}
}.onDelete { (index) in
guard let firstIndex = index.first else { return }
self.listViewModel.removeRows(for: arrNotAccomplished[firstIndex].id)
}
}
}
// Accomplished section
if !arrAccomplished.isEmpty {
Section(header: Text("Done")) {
ForEach(arrAccomplished) { itemCellViewModel in
ItemCellView(itemCellViewModel: itemCellViewModel) { (_) -> (Void) in
self.listViewModel.objectWillChange.send()
}
}.onDelete { (index) in
guard let firstIndex = index.first else { return }
self.listViewModel.removeRows(for: arrAccomplished[firstIndex].id)
}
}
}
}
// ====== other body view code =========
更新删除功能代码。
func removeRows(for id: UUID) {
self.itemCellViewModels.removeAll(where: {[=11=].id == id})
}
问题 2:
有时嵌套数组不会影响更新内部数组中的数据。
我使用了你的 onCommit()
闭包并将操作发送到主父视图并强制更新视图。
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
onCommit(itemCellViewModel.item) //<-- Here
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
注意:第一部分不需要两个 .onDelete
。
注:
如果您仍然面临动画故障,请使用主队列。
.onDelete(perform: { (indexSet) in
DispatchQueue.main.async {
listViewModel.removeRows(at: indexSet)
}
})
即使在您的代码中从数组中延迟删除项目,它也会起作用。
在待办事项样式列表中,我在更新列表时遇到了 2 个问题。
删除列表中的项目时,它会飞到随机的地方。 (故障)。我想这与使用独特的
有关identifier
勾选列表中的项目时,不会更改部分。视图仅在添加新项目后更新。
- 此外,也许您知道如何在没有更多项目时隐藏部分,并在添加任务时重新出现。
ListView
struct ContentView: View {
@ObservedObject var listViewModel = ListViewModel()
@State var newItem = ""
var body: some View {
VStack {
Form {
// To-do section
Section(header: Text("New")) {
ForEach(listViewModel.itemCellViewModels) { itemCellViewModel in
if itemCellViewModel.item.accomplished == false {
ItemCellView(itemCellViewModel: itemCellViewModel)
}
}.onDelete(perform: { indexSet in
//remove item from the shopping list
self.listViewModel.itemCellViewModels.remove(atOffsets: indexSet)
print(indexSet)
})
.onDelete(perform: listViewModel.removeRows)
}
// Accomplished section
Section(header: Text("Done")) {
ForEach(listViewModel.itemCellViewModels.indices, id: \.self) { index in
if listViewModel.itemCellViewModels[index].item.accomplished == true {
ItemCellView(itemCellViewModel: listViewModel.itemCellViewModels[index])
}
}.onDelete(perform: listViewModel.removeRows)
}
}
TextField("Enter item here", text: $newItem) { _ in
} onCommit: {
self.listViewModel.addItem(item: Item(productName: newItem, accomplished: false))
newItem = ""
}
.autocapitalization(.none)
.padding()
.border(Color.blue)
.padding()
}
// update ViewModel with placeholder data
.onAppear(perform: {self.listViewModel.itemCellViewModels.append(contentsOf: placeholderItems)})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
var placeholderItems = [
ItemViewModel(item: Item(productName: "1", accomplished: false)),
ItemViewModel(item: Item(productName: "2", accomplished: false)),
ItemViewModel(item: Item(productName: "3", accomplished: false)),
ItemViewModel(item: Item(productName: "4", accomplished: false)),
ItemViewModel(item: Item(productName: "5", accomplished: false)),
ItemViewModel(item: Item(productName: "6", accomplished: true)),
ItemViewModel(item: Item(productName: "7", accomplished: true)),
ItemViewModel(item: Item(productName: "8", accomplished: true)),
ItemViewModel(item: Item(productName: "9", accomplished: true)),
ItemViewModel(item: Item(productName: "10", accomplished: true))
]
/// The rest of the code
// Reusable View for Cells
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
import Foundation
import Combine
class ItemViewModel: ObservableObject, Identifiable {
@Published var item: Item
var id = UUID()
@Published var items: [Item] = []
@Published var completionStateIconName = ""
private var cancellables = Set<AnyCancellable>()
init(item: Item) {
self.item = item
$item
.map { item in
item.accomplished ? "checkmark.square" : "square"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables) // <- for memory management purposes
}
}
class ListViewModel: ObservableObject {
@Published var itemCellViewModels = [ItemViewModel]()
private var cancellables = Set<AnyCancellable>()
func addItem(item: Item) {
let itemVM = ItemViewModel(item: item)
self.itemCellViewModels.append(itemVM)
}
func removeRows(at offsets: IndexSet) {
itemCellViewModels.remove(atOffsets: offsets)
}
}
// Model
struct Item: Identifiable {
var id = UUID()
var productName: String
var accomplished: Bool
}
单元格的可重用视图
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
ItemViewModel
import Foundation
import Combine
class ItemViewModel: ObservableObject, Identifiable {
@Published var item: Item
var id = UUID()
@Published var items: [Item] = []
@Published var completionStateIconName = ""
private var cancellables = Set<AnyCancellable>()
init(item: Item) {
self.item = item
$item
.map { item in
item.accomplished ? "checkmark.square" : "square"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables) // <- for memory management purposes
}
}
ListViewModel
import Foundation
import Combine
class ListViewModel: ObservableObject {
@Published var itemCellViewModels = [ItemViewModel]()
private var cancellables = Set<AnyCancellable>()
func addItem(item: Item) {
let itemVM = ItemViewModel(item: item)
self.itemCellViewModels.append(itemVM)
}
func removeRows(at offsets: IndexSet) {
itemCellViewModels.remove(atOffsets: offsets)
}
}
型号
struct Item: Identifiable {
var id = UUID()
var productName: String
var accomplished: Bool
}
首先,你加错了ForEach
。
因为您只需要添加那些已完成或未完成的对象。您需要使用过滤器而不是迭代所有对象并检查 ForEach 内部的条件。通过删除它,您的第一个问题解决了动画,您还可以添加一个空条件,这样当您完成的数组为空时,该部分会自动将其从视图中删除。
作为下面的演示,我们为两个部分使用了不同的数组,因此部分行的索引将发生变化,并且您的数据源是单个数组。为此,您需要通过 id 从数组中删除项目。
struct ContentView: View {
@ObservedObject var listViewModel = ListViewModel()
var arrNotAccomplished: [ItemViewModel] {
return listViewModel.itemCellViewModels.filter({![=10=].item.accomplished})
}
var arrAccomplished: [ItemViewModel] {
return listViewModel.itemCellViewModels.filter({[=10=].item.accomplished})
}
@State var newItem = ""
var body: some View {
VStack {
Form {
// To-do section
if !arrNotAccomplished.isEmpty {
Section(header: Text("New")) {
ForEach(arrNotAccomplished) { itemCellViewModel in
ItemCellView(itemCellViewModel: itemCellViewModel) { (_) -> (Void) in
self.listViewModel.objectWillChange.send()
}
}.onDelete { (index) in
guard let firstIndex = index.first else { return }
self.listViewModel.removeRows(for: arrNotAccomplished[firstIndex].id)
}
}
}
// Accomplished section
if !arrAccomplished.isEmpty {
Section(header: Text("Done")) {
ForEach(arrAccomplished) { itemCellViewModel in
ItemCellView(itemCellViewModel: itemCellViewModel) { (_) -> (Void) in
self.listViewModel.objectWillChange.send()
}
}.onDelete { (index) in
guard let firstIndex = index.first else { return }
self.listViewModel.removeRows(for: arrAccomplished[firstIndex].id)
}
}
}
}
// ====== other body view code =========
更新删除功能代码。
func removeRows(for id: UUID) {
self.itemCellViewModels.removeAll(where: {[=11=].id == id})
}
问题 2: 有时嵌套数组不会影响更新内部数组中的数据。
我使用了你的 onCommit()
闭包并将操作发送到主父视图并强制更新视图。
struct ItemCellView: View {
@ObservedObject var itemCellViewModel: ItemViewModel
// send an item (doesn't return anything)
var onCommit: (Item) -> (Void) = { _ in }
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(itemCellViewModel.item.productName)
}
Spacer()
Button(action: {
itemCellViewModel.item.accomplished.toggle()
onCommit(itemCellViewModel.item) //<-- Here
}, label: {
Image(systemName: (itemCellViewModel.item.accomplished ? "checkmark.square" : "square"))
.resizable()
.frame(width: 25, height: 25)
})
}.opacity(itemCellViewModel.item.accomplished ? 0.3 : 1)
}
}
注意:第一部分不需要两个 .onDelete
。
注: 如果您仍然面临动画故障,请使用主队列。
.onDelete(perform: { (indexSet) in
DispatchQueue.main.async {
listViewModel.removeRows(at: indexSet)
}
})
即使在您的代码中从数组中延迟删除项目,它也会起作用。