【发布时间】:2021-04-17 00:44:14
【问题描述】:
我有一个合并函数,用于搜索项目列表并返回匹配项。它不仅跟踪要向用户显示的与搜索词匹配的项目,还跟踪哪些项目已被用户标记为“已选择”。
这个功能很好用,包括动画,直到我在合并发布者链中添加.debounce(for: .seconds(0.2), scheduler: RunLoop.main) 或 .receive(on: RunLoop.main)。此时,View 中的结果呈现变得莫名其妙——项目标题开始显示为标题视图、项目重复等等。
您可以在随附的 GIF 中看到结果。
GIF 版本使用的是.receive(on: RunLoop.main)。请注意,我什至不在这里使用搜索词,尽管它也会导致有趣的结果。还可能值得注意的是,如果删除了withAnimation { },则所有与问题行都可以正常工作。
我希望能够使用debounce,因为列表最终可能会很大,我不想在每次击键时过滤整个列表。
在这些情况下如何让表格视图正确呈现?
示例代码(请参阅内联 cmets 了解代码的痛点和解释。它应该运行良好,但如果两个相关行中的任何一个未注释):
import SwiftUI
import Combine
import UIKit
class Completer : ObservableObject {
@Published var items : [Item] = [] {
didSet {
setupPipeline()
}
}
@Published var filteredItems : [Item] = []
@Published var chosenItems: Set<Item> = []
@Published var searchTerm = ""
private var filterCancellable : AnyCancellable?
private func setupPipeline() {
filterCancellable =
Publishers.CombineLatest($searchTerm,$chosenItems) //listen for changes of both the search term and chosen items
.print()
// ** Either of the following lines, if uncommented will cause chaotic rendering of the table **
//.receive(on: RunLoop.main) //<----- HERE --------------------
//.debounce(for: .seconds(0.2), scheduler: RunLoop.main) //<----- HERE --------------------
.map { (term,chosen) -> (filtered: [Item],chosen: Set<Item>) in
if term.isEmpty { //if the term is empty, return everything
return (filtered: self.items, chosen: chosen)
} else { //if the term is not empty, return only items that contain the search term
return (filtered: self.items.filter { $0.name.localizedStandardContains(term) }, chosen: chosen)
}
}
.map { (filtered,chosen) in
(filtered: filtered.filter { !chosen.contains($0) }, chosen: chosen) //don't include any items in the chosen items list
}
.sink { [weak self] (filtered, chosen) in
self?.filteredItems = filtered
}
}
func toggleItemChosen(item: Item) {
withAnimation {
if chosenItems.contains(item) {
chosenItems.remove(item)
} else {
searchTerm = ""
chosenItems.insert(item)
}
}
}
}
struct ContentView: View {
@StateObject var completer = Completer()
var body: some View {
Form {
Section {
TextField("Term", text: $completer.searchTerm)
}
Section {
ForEach(completer.filteredItems) { item in
Button(action: {
completer.toggleItemChosen(item: item)
}) {
Text(item.name)
}.foregroundColor(completer.chosenItems.contains(item) ? .red : .primary)
}
}
if completer.chosenItems.count != 0 {
Section(header: HStack {
Text("Chosen items")
Spacer()
Button(action: {
completer.chosenItems = []
}) {
Text("Clear")
}
}) {
ForEach(Array(completer.chosenItems)) { item in
Button(action: {
completer.toggleItemChosen(item: item)
}) {
Text(item.name)
}
}
}
}
}.onAppear {
completer.items = ["Chris", "Greg", "Ross", "Damian", "George", "Darrell", "Michael"]
.map { Item(name: $0) }
}
}
}
struct Item : Identifiable, Hashable {
var id = UUID()
var name : String
}
【问题讨论】:
-
idForEach(Array(completer.chosenItems))可能是问题所在 -
我之前也发生过类似的事情,因为每个项目都没有唯一的 ID - 不确定,但
Array可能会删除您的Item的id -
不,很遗憾,似乎不是这样。将
chosenItems更改为[Item]而不是Set,因此我可以直接在ForEach中使用它(包括显式指定id)无效。另外,它仍然保持其类型 (Item),即Identifiable并具有id属性,因此没有任何内容被“擦除” -
我没有答案,但是如果您取出“withAnimation”,它似乎可以与包含的 .debounce 一起使用。
-
@nicksarno 是的,我可能应该将其添加到问题中。最初我发现了这个问题,因为一切都很好,然后我添加了
withAnimation并且它坏了,这导致我将罪魁祸首缩小到RunLoop的东西