Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple NSFetchedResultsControllers in Swift 3

I have a class in Swift that wraps multiple NSFetchedResultsControllers, becomes their delegate, and converts the IndexPaths before returning to it's own delegate. This class can take NSFetchedResultsControllers returning different entities as long as they conform to the same Protocol. When upgrading to Swift 3 I can't get this same functionality to compile.

Let say I want to wrap two NSFetchedResultsControllers returning two different Entity types to show in a single tableView. Both CoreData entities conform to the following protocol

protocol ManagedObjectDisplayType : NSFetchRequestResult {
    var id:String { get }
    func friendlyName() -> String
}

The problem is that now NSFetchedResultsControllers are generic, there is no concrete type of NSFetchedResultsController that I can pass in to my Wrapper class since the two controllers are of different types.

For example:

 let entity1Request = NSFetchRequest<Entity1>(entityName: entityName)
 let entity1Frc = NSFetchedResultsController<ManagedObjectDisplayType>(fetchRequest: entity1Request, managedObjectContext:mainManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
 let entity2Request = NSFetchRequest<Entity2>(entityName: entityName)
 let entity2Frc = NSFetchedResultsController<ManagedObjectDisplayType>(fetchRequest: entity2Request, managedObjectContext:mainManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

When I do this I get the following error: "Using 'ManagedObjectDisplayType' as a concrete type conforming to protocol 'NSFetchRequestResult' is not supported" which makes total sense.

But I'm not sure of another way to do what I'm trying to do.

like image 506
Nathan Hart Avatar asked Nov 01 '16 17:11

Nathan Hart


1 Answers

This looks like a job for...

enter image description here

Type Erasure!

There has been lots written about type erasure in Swift, but this is an interesting situation. I'm assuming you're having problems creating an object that can hold multiple NSFetchedResultsController for your different types. The problem is that Swift (in its current version) needs a concrete type to correctly allocate the needed memory. The standard solution to this in the standard library and elsewhere is to create a box that hides the underlying type. In this case, the AnyFetchedResultsController class below effectively erases the concrete type (an NSFetchedResultsController<T> where T: ManagedObjectDisplayType) that it boxes.

class AnyFetchedResultsController: CustomDebugStringConvertible
{
    var descImpl: () -> String
    var performImpl: () throws -> ()

    init<T>(_ controller: NSFetchedResultsController<T>) where T: ManagedObjectDisplayType {    
        descImpl = { controller.debugDescription }
        performImpl = { try controller.performFetch() }
    }

    func performFetch() throws {
        try performImpl()
    }

    var debugDescription: String {
        return "wrapping \(descImpl())"
    }
}

let entity1Request = NSFetchRequest<Entity1>(entityName: "Foobar")
let entity1Frc = NSFetchedResultsController<Entity1>(fetchRequest: entity1Request, managedObjectContext:mainManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
let entity2Request = NSFetchRequest<Entity2>(entityName: "Barfoo")
let entity2Frc = NSFetchedResultsController<Entity2>(fetchRequest: entity2Request, managedObjectContext:mainManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)


let frcs: [AnyFetchedResultsController] = [AnyFetchedResultsController(entity1Frc), AnyFetchedResultsController(entity2Frc)]

Now that you have a way of storing these fetched results controllers, you'll need to flesh out the AnyFetchedResultsController class with any additional methods you need to call on the underlying NSFetchedResultsController.

I hope that makes sense. Please come back if you have further questions!

Edit

The original signature for AnyFetchedResultsController.init:

init<T, U: NSFetchedResultsController<T>>(_ controller: U, _ managedObjectType: T? = nil) where T: ManagedObjectDisplayType

was pretty complicated, and had a dummy managedObjectType parameter which seemed to be necessary to fix some compiler errors. However, I just discovered that the more simple:

init<T>(_ controller: NSFetchedResultsController<T>) where T: ManagedObjectDisplayType

(also above) seems to work just as well.

like image 158
Dave Weston Avatar answered Sep 28 '22 09:09

Dave Weston