SwiftUI 和 CloudKit 异步数据加载

Posted

技术标签:

【中文标题】SwiftUI 和 CloudKit 异步数据加载【英文标题】:SwiftUI and CloudKit asynchronous data loading 【发布时间】:2020-06-12 22:27:23 【问题描述】:

当我的应用程序的主屏幕出现时,我正在使用 CloudKit 从 iCloud 获取大量 Tool 对象,并且我正在通过过滤器运行这些对象。 Tool 数组存储在名为 UserDataObservableObject 类中的 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 异步数据加载的主要内容,如果未能解决你的问题,请参考以下文章

Cloudkit 与组合

如何将 UIImage 异步加载到 SwiftUI Image 中?

如何在新的 SwiftUI 应用生命周期中接受 CloudKit 共享?

使用 combine's Publisher 在 SwiftUI Image 中从远程 URL 异步加载图像

使用 CloudKit + CoreData,如何在没有 SwiftUI 的 @FetchRequest 的情况下从远程云更新中更新 UI?

在 SwiftUI 视图中结合 onChange 和 onAppear 事件?