Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use of NSFetchedResultsController in Clean Architecture

I've searched for an answer to this without much luck. This question is pretty much the same but the answer isn't very clear (at least to me!): Which it is the place for NSFetchedResultsController in VIPER architecture?

The NSFetchedResultsController seems like a very useful approach for iOS apps but all the examples I've seen place this very much at the ViewController layer - at least, the VC becomes a delegate. In a Clean Architecture/Viper, the model layer is very much disconnected from the View layer and I can't figure out how the NSFRC is used in such an architecture. The answer to the above question implies that the Interactor should be a delegate but that doesn't make sense - the Managed Objects would then be surfaced to the Interactor, rather than PONSOs. Perhaps I don't understand it well enough yet, but (a) does it have a place in a Clean Architecture; and (b) if it does, then wants the right Swift implementation pattern?

like image 852
ad-johnson Avatar asked Jul 27 '17 22:07

ad-johnson


2 Answers

This is what I did in the end. NSFetchedResultsController (NFRC)needs to be approached in two ways - fetching data, i.e. executing the query, and notifications of changes to the ManagedObject (MO)set via delegate calls.

Fetching data does not fire delegate calls. So, you would normally return the results of the running the fetch, i.e. anNFRC.fetchedObjects(), re-package as PONSOS in the worker or interactor and pass these to the Presenter to pass to the ViewController.

I found it easier and just as conformant to use the DataSource Delegate as the ViewController (when a Table View is part of the implementation) - I implement that as a separate class to the ViewController.

This approach maintains the standard VIP cycle and requires no model knowledge in the View Layer.

Handling delegate calls is a little more tricky. The NFRC is typically tied to the View layer to handle Table View data delegate requests: the NFRC notifies of Insert, Delete, Move, Update changes and the delegate handles it appropriately. However, in a VIP architecture that can't happen as the NFRC cannot be attached to the View - it lives in the Model layer and needs to stay there.

I instantiated this in the Store instance and made the Store instance a NFRC delegate and implemented the delegate methods as:

    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)
        }
    }

Basically, I initialise an empty array (Begin Update), collate all the notifications as event objects (PONSOS) into that array (I,D,M,U), then run a completion handler when finished (End Update). The completion handler is passed in as part of the fetch() operation and stored for future use - i.e. for when MO changes need to be notified. The completion handler is passed through from the Interactor and looks like:

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

So it passes all the events to the Presenter which passes to the Data Source Delegate which can process them.

However that's not enough. To be efficient, the Data Source delegate really needs to interact with the NSFRC to map a table view row to a data row at the right index path, handle section info etc. What I did, therefore, was create a protocol called DynamicDataSource and an implementation of that which is initialised by the Interactor to 'wrap' the NSFRC and proxy its methods. Whilst the model is technically handed to the View layer, it's actually wrapped behind a protocol and the implementation converts MOs to PONSOS. So I see it as an extension (not a Swift extension!) of the Presenter layer.

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

If a persistence store was changed to, say, a Memory Store or JSON layer then the Dynamic Data Source implementation could handle that appropriately without affecting the View. Clearly this is a complex way of using the NFRC but I think it's a useful class to use. For a simple app, it's overkill probably. However, it works, and I think it's a good, conformant compromise.

It's worth adding that I'm pretty new to Swift and IOS development so this may not be the best code in the world and there might be better ways of doing it! I'm always open to feedback and suggestions for improvement.

like image 148
ad-johnson Avatar answered Nov 05 '22 06:11

ad-johnson


You can see the NSFetchedResultController as a controller or adapter.

I don't agree with ad-johnson that the NSFRC belong in model.

The Dependency Rule in Clean Architecture says that dependencies must point inward. It doesn't say that use cases or adapters should be unaware of the model layer.

like image 42
Mycroft Canner Avatar answered Nov 05 '22 08:11

Mycroft Canner