SwiftUI 和 CloudKit 异步数据加载
SwiftUI and CloudKit asynchronous data loading
当我的应用程序的主屏幕出现时,我使用 CloudKit 从 iCloud 获取大量 Tool
对象,并且我通过过滤器 运行 这些对象。 Tool
数组存储在名为 UserData
.
的 ObservableObject
class 中的 ToolViewModel
对象 toolVM
中
这是我的 ViewModel 和我的视图代码:
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{ [=11=] 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的superclass(因为我还有两个类似的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 { [=12=].isFavorite } : instrumentCategories[category.name]!
} else {
list += showFavoritesOnly ? instrumentCategories[category.name]!.filter { [=12=].isFavorite && [=12=].contains(searchString: searchString) } : instrumentCategories[category.name]!.filter { [=12=].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))
}
}
}
这是我的用户数据class:
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: { [=13=].category })
debugPrint("Successfully loaded instruments of type Tools and initialized categories and category dictionary")
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
}
}
最后但同样重要的是,实际上从 iCloud 加载数据的助手 class:
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: [=14=]) })) }
case .drivers:
DispatchQueue.main.async { completion(.success(results.compactMap { Driver(record: [=14=]) })) }
case .adapters:
DispatchQueue.main.async { completion(.success(results.compactMap { Adapter(record: [=14=]) })) }
}
}
}
}
我遇到的问题如下:
视图在从 iCloud 加载数据之前初始化。我用空 Tool
数组初始化 ViewModel 中的 tools
变量。因此,当视图出现时不显示任何工具。
虽然 tools
是一个 @Published
变量,但在异步 iCloud 加载过程完成后,视图不会重新加载。这是我期望的行为。只要我开始在搜索字段中输入一些搜索字符串,工具就会出现。这只是第一次加载。
在 UserData
初始化器中初始化 toolVM
也是行不通的,因为我无法从异步加载闭包中访问它。
有趣:如果我将 toolVM
变量作为 @ObservedObject
移动到视图本身(您可以在我的视图中看到它,我在代码中注释掉了这一行),视图 将在数据加载完成后重新加载。不幸的是,这对我来说不是一个选择,因为我需要在应用程序的其他部分访问 toolVM
ViewModel,因此我将它存储在我的 UserData class.
中
我假设它与异步加载有关。
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: { [=10=].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
视图的 ToolViewModel
依赖部分分离到观察到的 ToolViewModel
的专用较小子视图中,并在中传递引用它来自 userData
.
当我的应用程序的主屏幕出现时,我使用 CloudKit 从 iCloud 获取大量 Tool
对象,并且我通过过滤器 运行 这些对象。 Tool
数组存储在名为 UserData
.
ObservableObject
class 中的 ToolViewModel
对象 toolVM
中
这是我的 ViewModel 和我的视图代码:
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{ [=11=] 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的superclass(因为我还有两个类似的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 { [=12=].isFavorite } : instrumentCategories[category.name]!
} else {
list += showFavoritesOnly ? instrumentCategories[category.name]!.filter { [=12=].isFavorite && [=12=].contains(searchString: searchString) } : instrumentCategories[category.name]!.filter { [=12=].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))
}
}
}
这是我的用户数据class:
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: { [=13=].category })
debugPrint("Successfully loaded instruments of type Tools and initialized categories and category dictionary")
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
}
}
最后但同样重要的是,实际上从 iCloud 加载数据的助手 class:
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: [=14=]) })) }
case .drivers:
DispatchQueue.main.async { completion(.success(results.compactMap { Driver(record: [=14=]) })) }
case .adapters:
DispatchQueue.main.async { completion(.success(results.compactMap { Adapter(record: [=14=]) })) }
}
}
}
}
我遇到的问题如下:
视图在从 iCloud 加载数据之前初始化。我用空 Tool
数组初始化 ViewModel 中的 tools
变量。因此,当视图出现时不显示任何工具。
虽然 tools
是一个 @Published
变量,但在异步 iCloud 加载过程完成后,视图不会重新加载。这是我期望的行为。只要我开始在搜索字段中输入一些搜索字符串,工具就会出现。这只是第一次加载。
在 UserData
初始化器中初始化 toolVM
也是行不通的,因为我无法从异步加载闭包中访问它。
有趣:如果我将 toolVM
变量作为 @ObservedObject
移动到视图本身(您可以在我的视图中看到它,我在代码中注释掉了这一行),视图 将在数据加载完成后重新加载。不幸的是,这对我来说不是一个选择,因为我需要在应用程序的其他部分访问 toolVM
ViewModel,因此我将它存储在我的 UserData class.
我假设它与异步加载有关。
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: { [=10=].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
视图的 ToolViewModel
依赖部分分离到观察到的 ToolViewModel
的专用较小子视图中,并在中传递引用它来自 userData
.