//===----------------------------------------------------------------------===// // // This source file is part of the SwiftLibs open source project // // Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors // Licensed under the EUPL 1.2 or later. // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftLibs project authors // //===----------------------------------------------------------------------===// #if canImport(Combine) && canImport(CoreData) import Combine import CoreData /// This class fetches objects from a given managed object context and it notifies of changes in the object fetched if any. public class Fetcher: NSObject, NSFetchedResultsControllerDelegate { // MARK: Properties /// The publisher that emits the changes detected to the Location entities in a given object context. public let didChangePublisher = PassthroughSubject<[Change], Never>() private let fetchedResultsController: NSFetchedResultsController /// The number of sections in the data. public var numberOfSections: Int { fetchedResultsController.sections?.count ?? 0 } private var changesToNotify: [Change] = [] // MARK: Initialisers /// Initialises the fetcher give the given parameters. /// - Parameters: /// - fetchRequest: The fetch request to use to get the objects. /// - managedObjectContext: The managed object context against the fetch request is executed. /// - sectionNameKeyPath: A key path on result objects that returns the section name. /// - cacheName: The name of the cache file the receiver should use. public init( fetchRequest: NSFetchRequest, managedObjectContext: NSManagedObjectContext, sectionNameKeyPath: String? = nil, cacheName: String? = nil ) { self.fetchedResultsController = .init( fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: cacheName ) super.init() self.fetchedResultsController.delegate = self } // MARK: Functions /// Perform the fetching. public func fetch() throws { try fetchedResultsController.performFetch() } /// Retrieve the number of objects in a given section number. /// - Parameter section: The section number to inquiry about. /// - Returns: A number of objects in the given section number. public func numberOfObjects(in section: Int) throws -> Int { guard let sections = fetchedResultsController.sections else { throw FetcherError.fetchNotExecuted } guard sections.endIndex > section else { throw FetcherError.sectionNotFound } return sections[section].numberOfObjects } /// Retrieve an object out of a given index path. /// - Parameter indexPath: The index path to use to retrieve an object. /// - Returns: A `NSManagedObject` entity positioned in the given index path. public func object(at indexPath: IndexPath) throws -> Model { guard fetchedResultsController.sections != nil else { throw FetcherError.fetchNotExecuted } return fetchedResultsController.object(at: indexPath) } // MARK: NSFetchedResultsControllerDelegate public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { changesToNotify.removeAll() } public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { didChangePublisher.send(changesToNotify) } public func controller( _ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType ) { if type == .insert { changesToNotify.append(.section(.inserted(sectionIndex))) } else if type == .delete { changesToNotify.append(.section(.deleted(sectionIndex))) } } public func controller( _ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath? ) { switch type { case .insert: guard let newIndexPath else { return } changesToNotify.append(.object(.inserted(at: newIndexPath))) case .delete: guard let indexPath else { return } changesToNotify.append(.object(.deleted(from: indexPath))) case .move: guard let indexPath, let newIndexPath else { return } changesToNotify.append(.object(.moved(from: indexPath, to: newIndexPath))) case .update: guard let indexPath else { return } changesToNotify.append(.object(.updated(at: indexPath))) default: break } } } // MARK: - Errors public enum FetcherError: Error { case fetchNotExecuted case sectionNotFound } // MARK: - Enumerations public enum Change: Hashable { public enum SectionUpdate: Hashable { case inserted(Int) case deleted(Int) } public enum ObjectUpdate: Hashable { case inserted(at: IndexPath) case deleted(from: IndexPath) case updated(at: IndexPath) case moved(from: IndexPath, to: IndexPath) } case section(SectionUpdate) case object(ObjectUpdate) } #endif