SwiftUI 和 CloudKit 异步数据加载

标签 swiftui cloudkit

当我的应用主屏幕出现时,我正在使用 CloudKit 从 iCloud 获取大量 Tool 对象,并且我正在通过过滤器运行这些对象。 Tool 数组存储在名为 UserDataObservableObject 类内的 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/

相关文章:

ios - 如何在 swiftUI 中呈现 crossDissolve View ?

swift - SwiftUI 中文本的自定义字体大小

SwiftUI Tabbar 在选项卡的 View 中保留 navigationBarTitle 位置

unit-testing - 为单元测试创​​建模拟云工具包对象

ios - 更改 iCloud 帐户时 NSUbiquitousKeyValueStore 同步延迟

ios - discoverAllContactUserInfosWithCompletionHandler 结果空白

ios - 如何在 SwiftUI 中迭代/foreach 数组

layout - 如何在 SwiftUI 中对齐底部文本

ios - 使用 watchOS 3 时出现 CloudKit 错误

ios - CloudKit 订阅有时不起作用