macOS SwiftUI:如何触发删除项目?

huangapple go评论70阅读模式
英文:

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_ &gt; _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(&quot;Item at \(item.timestamp!, formatter: itemFormatter)&quot;)
                } label: {
                    Text(item.timestamp!, formatter: itemFormatter)
                }
            }
            .onDelete(perform: deleteItems)
        }
        .toolbar {
#if os(iOS)
            ToolbarItem(placement: .navigationBarTrailing) {
                EditButton()
            }
#endif
            ToolbarItem {
                Button(action: addItem) {
                    Label(&quot;Add Item&quot;, systemImage: &quot;plus&quot;)
                }
            }
        }
        Text(&quot;Select an item&quot;)
    }
}

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(&quot;Unresolved error \(nsError), \(nsError.userInfo)&quot;)
        }
    }
}

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.

macOS SwiftUI:如何触发删除项目?

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(&quot;Unresolved error \(nsError), \(nsError.userInfo)&quot;)
        }
    }
} label: {
    Image(systemName: &quot;trash&quot;)
}

where selection is set up like this:

@State var selection = Set&lt;Item&gt;()
// ...
List(selection: $selection) {
    // ...
}

A more complete example:

@State var items = [&quot;1&quot;, &quot;2&quot;, &quot;3&quot;]
@State var selection = Set&lt;String&gt;()

var body: some View {
    NavigationView {
        List(selection: $selection) {
            ForEach(items, id: \.self) { (item) in
                NavigationLink {
                    Text(&quot;Item at \(item)&quot;)
                } label: {
                    Text(item)
                }
            }
        }
        .toolbar {
            ToolbarItem {
                Button {
                    withAnimation {
                        items.removeAll(where: { selection.contains($0) })
                    }
                } label: {
                    Image(systemName: &quot;trash&quot;)
                }
            }
        }
    }.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上删除项目,需要以下步骤:

  1. 添加一个selection状态变量:
@State private var selection = Set<Item>()
  1. selection传递给NavigationView中的List
List(selection: $selection) {
  ...
}
  1. id: \.self传递给ForEach循环:
ForEach(items, id: \.self) { item in
    ...
}
  1. 添加一个与生成的Add Item项类似的用于“垃圾桶”按钮的ToolbarItem
ToolbarItem {
    Button(action: deleteItem) {
        Label("Delete Item", systemImage: "trash")
    }
}
  1. 添加供上面按钮调用的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&#39;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:

  1. add a selection state variable:
@State private var selection = Set&lt;Item&gt;()
  1. pass selection to the List in NavigationView:
List(selection: $selection) {
  ...
  1. pass id: \.self to the ForEach loop:
ForEach(items, id: \.self) { item in
    ...
  1. add a ToolbarItem for the trash button, analogous to the generated Add Item item:
ToolbarItem {
    Button(action: deleteItem) {
        Label(&quot;Delete Item&quot;, systemImage: &quot;trash&quot;)
    }
}
  1. add the deleteItem function for the button above to call, analogous to the generated addItem item:
private func deleteItem() {
    withAnimation {
        print(&quot;delete button clicked&quot;)
        // TODO: BUG - upon deleting a second item,
        // selection contains first and second item
        print(&quot;selection: \(selection)&quot;)
        // NOTE: sample code from @Sweeper&#39;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 &#39;Binding&lt;Subject&gt;&#39;
        // + Referencing subscript &#39;subscript(dynamicMember:)&#39; requires ...
        //    ... wrapper &#39;Binding&lt;FetchRequest&lt;Item&gt;.Configuration&gt;&#39;
        // --&gt; adapting approach from generated deleteItems(...) function
        selection.forEach(viewContext.delete)

        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError(&quot;Unresolved error \(nsError), \(nsError.userInfo)&quot;)
        }
    }
}

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.

huangapple
  • 本文由 发表于 2023年6月12日 20:32:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76456703.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定