Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write an extension for NSFetchedResultsController in Swift 4

I'm trying to write a simple extension to the NSFetchedResultsController class in Swift 4.

Here's my first attempt - which worked in Swift 3:

public extension NSFetchedResultsController
{
    public func sectionCount() -> Int
    {
        if self.sections == nil
        {
            return 0
        }

        return self.sections!.count
    }
}

But I get this compile error in Xcode 9 beta 2 with Swift 4:

Extension of a generic Objective-C class cannot access the class's generic parameters at runtime

I've tried other variations to no avail. Note that I can create an extension bound to a specific type of NSManagedObject matching the resultType; but this has the disadvantage that I need to create an extension for every managed object type used with the NSFetchedResultsController.

The latest Swift 4 docs don't seem to explain this well.

like image 513
Daniel Avatar asked Jul 02 '17 01:07

Daniel


3 Answers

I think I found the answer to this one. The function needs to be available to Objective-C.

Adding @objc to the func should remove the compile error. At least it did for me!

like image 191
fisher Avatar answered Nov 19 '22 13:11

fisher


We ended up with subclass (Swift 4). Idea behind - pass type as argument. Rest of setup operates NSManagedObject type in order to work around compile errors.

public class FetchedResultsController<T: NSManagedObject>: NSFetchedResultsController<NSManagedObject> {

   public convenience init(fetchRequest: NSFetchRequest<NSManagedObject>, context: NSManagedObjectContext, _: T.Type) {
      self.init(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
   }

   public var numberOfSections: Int {
      return sections?.count ?? 0
   }

   public var numberOfFetchedEntities: Int {
      return fetchedObjects?.count ?? 0
   }

   public func entity(at indexPath: IndexPath) -> T {
      if let value = object(at: indexPath) as? T {
         return value
      } else {
         fatalError()
      }
   }

   public func entity(at row: Int) -> T {
      let ip = IndexPath(item: row, section: 0)
      return entity(at: ip)
   }

   public var fetchedEntities: [T] {
      let result = (fetchedObjects ?? []).compactMap { $0 as? T }
      return result
   }
}

Extensions:

extension Requests {

   public static func all<T: NSManagedObject>(_: T.Type) -> NSFetchRequest<NSManagedObject> {
      let request = NSFetchRequest<NSManagedObject>(entityName: T.entityName)
      return request
   }

   public static func ordered<T: NSManagedObject>(by: String, ascending: Bool, _: T.Type) -> NSFetchRequest<NSManagedObject> {
      let request = NSFetchRequest<NSManagedObject>(entityName: T.entityName)
      request.sortDescriptors = [NSSortDescriptor(key: by, ascending: ascending)]
      return request
   }

}


extension NSManagedObject {

    public static var entityName: String {
        let className = NSStringFromClass(self) // As alternative can be used `self.description()` or `String(describing: self)`
        let entityName = className.components(separatedBy: ".").last!
        return entityName
    }

    public static var entityClassName: String {
        let className = NSStringFromClass(self)
        return className
    }

}

Usage:

extension ParentChildRelationshipsMainController {

   private func setupFetchedResultsController() -> FetchedResultsController<IssueList> {
      let request = DB.Requests.ordered(by: #keyPath(IssueList.title), ascending: true, IssueList.self)
      let result = FetchedResultsController(fetchRequest: request, context: PCDBStack.shared.mainContext, IssueList.self)
      return result
   }
}

extension ParentChildRelationshipsMainController: NSTableViewDataSource, NSTableViewDelegate {

   func numberOfRows(in tableView: NSTableView) -> Int {
      return frController.numberOfFetchedEntities
   }

   func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
      let rowID = NSUserInterfaceItemIdentifier("rid:generic")
      let item = frController.entity(at: row)
      // ...
   }
}
like image 23
Vlad Avatar answered Nov 19 '22 13:11

Vlad


Subclass-ing is not good, composition is a better way to go. Here is my take on this:

public protocol FetchedDataProtocol {
    associatedtype T: NSFetchRequestResult
    var controller: NSFetchedResultsController<T> { get }
    subscript(_ indexPath: IndexPath) -> T { get }
}

Doing it this way you avoid casting and mapping:

public extension FetchedDataProtocol {
    subscript(_ indexPath: IndexPath) -> T {
        return controller.object(at: indexPath)
    }
}

And then you use it as your own class that has subscript (and whatever you want):

public struct MainFetchedData<T: NSFetchRequestResult>: FetchedDataProtocol {
    public let controller: NSFetchedResultsController<T>
    public init(_ controller: NSFetchedResultsController<T>) {
        self.controller = controller
    }
}
like image 1
Lazar Norris Avatar answered Nov 19 '22 15:11

Lazar Norris