SwiftUI ForEach在大JSON上非常慢



我有一个字段,用户开始输入处方药的名称,在3个字符之后,它搜索包含超过36k个条目的4.5mb JSON文件。结构中只有两个字段持有解码的JSON(名称和id),我正在做一个简单的ForEach。然而,它是非常缓慢的,我需要使它实时。这是应用程序中的本地JSON文件(即我没有调用外部URL)。

有几个问题我需要解决:

  1. 我需要使它尽可能地响应
  2. 我需要返回按名称排序的结果,但包括数字(即泰诺50MG应该在泰诺100MG之前)。

对于问题#2,我尝试使用比较,它可以工作,但进一步减慢了速度,如下所示:

var allDrugsSorted: [Mod_Prescription_DrugsList] {
return Mod_Prescription_DrugsList.allDrugs.sorted { first, second in
first.name.compare(second.name, options: .numeric) == .orderedAscending
} // End Return
}

这是我的ForEach:

if isFocused == .prescription_name &&
viewModel.prescriptionName.count >= 3 {
ForEach(
viewModel.allDrugsSorted.filter {
$0.name
.lowercased()
.hasPrefix(
viewModel.prescriptionName.lowercased()
)
}.prefix(5), id: .self) { prescription in
Button {
isFocused = nil
} label: {
VStack(alignment: .leading) {
Text(LocalizedStringKey(prescription.name))
}
.frame(maxWidth: .infinity, alignment: .leading)
} // End Label
.contentShape(Rectangle())
.padding()
} // End ForEach
} // End If

我已经尝试了。id(UUID())技巧,但它没有任何帮助。

这是我加载JSON的结构体:

struct Mod_Prescription_DrugsList: Codable, Hashable {
let rxcui: String
let name: String
static let allDrugs = Bundle.main.decode([Mod_Prescription_DrugsList].self, from: "Drugs.json") 
static let example = allDrugs[0]
}

这是Bundle上的decode扩展:

extension Bundle {
func decode<T: Decodable>(
_ type: T.Type,
from file: String,
dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys
) -> T {
guard let url = self.url(
forResource: file,
withExtension: nil
) else {
fatalError("Failed to locate (file) in bundle.")
}
guard let data = try? Data(
contentsOf: url
) else {
fatalError("Failed to load (file) from bundle.")
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
fatalError("Failed to decode (file) from bundle due to missing key '(key.stringValue)' - (context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
fatalError("Failed to decode (file) from bundle due to type mismatch - (context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
fatalError("Failed to decode (file) from bundle due to missing (type) value - (context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
fatalError("Failed to decode (file) from bundle because it appears to be invalid JSON")
} catch {
fatalError("Failed to decode (file) from bundle - (error.localizedDescription)")
}
}
}

我还尝试在init上的视图模型中创建一个解码数组,虽然它加快了一些速度,但它仍然不是很好,而且它是不可用的,因为视图现在几乎需要一分钟才能加载。自我。allDrugsArray是视图模型中的@Published变量。下面是我写的函数:

func getAllDrugs() {
let sortedDrugs = Mod_Prescription_DrugsList.allDrugs.sorted { first, second in
first.name.compare(second.name, options: .numeric) == .orderedAscending
}
sortedDrugs.forEach { drug in
self.allDrugsArray.append(drug)
}
}

你真的需要重新构建这个。每次View需要更新时,加载List,对其进行排序和过滤。您应该在内存中保持此列表的排序状态,并根据需要对其进行过滤。

下面的架构应该可以工作:

import Combine
class Viewmodel: ObservableObject{
//used to show the results
@Published var sortedAndFiltered: [Mod_Prescription_DrugsList] = []
//used to store the list
@Published var sorted: [Mod_Prescription_DrugsList] = []
//filter string
@Published var prescriptionName: String = ""
//Load, sort and assign the items here

init(){
$prescriptionName
.debounce(for: 0.4, scheduler: RunLoop.main) //Wait for user to stop typing
.receive(on: DispatchQueue.global()) // perform filter on background
.map{[weak self] filterString in
guard filterString.count > 3, let self = self else{
return []
}
//n apply the filter
return self.sorted.filter {
$0.name
.lowercased()
.hasPrefix(
filterString.lowercased()
)
}
}
.receive(on: RunLoop.main) // switch back to uithread
.assign(to: &$sortedAndFiltered)
}


func load(){
sorted = Mod_Prescription_DrugsList.allDrugs.sorted { first, second in
first.name.compare(second.name, options: .numeric) == .orderedAscending
}
}

}
struct ContentView: View{

@FocusState var isFocused: Bool
@StateObject private var viewmodel: Viewmodel = Viewmodel()

var body: some View{
VStack{
if isFocused &&
viewmodel.sortedAndFiltered.count >= 3 {
ForEach(viewmodel.sortedAndFiltered, id: .self) { prescription in
Button {
isFocused = false
} label: {
VStack(alignment: .leading) {
Text(LocalizedStringKey(prescription.name))
}
.frame(maxWidth: .infinity, alignment: .leading)
} // End Label
.contentShape(Rectangle())
.padding()
} // End ForEach
} // End If
}.onAppear{ // I had to add a VStack to be able to do this just add it to the element surrounding your ForEach
// If list is empty load it in the background, so your app stays responsive
if viewmodel.sorted.count == 0{
Task{
viewmodel.load()
}
}
}
}
}

这是一种更通用的方法,因此您必须将此解决方案集成到您的工作中。我已经说了我认为必要的话。如果你有问题,不要犹豫,尽管问。我没有机会测试这个,所以可能有一些小的事情要解决。

最新更新