4

我一直在编写我的第一个 SwiftUI 应用程序,用于管理图书收藏。它有List大约 3,000 个项目,可以非常有效地加载和滚动。如果使用切换控件过滤列表以仅显示书籍我没有更新前 UI 冻结了 20 到 30 秒,大概是因为 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"))
    }
4

6 回答 6

8

您是否尝试过将过滤后的数组传递给 ForEach。像这样的东西:

ForEach(userData.bookList.filter {  return !$0.own }) { book in
    NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}

更新

事实证明,它确实是一个丑陋、丑陋的 bug:

我没有过滤数组,而是在翻转开关时将 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 完全丢弃列表,而不是一次删除一行。它通过在两个单独的封装视图中使用两个单独的列表来做到这一点:FilteredNotFiltered. 下面是一个包含 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)")
    }
}
于 2019-08-24T12:33:11.917 回答
4

查看这篇文章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 旧列表已经消失,现在有一个新列表,这意味着它不会”不要试图以动画的方式移动行。”

于 2020-02-12T13:52:29.053 回答
2

无需复杂的解决方法,只需清空 List 数组,然后设置新的过滤器数组。可能需要引入延迟,以便后续写入不会省略清空 listArray。

List(listArray){item in
  ...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
  self.listArray = newList
}
于 2019-10-10T19:08:09.137 回答
2

我认为我们必须等到 SwiftUI List 的性能在随后的 beta 版本中有所提高。当列表从一个非常大的数组(500+)过滤到非常小的数组时,我经历了同样的滞后。我创建了一个简单的测试应用程序来为具有整数 ID 和带有按钮的字符串的简单数组的布局计时,以简单地更改正在呈现的数组 - 相同的延迟。

于 2019-08-24T14:10:34.067 回答
2

在寻找如何调整 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()
    }
}
于 2020-09-22T21:05:31.300 回答
0

如果您在“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()
    }
}
于 2019-09-26T08:31:10.057 回答