Как эффективно отфильтровать длинный список в SwiftUI?
Я писал свое первое приложение SwiftUI, которое управляет коллекцией книг. Оно имеет List
около 3000 предметов, которые загружаются и прокручиваются довольно эффективно. При использовании переключателя для фильтрации списка для отображения только тех книг, у которых пользовательский интерфейс не обновляется на двадцать-тридцать секунд перед обновлением, вероятно, потому что поток пользовательского интерфейса занят, решая, показывать ли каждую из 3000 ячеек или нет.
Есть ли хороший способ для обработки обновлений больших списков, как это в 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"))
}
2 ответа
Вы пытались передать отфильтрованный массив в ForEach. Что-то вроде этого:
ForEach(userData.bookList.filter { return !$0.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"))
}
}
Временное решение
Я нашел обходной путь, который работает быстро, но требует некоторого рефакторинга кода. "Волшебство" происходит путем инкапсуляции. Обходной путь заставляет 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 { $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)")
}
}
Проверьте эту статью https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
Короче говоря, решение, предлагаемое в этой статье, - добавить .id(UUID()) в список:
List(items, id: \.self) {
Text("Item \($0)")
}
.id(UUID())
"Теперь у использования id() есть обратная сторона: вы не получите анимацию обновления. Помните, мы фактически сообщаем SwiftUI, что старый список исчез, а теперь есть новый список, что означает, что он выиграл t пытаться перемещать строки в анимированном виде ".
Вместо сложного обходного пути просто очистите массив List, а затем установите новый массив фильтров. Возможно, потребуется ввести задержку, чтобы очистка listArray не была пропущена при последующей записи.
List(listArray){item in
...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.listArray = newList
}
В поисках того, как адаптировать ответ Seitenwerk к моему решению, я нашел расширение Binding, которое мне очень помогло. Вот код:
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{$0.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($0) }, in: 0...10)
func didSet(execute: @escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
LazyView не является обязательным, но я потрудился показать его, поскольку он очень помогает в производительности списка и не позволяет swiftUI создавать целевое содержимое NavigationLink для всего списка.
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Я думаю, нам нужно подождать, пока производительность SwiftUI List не улучшится в последующих бета-версиях. Я испытывал ту же задержку, когда списки фильтруются от очень большого массива (500+) до очень маленьких. Я создал простое тестовое приложение, чтобы рассчитать макет для простого массива с целочисленными идентификаторами и строк с кнопками, чтобы просто изменить, какой массив выводится - то же самое отставание.
Этот код будет работать правильно при условии, что вы инициализируете свой класс в файле SceneDelegate следующим образом:
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()
}
}