【问题标题】:How do I efficiently filter a long list in SwiftUI?如何有效地过滤 SwiftUI 中的长列表?
【发布时间】:2019-08-24 11:18:16
【问题描述】:

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

【问题讨论】:

    标签: swiftui swiftui-list


    【解决方案1】:

    您是否尝试过将过滤后的数组传递给 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)")
        }
    }
    

    【讨论】:

    • 我已经试过了。它会快速呈现初始列表,但如果我将!$0.own 绑定到切换状态,我也会遇到同样的性能问题。
    • 信不信由你,这个错误的原因与 Android 类似列表 I mentioned to you before 的去/初始化有关。不要再震惊了:)
    • 是的,谢谢,我记住了!这实际上就是我想出解决方法的想法。通过完全销毁 List,而不是让它缓存行以供以后使用。
    • 如果您有一个更复杂的过滤器以使结果列表是动态的,这是否也有效?
    • 这成功了,它只需要使用 2 个过滤器视图就可以工作,您可以从根视图将过滤器作为状态变量传递!非常感谢您提供此解决方法。你治好了我的头痛
    【解决方案2】:

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

    【讨论】:

    • 我尝试了这个解决方案,但每次我从我有 List with .id(UUID()) 解决方案的视图返回时都会崩溃。
    • 我也有同样的问题@sabiland,你找到解决方案了吗?
    【解决方案3】:

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

    【讨论】:

    • 我认为你是对的。我相信这个问题很快就会得到解决。
    【解决方案4】:

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

    List(listArray){item in
      ...
    }
    
    self.listArray = []
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
      self.listArray = newList
    }
    

    【讨论】:

    • 在哪里放代码sn-p清空列表?
    • 就在您需要的地方?例如,当您想在代码中的某处更新或添加元素时,您复制列表,然后根据需要对其进行修改,然后使用上面的片段清空原始列表,以便刷新。在短暂的延迟之后,您再次添加更新的列表。
    【解决方案5】:

    在寻找如何调整 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()
        }
    }
    

    【讨论】:

      【解决方案6】:

      如果您在“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()
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-08-19
        • 2022-01-06
        • 2016-01-29
        • 2023-02-06
        • 2012-06-23
        相关资源
        最近更新 更多