5

我一直在寻找答案,但运气不佳。这个问题几乎相同,但答案不是很清楚(至少对我来说!): 它是 VIPER 架构中 NSFetchedResultsController 的位置?

NSFetchedResultsController 似乎对于 iOS 应用程序来说是一种非常有用的方法,但我所看到的所有示例都将其放在 ViewController 层——至少,VC 成为了一个委托。在 Clean Architecture/Viper 中,模型层与视图层非常脱节,我无法弄清楚 NSFRC 是如何在这样的架构中使用的。上述问题的答案暗示交互者应该是一个代表,但这没有意义——托管对象随后将呈现给交互者,而不是 PONSO。也许我还不太了解它,但是(a)它是否在清洁架构中占有一席之地?(b) 如果是,那么是否需要正确的 Swift 实现模式?

4

3 回答 3

4

这就是我最后所做的。NSFetchedResultsController (NFRC) 需要以两种方式处理 - 获取数据,即执行查询,以及通过委托调用通知 ManagedObject (MO) 集的更改。

获取数据不会触发委托调用。因此,您通常会返回运行 fetch 的结果,即 anNFRC.fetchedObjects(),在 worker 或 interactor 中重新打包为 PONSOS,并将这些传递给 Presenter 以传递给 ViewController。

我发现使用 DataSource Delegate 和 ViewController 一样容易且符合规范(当 Table View 是实现的一部分时)——我将它实现为 ViewController 的单独类。

这种方法保持标准的 VIP 周期,并且不需要视图层中的模型知识。

处理委托调用有点棘手。NFRC 通常与 View 层绑定以处理 Table View 数据委托请求:NFRC 通知插入、删除、移动、更新更改,并且委托适当地处理它。但是,在 VIP 架构中,由于 NFRC 无法附加到视图,因此无法发生 - 它位于模型层并需要留在那里。

我在 Store 实例中对此进行了实例化,并使 Store 实例成为 NFRC 委托,并将委托方法实现为:

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
print("item changed")
        guard let managedItem = anObject as? ManagedItem else {
            return
        }
        let item = managedItem.toItem()
        var eventType: EventType
        switch type {
        case .insert:
            eventType = EventType.insert
        case .delete:
            eventType = EventType.delete
        case .move:
            eventType = EventType.move
        case .update:
            eventType = EventType.update
        }

        let itemChangeEvent = ItemChangeEvent(eventType: eventType, item: item, index: indexPath, newIndex: newIndexPath)
        results.append(itemChangeEvent)
    }

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        results = []
        print ("Begin update")
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        print("End updates")
        if let completionHandler = completion {
            completionHandler(results)
        }
    }

基本上,我初始化一个空数组(开始更新),将所有通知作为事件对象(PO​​NSOS)整理到该数组(I,D,M,U)中,然后在完成时运行完成处理程序(结束更新)。完成处理程序作为 fetch() 操作的一部分传入并存储以供将来使用 - 即当需要通知 MO 更改时。完成处理程序从 Interactor 传递过来,如下所示:

    func processFetchResults(itemChangeEvents: [ItemChangeEvent]) {
    let response = ListItems.FetchItems.Response(itemEvents: itemChangeEvents)
    presenter?.presentFetchedItems(response: response)
}

因此,它将所有事件传递给 Presenter,Presenter 传递给可以处理它们的 Data Source Delegate。

然而,这还不够。为了提高效率,数据源委托确实需要与 NSFRC 交互,以将表视图行映射到正确索引路径上的数据行,处理部分信息等。因此,我所做的是创建一个名为 DynamicDataSource 的协议和一个由交互器初始化以“包装” NSFRC 并代理其方法的实现。虽然模型在技术上被交给了视图层,但它实际上被包装在协议后面,并且实现将 MO 转换为 PONSOS。所以我认为它是 Presenter 层的扩展(不是 Swift 扩展!)。

    protocol ListItemsDynamicDataSource: AnyObject {
    // MARK: - Helper methods
    func numberOfSections() -> Int
    func rowsInSection(_ section: Int) -> Int
    func getItem(index: IndexPath) -> ListItems.FetchItems.ViewModel.DisplayedItem
}

如果将持久性存储更改为内存存储或 JSON 层,则动态数据源实现可以适当地处理它而不影响视图。显然这是使用 NFRC 的一种复杂方式,但我认为这是一个有用的类。对于一个简单的应用程序,这可能是矫枉过正。但是,它可以工作,而且我认为这是一个很好的、符合要求的折衷方案。

值得补充的是,我对 Swift 和 IOS 开发还很陌生,所以这可能不是世界上最好的代码,而且可能有更好的方法!我总是乐于接受反馈和改进建议。

于 2017-08-11T14:46:01.617 回答
1

您可以将 NSFetchedResultController 视为控制器或适配器。

我不同意 ad-johnson 的 NSFRC 属于模型。

Clean Architecture 中的依赖规则说依赖必须指向内部。它并不是说用例或适配器应该不知道模型层。

于 2019-09-23T09:17:18.963 回答
0

也许 NSFetchedResultsController 在 iOS 项目中使数据与视图层之间的耦合最紧密。为了解决这个问题,我在 NSFetchedResultsController 周围使用了一个包装器,其方式与 @ad-johnson 解释的类似。我还创建了一个 Xcode 模板,可以在项目中自动为我创建所需的协议。

我想我已经成功地通过边界将 NSFetchedResultsController 与应用程序的其他层完全分开,而不会丢失 NSFetchedResultController 例程功能。因此,我可以轻松地测试、修改甚至替换所有层,因为层之间存在松散耦合。

如果你想了解更多, GitHub 上有一个名为CarOwnership的项目,它描述了如何在一个干净的架构中处理 NSFetchedResultsController。您也可以在我的博客中找到它。

于 2020-05-16T15:50:46.623 回答