如何在 SwiftUI 中有效过滤长列表? [英] How do I efficiently filter a long list in SwiftUI?
问题描述
我一直在编写我的第一个 SwiftUI 应用程序,用于管理图书收藏.它有一个包含大约 3,000 个项目的 List
,可以非常有效地加载和滚动.如果使用切换控件过滤列表以仅显示我没有 UI 冻结的书籍在更新前二十到三十秒,大概是因为 UI 线程正忙于决定是否显示 3,000 个单元格中的每一个.
I've been writing my first SwiftUI application, which manages a book collection. It has a List
of around 3,000 items, which loads and scrolls pretty efficiently. If use a toggle control to filter the list to show only the books I don't have the UI freezes for twenty to thirty seconds before updating, presumably because the UI thread is busy deciding whether to show each of the 3,000 cells or not.
在 SwiftUI 中是否有处理此类大列表更新的好方法?
Is there a good way to do handle updates to big lists like this in SwiftUI?
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
ForEach(userData.bookList) { book in
if !self.userData.showWantsOnly || !book.own {
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
推荐答案
您是否尝试过将过滤后的数组传递给 ForEach.像这样:
Have you tried passing a filtered array to the ForEach. Something like this:
ForEach(userData.bookList.filter { return !$0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}
更新
事实证明,这确实是一个丑陋的、丑陋的错误:
Update
As it turns out, it is indeed an ugly, ugly bug:
我没有过滤数组,而是在开关翻转时将 ForEach 全部删除,并用简单的 Text("Nothing")
视图替换它.结果是一样的,需要30秒!
Instead of filtering the array, I just remove the ForEach all together when the switch is flipped, and replace it by a simple Text("Nothing")
view. The result is the same, it takes 30 secs to do so!
struct SwiftUIView: View {
@EnvironmentObject var userData: UserData
@State private var show = false
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if self.userData.showWantsOnly {
Text("Nothing")
} else {
ForEach(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
}
解决方法
我确实找到了一种运行速度很快的解决方法,但它需要一些代码重构.魔法"通过封装发生.该解决方法强制 SwiftUI 完全丢弃列表,而不是一次删除一行.它通过在两个单独的封装视图中使用两个单独的列表来实现:Filtered
和 NotFiltered
.下面是一个包含 3000 行的完整演示.
Workaround
I did find a workaround that works fast, but it requires some code refactoring. The "magic" happens by encapsulation. The workaround forces SwiftUI to discard the List completely, instead of removing one row at a time. It does so by using two separate lists in two separate encapsualted views: Filtered
and NotFiltered
. Below is a full demo with 3000 rows.
import SwiftUI
class UserData: ObservableObject {
@Published var showWantsOnly = false
@Published var bookList: [Book] = []
init() {
for _ in 0..<3001 {
bookList.append(Book())
}
}
}
struct SwiftUIView: View {
@EnvironmentObject var userData: UserData
@State private var show = false
var body: some View {
NavigationView {
VStack {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if userData.showWantsOnly {
Filtered()
} else {
NotFiltered()
}
}
}.navigationBarTitle(Text("Books"))
}
}
struct Filtered: View {
@EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList.filter { $0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct NotFiltered: View {
@EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct Book: Identifiable {
let id = UUID()
let own = Bool.random()
}
struct BookRow: View {
let book: Book
var body: some View {
Text("\(String(book.own)) \(book.id)")
}
}
struct BookDetail: View {
let book: Book
var body: some View {
Text("Detail for \(book.id)")
}
}
这篇关于如何在 SwiftUI 中有效过滤长列表?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!