Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I efficiently filter a long list in SwiftUI?

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.

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"))
    }
like image 937
Eifion Avatar asked Dec 03 '22 10:12

Eifion


2 Answers

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:

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"))
    }
}

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)")
    }
}
like image 132
kontiki Avatar answered Jan 03 '23 10:01

kontiki


Check this article https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui

In short the solution proposed in this article is to add .id(UUID()) to the list:

List(items, id: \.self) {
    Text("Item \($0)")
}
.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."

like image 42
Serg Avatar answered Jan 03 '23 10:01

Serg