Javier Cicchelli c0a49b2a85 [Feature] Persistence (#5)
This PR contains the work done to implement some useful protocols, classes and extensions to use to setup a persistence layer in an application.

To provide further details about the work done:
 - [x] declared the `Persistence` target in the Package file;
 - [x] forgot to declare the `Communications` and `Persistence` target to the `SwiftLibs` library in the `Package` file;
 - [x] defined the `Service` public protocol;
 - [x] implemented the `Fetcher` generic class;
 - [x] implemented the `bitBucket` static property in the `URL+Devices` public extension;
 - [x] updated the `README` file.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: #5
2023-04-17 23:12:11 +00:00

166 lines
5.3 KiB
Swift

//
// Fetcher.swift
// Persistence
//
// Created by Javier Cicchelli on 16/04/2023.
// Copyright © 2023 Röck+Cöde. All rights reserved.
//
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<Model: NSManagedObject>: 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<Model>
/// 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<Model>,
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<NSFetchRequestResult>) {
changesToNotify.removeAll()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
didChangePublisher.send(changesToNotify)
}
public func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
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<NSFetchRequestResult>,
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)
}