如何在 SwiftUI 中有效地过滤一长串列表?
How do I efficiently filter a long list in SwiftUI?
我一直在编写我的第一个 SwiftUI 应用程序,它管理一本书 collection。它有一个 List
大约 3,000 个项目,加载和滚动非常高效。如果使用切换控件来过滤列表以仅显示我没有的书籍 UI 在更新之前冻结二十到三十秒,大概是因为 UI 线程正忙于决定是否显示每个是否有 3,000 个单元格。
在 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。像这样:
ForEach(userData.bookList.filter { return ![=10=].own }) { book in
NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}
更新
事实证明,这确实是一个非常丑陋的错误:
我没有过滤数组,而是在翻转开关时将 ForEach 一起删除,并用一个简单的 Text("Nothing")
视图替换它。结果是一样的,耗时30秒!
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"))
}
}
解决方法
我确实找到了一种快速解决方法,但它需要进行一些代码重构。 "magic" 通过封装发生。解决方法强制 SwiftUI 完全丢弃列表,而不是一次删除一行。它通过在两个单独的封装视图中使用两个单独的列表来实现:Filtered
和 NotFiltered
。下面是一个包含 3000 行的完整演示。
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 { [=12=].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 List 性能在后续 beta 版本中得到改进。当列表从非常大的数组 (500+) 过滤到非常小的数组时,我遇到了同样的延迟。我创建了一个简单的测试应用程序来为具有整数 ID 的简单数组和带有按钮的字符串的布局计时,以简单地更改正在呈现的数组 - 同样的滞后。
如果您在 'SceneDelegate' 文件中初始化 class,此代码将正常工作,如下所示:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userData = UserData()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(userData)
)
self.window = window
window.makeKeyAndVisible()
}
}
无需复杂的解决方法,只需清空列表数组,然后设置新的过滤器数组即可。可能有必要引入延迟,以便清空 listArray 不会被随后的写入遗漏。
List(listArray){item in
...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.listArray = newList
}
查看这篇文章https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
简而言之,本文提出的解决方案是将.id(UUID())添加到列表中:
List(items, id: \.self) {
Text("Item \([=10=])")
}
.id(UUID())
"Now, there is a downside to using id() like this: you won't get your update animated. Remember, we're effectively telling SwiftUI the old list has gone away and there's a new list now, which means it won't try to move rows around in an animated way."
在寻找如何调整 Seitenwerk 对我的解决方案的响应时,我发现了一个对我有很大帮助的绑定扩展。这是代码:
struct ContactsView: View {
@State var stext : String = ""
@State var users : [MockUser] = []
@State var filtered : [MockUser] = []
var body: some View {
Form{
SearchBar(text: $stext.didSet(execute: { (response) in
if response != "" {
self.filtered = []
self.filtered = self.users.filter{[=10=].name.lowercased().hasPrefix(response.lowercased()) || response == ""}
}
else {
self.filtered = self.users
}
}), placeholder: "Buscar Contactos")
List{
ForEach(filtered, id: \.id){ user in
NavigationLink(destination: LazyView( DetailView(user: user) )) {
ContactCell(user: user)
}
}
}
}
.onAppear {
self.users = LoadUserData()
self.filtered = self.users
}
}
}
这是绑定扩展:
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print([=11=]) }, in: 0...10)
func didSet(execute: @escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute([=11=])
self.wrappedValue = [=11=]
}
)
}
}
LazyView 是可选的,但我不厌其烦地展示它,因为它对列表的性能有很大帮助,并防止 swiftUI 创建整个列表的 NavigationLink 目标内容。
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
我一直在编写我的第一个 SwiftUI 应用程序,它管理一本书 collection。它有一个 List
大约 3,000 个项目,加载和滚动非常高效。如果使用切换控件来过滤列表以仅显示我没有的书籍 UI 在更新之前冻结二十到三十秒,大概是因为 UI 线程正忙于决定是否显示每个是否有 3,000 个单元格。
在 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。像这样:
ForEach(userData.bookList.filter { return ![=10=].own }) { book in
NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}
更新
事实证明,这确实是一个非常丑陋的错误:
我没有过滤数组,而是在翻转开关时将 ForEach 一起删除,并用一个简单的 Text("Nothing")
视图替换它。结果是一样的,耗时30秒!
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"))
}
}
解决方法
我确实找到了一种快速解决方法,但它需要进行一些代码重构。 "magic" 通过封装发生。解决方法强制 SwiftUI 完全丢弃列表,而不是一次删除一行。它通过在两个单独的封装视图中使用两个单独的列表来实现:Filtered
和 NotFiltered
。下面是一个包含 3000 行的完整演示。
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 { [=12=].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 List 性能在后续 beta 版本中得到改进。当列表从非常大的数组 (500+) 过滤到非常小的数组时,我遇到了同样的延迟。我创建了一个简单的测试应用程序来为具有整数 ID 的简单数组和带有按钮的字符串的布局计时,以简单地更改正在呈现的数组 - 同样的滞后。
如果您在 'SceneDelegate' 文件中初始化 class,此代码将正常工作,如下所示:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userData = UserData()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(userData)
)
self.window = window
window.makeKeyAndVisible()
}
}
无需复杂的解决方法,只需清空列表数组,然后设置新的过滤器数组即可。可能有必要引入延迟,以便清空 listArray 不会被随后的写入遗漏。
List(listArray){item in
...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.listArray = newList
}
查看这篇文章https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
简而言之,本文提出的解决方案是将.id(UUID())添加到列表中:
List(items, id: \.self) {
Text("Item \([=10=])")
}
.id(UUID())
"Now, there is a downside to using id() like this: you won't get your update animated. Remember, we're effectively telling SwiftUI the old list has gone away and there's a new list now, which means it won't try to move rows around in an animated way."
在寻找如何调整 Seitenwerk 对我的解决方案的响应时,我发现了一个对我有很大帮助的绑定扩展。这是代码:
struct ContactsView: View {
@State var stext : String = ""
@State var users : [MockUser] = []
@State var filtered : [MockUser] = []
var body: some View {
Form{
SearchBar(text: $stext.didSet(execute: { (response) in
if response != "" {
self.filtered = []
self.filtered = self.users.filter{[=10=].name.lowercased().hasPrefix(response.lowercased()) || response == ""}
}
else {
self.filtered = self.users
}
}), placeholder: "Buscar Contactos")
List{
ForEach(filtered, id: \.id){ user in
NavigationLink(destination: LazyView( DetailView(user: user) )) {
ContactCell(user: user)
}
}
}
}
.onAppear {
self.users = LoadUserData()
self.filtered = self.users
}
}
}
这是绑定扩展:
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print([=11=]) }, in: 0...10)
func didSet(execute: @escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute([=11=])
self.wrappedValue = [=11=]
}
)
}
}
LazyView 是可选的,但我不厌其烦地展示它,因为它对列表的性能有很大帮助,并防止 swiftUI 创建整个列表的 NavigationLink 目标内容。
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}