Put Usage of a CoreDataFetchRequest out of Your Code and Into ... Where Exactly?

I created a generic CoreDataFetchRequest a while ago and love it.

It casts the results to the expected values or throws an error if something went wrong, which it shouldn’t ever, logically – but the Core Data API currently returns AnyObject everywhere, so I have to deal with that.

I have found three ways to use it. I’d like to know which one you prefer.

  1. Using the CoreDataFetchRequest directly
  2. Using a CoreDataStore to encapsulate using the requests
  3. Extending the CoreDataFetchRequest with behavior

This is the managed object I’m going to query:

@objc(Banana)
class Banana: NSManagedObject {

    static let entityName = "Banana"

    static func fetchRequest() -> CoreDataFetchRequest<Banana> {
    
        return CoreDataFetchRequest<Banana>(entityName: entityName)
    }
}

1: Using CoreDataFetchRequest as a wrapper

Here’s how it’s used directly:

func getAllBananas(context: NSManagedObjectContext) -> [Banana] {
    
    let request = Banana.fetchRequest()
    let results: [Banana]
    
    do {
        results = try request.executeInContext(self.context)
    } catch let error as CoreDataError {

        switch error {
        case .InconsistentCoreDataFetchRequestResults: 
            fatalError("Expected [Banana] but got mixed results.")
        }

        return []
    } catch let error as NSError {

        fatalError("Core Data error: \(error)")
        return []
    }
    
    return results
}

So I thought maybe I should wrap that up. CoreDataFetchRequest clients are “stores” or “repositories”. So I created a protocol for them.

2: Hide CoreDataFetchRequest in a CoreDataStore

Using a CoreDataStore-compliant object works like that:

protocol BananaReader {
    func allBananas() -> [Banana]
}

class CoreDataBananaReader {
    let context: NSManagedObjectContext

    init(context: NSManagedObjectContext) {
        self.context = context
    }
}

extension CoreDataBananaReader: CoreDataStore { }

extension CoreDataBananaReader: BananaReader {
    
    func allBananas() -> [Banana] {
        
        let request = Banana.fetchRequest()
        return resultOfFetchRequest(request)
    }
}

Simplified, this is what the CoreDataStore looks like:

protocol CoreDataRepository {

    var context: NSManagedObjectContext { get }

    func resultOfFetchRequest<T>(request: CoreDataFetchRequest<T>) -> [T]
    func firstResultOfFetchRequest<T>(request: CoreDataFetchRequest<T>) -> T?
}

extension CoreDataRepository {

    func resultOfFetchRequest<T>(request: CoreDataFetchRequest<T>) -> [T] {
    
        let results: [T]
    
        do {
            results = try request.executeInContext(context)
        } catch let error as CoreDataError {
        
            switch error {
            case .InconsistentCoreDataFetchRequestResults: 
                fatalError("Expected [\(T.self)] but got mixed results.")
            }
        
            return []
        } catch let error as NSError {
            fatalError("Core Data error: \(error)")
            return []
        }
    
        return results
    }

    func firstResultOfFetchRequest<T>(request: CoreDataFetchRequest<T>) -> T? {
    
        return resultOfFetchRequest(request).first
    }
}

So maybe put that stuff into the CoreDataFetchRequest itself?

3: Extending CoreDataFetchRequest to do something on its own

Using an extension is short and simple. The basic reader protocol stays the same:

protocol BananaReader {
    func allBananas() -> [Banana]
}

class CoreDataBananaReader {
    let context: NSManagedObjectContext

    init(context: NSManagedObjectContext) {
        self.context = context
    }
}

extension CoreDataBananaReader: BananaReader {
    
    func allBananas() -> [Banana] {
        
        return Banana.fetchRequest().allResultsInContext(context)
    }
}

Scratch the CoreDataStore protocol and instead add this:

extension CoreDataFetchRequest {

    func allResults(context context: NSManagedObjectContext) -> [T] {
    
        let results: [T]
    
        do {
            results = try executeInContext(context)
        } catch let error as CoreDataError {
        
            switch error {
            case .InconsistentCoreDataFetchRequestResults: 
                fatalError("Expected [\(T.self)] but got mixed results.")
            }
        
            return []
        } catch let error as NSError {
            fatalError("Core Data error: \(error)")
            return []
        }
    
        return results
    }    
}

So, what do you prefer? Any arguments pro 2 and contra 3?

Browse the blog archive