英文:
macOS SwiftUI: how to trigger deleting an item?
问题
I'm here to provide the translated content as requested. Here is the translation of the text you provided:
我正在开发我的首个SwiftUI应用,运行在macOS Monterey 12.6上,使用的是Xcode 14.2。
我创建了一个新的_Multiplatform_ > _App_ 项目(启用了Core Data),Xcode生成了一个可用的示例应用程序。
视图部分如下所示:
```lang-swift
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("项目时间:\(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("添加项目", systemImage: "plus")
}
}
}
Text("选择一个项目")
}
}
通过单击+
按钮可以添加新项目 - 但如何删除它们?
我猜示例代码主要是为iOS生成的;我尝试在iPhone模拟器上运行应用程序:在那里,通过在列表中滑动项目来删除项目,就像通常一样。
然而,在macOS上,似乎我需要其他方法来触发删除操作。
我已经添加了一个类似于添加项目
的ToolbarItem
/ Button
,Xcode也生成了这个deleteItems
函数:
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("未解决的错误 \(nsError), \(nsError.userInfo)")
}
}
}
我想在iOS上,通过onDelete
modifier(?)以某种方式自动调用此函数。
我想象我可以尝试获取列表中项目的索引,将其转换为IndexSet
,并将调用deleteItems(...)
连接到删除按钮,但我有一种直觉,SwiftUI提供了一种更简单的方式来执行这样的标准任务 - 毕竟,这是对Core Data模型的CRUD操作...
我尝试使这里,这里,这里和这里的示例代码工作,但显然我似乎缺乏理解代码并根据我的需求进行适应所需的SwiftUI经验。
如何触发删除项目操作?
Please note that the code parts are preserved in their original language (Swift).
<details>
<summary>英文:</summary>
I am working on my very first SwiftUI app on macOS Monterey 12.6 using Xcode 14.2.
I have created a new _Multiplatform_ > _App_ project (enabling Core Data) and Xcode generated a working sample app.
The view part looks like this:
```lang-swift
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
Adding new items by clicking the +
button works - but how do I delete them ?
I suppose the sample code is mostly generated with iOS in mind; I tried running the app on an iPhone simulator: Deleting there just happens as usual by swiping the item in the list.
However, on macOS it seems I need some other means to trigger the deletion.
I have added a ToolbarItem
/ Button
analogous to Add Item
and Xcode also generated this deleteItems
function:
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
I figure on iOS, this function gets called somehow automagically via the onDelete
modifier (?).
I imagine I could try to get the index of the item in the list, turn that into an IndexSet
somehow and connect calling deleteItems(...)
to the delete button, but I have a gut feeling SwiftUI provides some much simpler way for such a standard task - after all, that's CRUD on the Core Data model...
I tried to get sample code from here, here, here and here to work, but I clearly seem to lack the SwiftUI experience required to understand the code and adapt it for my needs.
How do I trigger deleting an item ?
答案1
得分: 1
如果这个List
不在NavigationView
中,您就可以用两个手指向左滑动一行,然后看到删除按钮。点击它将触发onDelete
修饰符。
然而,由于列表位于NavigationView
中,而macOS以一种“分割”的方式显示它和导航目标,类似于NavigationSplitView
。在这种展示模式下,没有两个手指向左滑动的手势。
还请注意,NavigationView
在最新版本的macOS中已经被弃用。如果您想要“分割”展示,应该使用NavigationSplitView
。
如果您使用NavigationStack
,它只显示导航链接,只有在点击其中一个链接时才显示目标,那么就支持用两个手指向左滑动的手势。
否则,您将需要自己设计删除项目的用户界面。您可以从现有的macOS应用程序中汲取灵感。例如,在Notes应用程序中,导航栏有一个“垃圾桶”按钮。
您可以考虑将以下内容放入您的toolbar
中:
Button {
withAnimation {
selection.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
} label: {
Image(systemName: "trash")
}
其中,selection
设置如下:
@State var selection = Set<Item>()
// ...
List(selection: $selection) {
// ...
}
更完整的示例:
@State var items = ["1", "2", "3"]
@State var selection = Set<String>()
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items, id: \.self) { (item) in
NavigationLink {
Text("Item at \(item)")
} label: {
Text(item)
}
}
}
.toolbar {
ToolbarItem {
Button {
withAnimation {
items.removeAll(where: { selection.contains($0) })
}
} label: {
Image(systemName: "trash")
}
}
}
}.onChange(of: selection) { newValue in
print(newValue)
}
}
另一种方法是添加一个contextMenu
项来删除项目,类似于联系人应用程序的做法。
英文:
If this List
were not in a NavigationView
, you would have been able to swipe left with 2 fingers on a row, and see the delete button. Clicking that will trigger the onDelete
modifier.
However, since the list is in a NavigationView
, and macOS displays it and the navigation destination in a "split" way, similar to a NavigationSplitView
. In this mode of presentation, there is no swipe with 2 fingers gesture.
Also note that NavigationView
is deprecated in the newest version of macOS. You should use NavigationSplitView
if you want a "split" presentation.
If you use a NavigationStack
, which only shows the navigation links, and only shows the destination if you click on one of them, then the swipe left with 2 fingers gesture is supported.
Otherwise, you will have to come up with your own UI of deleting things. You can take inspiration from how existing macOS apps do it. For example, in the Notes app, the navigation bar has a "trash" button.
You can consider putting this into your toolbar
:
Button {
withAnimation {
selection.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
} label: {
Image(systemName: "trash")
}
where selection
is set up like this:
@State var selection = Set<Item>()
// ...
List(selection: $selection) {
// ...
}
A more complete example:
@State var items = ["1", "2", "3"]
@State var selection = Set<String>()
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items, id: \.self) { (item) in
NavigationLink {
Text("Item at \(item)")
} label: {
Text(item)
}
}
}
.toolbar {
ToolbarItem {
Button {
withAnimation {
items.removeAll(where: { selection.contains($0) })
}
} label: {
Image(systemName: "trash")
}
}
}
}.onChange(of: selection) { newValue in
print(newValue)
}
}
An alternative is to add a contextMenu
item for deleting items, similar to how the Contacts app does it.
答案2
得分: 1
由于@Sweeper的更完整示例更新,我能够找出问题所在(最终,仅通过基于文本的比较😟):我的代码中的ForEach(...)
循环缺少了一个id: \.self
参数:
// 不起作用,`selection` 从未设置
ForEach(items)
// 起作用
ForEach(items, id: \.self)
我不明白为什么需要id
,它从未在代码中使用过;可能是一些苹果/ SwiftUI / Swift / 其他黑魔法 - 有点令人沮丧,但我想这就是学习过程😞。有关id: \.self
的更多信息在这里。
所以,总结一下,要修改由Xcode生成的示例项目以在macOS上删除项目,需要以下步骤:
- 添加一个
selection
状态变量:
@State private var selection = Set<Item>()
- 将
selection
传递给NavigationView
中的List
:
List(selection: $selection) {
...
}
- 将
id: \.self
传递给ForEach
循环:
ForEach(items, id: \.self) { item in
...
}
- 添加一个与生成的
Add Item
项类似的用于“垃圾桶”按钮的ToolbarItem
:
ToolbarItem {
Button(action: deleteItem) {
Label("Delete Item", systemImage: "trash")
}
}
- 添加供上面按钮调用的
deleteItem
函数,类似于生成的addItem
项:
private func deleteItem() {
withAnimation {
print("delete button clicked")
// TODO: BUG - 删除第二个项目后,
// selection 包含第一个和第二个项目
print("selection: \(selection)")
// 注意:来自@Sweeper回答的示例代码:
// items.removeAll(where: { selection.contains($0) })
// 会出现一堆难以理解的错误消息,例如:
// + 不能调用非函数类型的值 'Binding<Subject>'
// + 引用下标 'subscript(dynamicMember:)' 需要 ...
// ... 包装器 'Binding<FetchRequest<Item>.Configuration>'
// --> 从生成的deleteItems(...)函数中调整方法
selection.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
据我所知,不需要onChange
修饰符来选择和删除本身;然而,修复上面代码中提到的注释中的错误可能会有用。
英文:
Thanks to @Sweeper's update with the more complete example, I was able to figure out the problem (in the end, simply by comparing on a text basis 😟): The ForEach(...)
loop in my code was missing an id: \.self
argument:
// doesn't work, `selection` never is set
ForEach(items)
// works
ForEach(items, id: \.self)
I don't understand why id
is required, it is never used in the code; must be some Apple / SwiftUI / Swift / whatever black magic - somewhat frustrating, but I guess that's the learning curve for you 😞. More on id: \.self
here.
So, in conclusion, the steps required to amend an Xcode-generated sample project to delete items on macOS are as follows:
- add a
selection
state variable:
@State private var selection = Set<Item>()
- pass
selection
to theList
inNavigationView
:
List(selection: $selection) {
...
- pass
id: \.self
to theForEach
loop:
ForEach(items, id: \.self) { item in
...
- add a
ToolbarItem
for the trash button, analogous to the generatedAdd Item
item:
ToolbarItem {
Button(action: deleteItem) {
Label("Delete Item", systemImage: "trash")
}
}
- add the
deleteItem
function for the button above to call, analogous to the generatedaddItem
item:
private func deleteItem() {
withAnimation {
print("delete button clicked")
// TODO: BUG - upon deleting a second item,
// selection contains first and second item
print("selection: \(selection)")
// NOTE: sample code from @Sweeper's answer:
// items.removeAll(where: { selection.contains($0) })
// fails with a bunch of inscrutable error messages, e.g:
// + Cannot call value of non-function type 'Binding<Subject>'
// + Referencing subscript 'subscript(dynamicMember:)' requires ...
// ... wrapper 'Binding<FetchRequest<Item>.Configuration>'
// --> adapting approach from generated deleteItems(...) function
selection.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
AFAICT, an onChange
modifier is not required for selecting and deleting itself; it might however be useful to fix the bug mentioned in the comment in the code above.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论