【问题标题】:SwiftUI Bug - List changes lock UI - (OLD TITLE: SwiftUI CoreData fetch is very slow)SwiftUI Bug - List changes lock UI - (OLD TITLE: SwiftUI CoreData fetch is very slow)
【发布时间】:2020-02-11 11:17:25
【问题描述】:

更新 #4

  • 我对这篇文章进行了重新排序,以便更轻松地阅读。您将在下面阅读的内容将详细介绍我在使用 SwiftUI 时遇到的错误。我最近要求苹果提供代码级支持,苹果也确认了这一点,并要求我联系反馈以寻求解决方案(这也完成了,还没有答案)。

错误是这样的:在 SwiftUI 视图中显示 List 或 ForEach 后,如果您通过更改列出的项目数来更改该视图,UI 会在尝试计算数量时锁定已更改/需要更改的行数..

我在 Apple 开发者论坛中看到其他人遇到过此错误。他们的临时解决方案是“将数组设置为空白”,从而在修改列出的数据集之前完全清除列表约 100 毫秒。这将充分避免用户使用数据数组迭代 List 或 ForEach 的锁定。

问题是,正如本文所述,使用 CoreData,似乎没有任何方法可以清除推送的字母之间的列表(获取请求)。

在更新 #3 中,有一个 GitHub 项目,其中显示了此问题的示例以及示例数据。

感谢您提供有关解决方法的任何意见。

更新 #3

不好.. 如this post 中所述,我能够从使用 CoreData 更改为本地 SQLite 数据库文件。我的结果是搜索和使用 CoreData 一样慢。我不知道这里发生了什么。但也许它与 SwiftUI 输出的渲染结果有关?无论哪种方式,搜索和显示大量数据似乎都是不可能的..

根据 J. Doe 的要求,我在 GitHub 上发布了一个示例项目来演示此问题。这个项目可以找到here

我希望有人能看到我做错了什么。我很难相信这只是 iOS 的限制..

原帖

有什么想法吗?

我觉得我缺少一些基本的东西。我的获取请求(下面的代码)非常慢。我试图向 CoreData 模型添加一个索引,但有负面改进(下面 J. Doe 的建议)。我在想也许我需要以某种方式在 fetch 请求中添加一个 fetchBatchSize 声明(想通了 - 请参阅下面的更新 #2 - 没有帮助),但是使用 SwiftUI 中的属性包装器 @FetchRequest,似乎没有办法做这个。

下面的代码正在处理一个包含大约 5,000 条记录的测试数据集。在搜索中,每次更改输入(输入每个字母)时,搜索都会再次运行,这会将系统拖到停止状态(CPU 占用 100% 以上,内存使用量不断增加)。

在以前的应用程序中,我完成了类似的任务,但这些应用程序使用 SQLite 数据文件并且是用 ObjC 编写的。在那些情况下,事情真的很快,是这个测试数据集的 3 倍多。

如果有人能指出正确的方向来加快我的 CoreData 获取速度,我将不胜感激。如果我不需要的话,我不想回到 SQLite 文件..

非常感谢!

使用 SwiftUI,这是我的代码:

struct SearchView: View {


    @Binding var searchTerm:String
    var titleBar:String

    var fetch: FetchRequest<MyData>
    var records: FetchedResults<MyData>{fetch.wrappedValue}

    init(searchTerm:Binding<String>, titleBar:String) {
        self._searchTerm = searchTerm
        self.titleBar = titleBar
        self.fetch = FetchRequest(entity: MyData.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ MyData.header, ascending: true)], predicate: NSCompoundPredicate(type: .and, subpredicates: [ NSCompoundPredicate(type: .or, subpredicates: [NSPredicate(format: "%K CONTAINS[cd] %@", #keyPath(MyData.title),searchTerm.wrappedValue), NSPredicate(format: "%K CONTAINS[cd] %@", #keyPath(MyData.details),searchTerm.wrappedValue)]), NSPredicate(format: "%K == %@", #keyPath(MyData.titleBar),titleBar)])) //fetch request contains logic for module and search data - need to fix sort order later
    }

    var body: some View {


        List{

            Section(header: SearchBar(text: $searchTerm)) {

                ForEach(records, id: \.self) { fetchedData in

                    VStack {
                        NavigationLink(destination: DetailView(titleBar: fetchedData.title!, statuteData: fetchedData.details!, isFavorite: fetchedData.isFavorite)) {

                            HStack {
                                Text(fetchedData.header!)
                                    .font(.subheadline)

                                VStack(alignment: .leading) {
                                    Text(fetchedData.title!)
                                }
                                .scaledToFit()

                                Spacer()

                                if fetchedData.isFavorite {
                                    Image(systemName: "star.fill")
                                        .imageScale(.small)
                                        .foregroundColor(.yellow)
                                }
                            }
                        }
                    }
                }
            }.navigationBarTitle(Text("Search"))
        }
    }
}

感谢您的帮助。

更新:

在编辑之前,我报告了存储数据的另一个问题,但是,该问题已通过以下帖子解决:

CoreData writing objects slow

更新 #2:

我最初的问题是如何在我的 fetch 中添加批次限制以查看是否有帮助。我能够在不使用 FetchRequest 包装器的情况下重写提取,使用 NSFetchRequest 并添加批处理限制。它对这种情况没有任何帮助..

再次感谢

【问题讨论】:

  • 您添加了索引?有什么复制项目吗?
  • 找到 - 添加索引选项 - 将尝试并报告我的发现..
  • 为我的谓词中的两个属性添加了索引,分别和一起。除非我在那里做错了什么,否则没有帮助..
  • 复制粘贴代码不起作用。我可以看看,但前提是问题很容易重现。一种方法是使用最少的代码创建一个项目来重现问题并将其上传到 github 并在此处添加 github 链接
  • 我会尝试在 AND 复合谓词中交换两个子谓词:首先(快速)和 CONTAINS 进行严格的 == 比较,因为它要慢得多(特别是因为有两个其中的子谓词)。

标签: xcode sqlite core-data swiftui


【解决方案1】:

我在list 中遇到了同样的问题,即从核心数据中获取结果时滚动速度非常慢。与我之前使用 UIKit 的解决方案(相同的数据,相同的获取)相比,使用 swiftUI 的速度很慢。除了必须使用 fetchOffset 和 fetchLimit 之外,我发现一个主要的性能问题是由于使用了NavigationLink。如果没有 NavigationLink,性能很好,但使用 NavigationLink 就不行了。

搜索解决方案时,我找到了 Anupam Chugh 撰写的 this blog post,他撰写了关于此问题的文章,并提供了我在下面复制的解决方案。我很感激他。这是他的解决方案,不是我的。

关键是当NavigationLinklist 中使用时,目标视图会立即加载,即使用户尚未导航到该视图。为了克服这个问题,必须使目标视图变得惰性。

在我的例子中,我在主视图中选择了一种食物,然后在详细视图中显示了所选食物的 180 多个属性的列表...

解决方案:

使用以下代码创建一个新文件

import SwiftUI

/// Creates a lazy view from view.
///
/// Helpfull for use of `NavigationLink` within `list`, where destination views    are loaded immediately even when the user hasn’t navigated to that view.
/// Embedding the destination view in LazyView makes the destination view lazy and speeds up performance significantly for long lists.
///
/// ```Swift
/// NavigationLink(destination: LazyView(Text("Detail Screen"))){
///    Text("Tap me to see detail screen!")
/// }
/// ```
///
/// Source: [Blog post by Anupam Chugh on Medium]( https://medium.com/better-programming/swiftui-navigation-links-and-the-common-pitfalls-faced-505cbfd8029b). Thank you!!!!
struct LazyView<Content: View>: View {
    let build: () -> Content
        init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

并像这样使用它:

NavigationLink(destination: LazyView(Text("Detail Screen"))){
   Text("Tap me to see detail screen!")
}

我希望这对其他人也有帮助。

【讨论】:

  • 很棒的信息,谢谢。我的列表包含很多导航链接。我将实现这一点。欣赏洞察力!
  • 我想知道如果您导航到的视图带有参数,您将如何修改它?如果你熟悉,你能举个例子吗?我有很多大列表,它们看起来很笨重,毫无疑问是因为这种情况......再次感谢。
  • 我这样使用它:ForEach(foods) { (food: Food) in NavigationLink(destination: LazyView(self.foodDetailsView(food: food)) ) { FoodNutrientsRowView(food: food, formatter: self.formatter) } }
  • and func foodDetailsView(food: Food) -> some View { FoodDetailsView(ingredientCollection: anIngredientCollection, food: food) }
【解决方案2】:

这是解决方法,我可以补充一下,这是完全不可接受的。它使应用程序在技术上可以运行,但速度很慢,感觉就像在 8086 上加载 Windows 10。荒谬。

此外,Apple 反馈仍然没有回答甚至确认。即使他们说他们无法帮助我,我的代码级支持请求也会被扣除。不开心..

无论如何,如果您想构建一个感觉就像在泥泞中挖掘的应用程序,使用 CoreData 并能够搜索该数据,这就是您的解决方法..

第一个: 创建您的数据的可散列模型,或者至少是您需要搜索和/或显示的部分数据:

struct MyDataModel: Hashable {
  let title: String
  let name: String
  let myData: String
}

第二: 创建一个 ObservableObject 类,该类发布一个变量,其中包含您刚刚创建的数据模型类的数组:

class MyData:ObservableObject {
  @Published var searchDataArray = [MyDataModel]()
}

第三: 确保将环境变量推送到您计划使用的视图中: (这个例子在我的 SceneDelegate.swift 文件中

let myData = MyData()

并将.environmentObject(myData) 附加到您需要的任何视图中。

第四: 从您的视图访问 Env Var:@EnvironmentObject var myData: MyData 并将您的获取结果加载到已发布的数据数组中,我使用此功能完成任务:

func arrayFiller(){ 

    if self.myData.searchDataArray.count > 0 {
        self.myData.searchDataArray.removeAll()
    }

    for item in self.fetchRequest {
        self.myData.searchDataArray.append(MyDataModel(title: item.title!, name: item.name!, myData: item:myData!))
    }
}

最后,从您要搜索的视图中,您可以迭代已发布的 env var,并且您可以在更改搜索条件之间延迟清除数组以避免错误。

ForEach(self.myData.searchDataArray, id: \.self) { fetchedItem in
    Text(fetchedItem.name)
}

然后,我使用 .onReceive 观察我的 searchTerm 变量的变化,擦除已发布的数组,等待 10 毫秒,然后用与我的搜索词匹配的数据重新填充数组。

它真的很慢而且很可怕。它有效,但我认为我不能在这个烂摊子的情况下接近生产。

【讨论】:

  • 如果您选择仅在按下“搜索”或“开始”时更新搜索词,它的运行速度会更快。所以没有更多的流动列表,它实际上看起来可以通过使用“搜索 Go”方法。
猜你喜欢
  • 2022-12-19
  • 2022-12-31
  • 2021-10-09
  • 2020-10-19
  • 1970-01-01
  • 2020-05-01
  • 2020-12-09
  • 2021-10-17
相关资源
最近更新 更多