SwiftUI 和 CloudKit 异步数据加载
Posted
技术标签:
【中文标题】SwiftUI 和 CloudKit 异步数据加载【英文标题】:SwiftUI and CloudKit asynchronous data loading 【发布时间】:2020-06-12 22:27:23 【问题描述】:当我的应用程序的主屏幕出现时,我正在使用 CloudKit 从 iCloud 获取大量 Tool
对象,并且我正在通过过滤器运行这些对象。 Tool
数组存储在名为 UserData
的 ObservableObject
类中的 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 $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 的超类(因为我还有两个类似的 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) ))
我遇到的问题如下:
视图在从 iCloud 加载数据之前初始化。我用空的Tool
数组初始化 ViewModel 中的tools
变量。因此,视图出现时不会显示任何工具。
虽然tools
是一个@Published
变量,但在异步iCloud 加载过程完成后视图不会重新加载。这是我所期望的行为。一旦我开始在搜索字段中输入一些搜索字符串,工具就会出现。这只是关于第一次加载。
在 UserData
初始化器中初始化 toolVM
也不起作用,因为我无法从异步加载闭包中访问它。
有趣的是:如果我将toolVM
变量作为@ObservedObject
移动到视图本身(您可以在我的视图中看到它,我在代码中注释掉了这一行),视图将 数据加载完成后重新加载。不幸的是,这不是我的选择,因为我需要在应用程序的其他部分访问 toolVM
ViewModel,因此我将它存储在我的 UserData 类中。
我认为它与异步加载有关。
【问题讨论】:
【参考方案1】: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
视图的ToolViewModel
依赖部分分离到具有观察到的ToolViewModel
的专用较小子视图中,并从userData
传递引用。
【讨论】:
以上是关于SwiftUI 和 CloudKit 异步数据加载的主要内容,如果未能解决你的问题,请参考以下文章
如何将 UIImage 异步加载到 SwiftUI Image 中?
如何在新的 SwiftUI 应用生命周期中接受 CloudKit 共享?
使用 combine's Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像
使用 CloudKit + CoreData,如何在没有 SwiftUI 的 @FetchRequest 的情况下从远程云更新中更新 UI?