当我的应用主屏幕出现时,我正在使用 CloudKit 从 iCloud 获取大量 Tool
对象,并且我正在通过过滤器运行这些对象。 Tool
数组存储在名为 UserData
的 ObservableObject
类内的 ToolViewModel
对象 toolVM
中>.
这是我的 ViewModel 和我的 View 代码:
class ToolViewModel: InstrumentViewModel {
@Published var tools = [Tool]()
func filter(searchString: String, showFavoritesOnly: Bool) -> [Tool] {
let list = super.filter(searchString: searchString, showFavoritesOnly: showFavoritesOnly)
return list.map{ $0 as! Tool }.sorted()
}
func cleanUpCategories(from category: String) {
super.cleanUpCategories(instruments: tools, from: category)
}
}
struct ToolList: View {
// @ObservedObject var toolVM = ToolViewModel()
@ObservedObject var userData: UserData
@State private var searchString = ""
@State private var showCancelButton: Bool = false
var filteredTools: [Tool] {
userData.toolVM.filter(searchString: searchString, showFavoritesOnly: userData.showFavoritesOnly)
}
var body: some View {
NavigationView {
VStack {
// Search view
SearchView(searchString: $searchString, showCancelButton: $showCancelButton)
.padding(.horizontal)
// Tool list
List {
ForEach(filteredTools) { tool in
NavigationLink(destination: ToolDetail(tool: tool, userData: self.userData)) {
ToolRow(tool: tool)
}
} // ForEach ends
.onDelete(perform: onDelete)
} // List ends
} // VStack ends
.navigationBarTitle("Tools")
} // NavigationView ends
.onAppear() {
if self.userData.toolVM.tools.isEmpty {
// Tools (probably) haven't been loaded yet (or are really empty), so try it
self.userData.updateTools()
}
}
}
...
}
这是 ViewModel 的父类(super class)(因为我还有两个类似的 ViewModel,所以我介绍了这个):
class InstrumentViewModel: ObservableObject {
@Published var categories = [InstrumentCategory]()
@Published var instrumentCategories = [String: [Instrument]]()
func filter(searchString: String, showFavoritesOnly: Bool) -> [Instrument] {
var list = [Instrument]()
for category in categories {
if category.isSelected && instrumentCategories[category.name] != nil {
if searchString == "" {
list += showFavoritesOnly ? instrumentCategories[category.name]!.filter { $0.isFavorite } : instrumentCategories[category.name]!
} else {
list += showFavoritesOnly ? instrumentCategories[category.name]!.filter { $0.isFavorite && $0.contains(searchString: searchString) } : instrumentCategories[category.name]!.filter { $0.contains(searchString: searchString) }
}
}
}
return list
}
func setInstrumentCategories(instruments: [Instrument]) {
var categoryStrings = Set<String>()
for instrument in instruments {
categoryStrings.insert(instrument.category)
}
for categoryString in categoryStrings.sorted() {
categories.append(InstrumentCategory(name: categoryString))
}
}
}
这是我的 UserData 类:
final class UserData: ObservableObject {
@Published var showFavoritesOnly = false
@Published var toolVM = ToolViewModel()
func updateTools() {
DataHelper.loadFromCK(instrumentType: .tools) { (result) in
switch result {
case .success(let loadedInstruments):
self.toolVM.tools = loadedInstruments as! [Tool]
self.toolVM.setInstrumentCategories(instruments: loadedInstruments)
self.toolVM.instrumentCategories = Dictionary(grouping: self.toolVM.tools, by: { $0.category })
debugPrint("Successfully loaded instruments of type Tools and initialized categories and category dictionary")
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
}
}
最后但并非最不重要的一点是实际从 iCloud 加载数据的帮助器类:
struct DataHelper {
static func loadFromCK(instrumentType: InstrumentCKDataTypes, completion: @escaping (Result<[Instrument], Error>) -> ()) {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: instrumentType.rawValue, predicate: predicate)
getCKRecords(instrumentType: instrumentType, forQuery: query, completion: completion)
}
private static func getCKRecords(instrumentType: InstrumentCKDataTypes, forQuery query: CKQuery, completion: @escaping (Result<[Instrument], Error>) -> ()) {
CKContainer.default().publicCloudDatabase.perform(query, inZoneWith: CKRecordZone.default().zoneID) { results, error in
if let error = error {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
guard let results = results else { return }
switch instrumentType {
case .tools:
DispatchQueue.main.async { completion(.success(results.compactMap { Tool(record: $0) })) }
case .drivers:
DispatchQueue.main.async { completion(.success(results.compactMap { Driver(record: $0) })) }
case .adapters:
DispatchQueue.main.async { completion(.success(results.compactMap { Adapter(record: $0) })) }
}
}
}
}
我遇到的问题如下:
View 在从 iCloud 加载数据之前初始化。我使用空的 Tool
数组初始化 ViewModel 中的 tools
变量。因此,当 View 出现时,不会显示任何工具。
虽然 tools
是一个 @Published
变量,但异步 iCloud 加载过程完成后 View 不会重新加载。这是我所期望的行为。一旦我开始在搜索字段中输入一些搜索字符串,这些工具就会出现。这只是关于第一次加载。
在 UserData
初始化程序中初始化 toolVM
也是行不通的,因为我无法从异步加载闭包中访问它。
有趣的是:如果我将 toolVM
变量作为 @ObservedObject
移动到 View 本身中(您可以在我的 View 中看到它,我在代码中注释了这一行), View 将在数据加载完成后重新加载。不幸的是,这对我来说不是一个选择,因为我需要访问应用程序其他部分的 toolVM
ViewModel,因此我将其存储在我的 UserData 类中。
我认为这与异步加载有关。
最佳答案
ToolViewModel
引用未更改,因此在 UserData
级别未发布任何内容。这是可能的解决方案 - 有意强制发布:
DataHelper.loadFromCK(instrumentType: .tools) { (result) in
switch result {
case .success(let loadedInstruments):
self.toolVM.tools = loadedInstruments as! [Tool]
self.toolVM.setInstrumentCategories(instruments: loadedInstruments)
self.toolVM.instrumentCategories = Dictionary(grouping: self.toolVM.tools, by: { $0.category })
debugPrint("Successfully loaded instruments of type Tools and initialized categories and category dictionary")
self.objectWillChange.send() // << this !!
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
-
替代解决方案是将ToolList
View 的ToolViewModel
依赖部分分离为专用的较小 subview ,并观察到ToolViewModel
并从 userData
传递引用。
关于SwiftUI 和 CloudKit 异步数据加载,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62353354/