[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
This commit is contained in:
parent
cd47043a30
commit
c0a49b2a85
@ -20,9 +20,11 @@ let package = Package(
|
||||
.library(
|
||||
name: "SwiftLibs",
|
||||
targets: [
|
||||
"Communications",
|
||||
"Coordination",
|
||||
"Core",
|
||||
"Dependencies"
|
||||
"Dependencies",
|
||||
"Persistence"
|
||||
]
|
||||
),
|
||||
],
|
||||
@ -46,6 +48,10 @@ let package = Package(
|
||||
name: "Dependencies",
|
||||
dependencies: []
|
||||
),
|
||||
.target(
|
||||
name: "Persistence",
|
||||
dependencies: []
|
||||
),
|
||||
// MARK: Test targets
|
||||
.testTarget(
|
||||
name: "CommunicationsTests",
|
||||
@ -76,6 +82,13 @@ let package = Package(
|
||||
],
|
||||
path: "Tests/Dependencies"
|
||||
),
|
||||
.testTarget(
|
||||
name: "PersistenceTests",
|
||||
dependencies: [
|
||||
"Persistence"
|
||||
],
|
||||
path: "Tests/Persistence"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -11,3 +11,4 @@ Currently, this package contains the following libraries:
|
||||
* `Coordination`: protocols to implement the [Coordinator pattern](https://khanlou.com/2015/01/the-coordinator/) and some ready-to-use platform-specific concrete routers;
|
||||
* `Core`: extensions we usually add to the base layer functionality and primitive types provided by the [Swift standard library](https://https://www.swift.org/documentation/#standard-library);
|
||||
* `Dependencies`: a ready-to-use, simple [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) mechanism that levers heavily on the [dynamic property wrappers](https://www.hackingwithswift.com/plus/intermediate-swiftui/creating-a-custom-property-wrapper-using-dynamicproperty) provided by the [Swift programming language](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Projecting-a-Value-From-a-Property-Wrapper);
|
||||
* `Persistence`: protocols, extensions and a ready-to-use fetcher class to simplify the building of the [CoreData](https://developer.apple.com/documentation/coredata) persistence layer;
|
||||
|
165
Sources/Persistence/Classes/Fetcher.swift
Normal file
165
Sources/Persistence/Classes/Fetcher.swift
Normal file
@ -0,0 +1,165 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
18
Sources/Persistence/Extensions/URL+Devices.swift
Normal file
18
Sources/Persistence/Extensions/URL+Devices.swift
Normal file
@ -0,0 +1,18 @@
|
||||
//
|
||||
// URL+Devices.swift
|
||||
// Persistence
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension URL {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// URL to a virtual device in which any data written vanishes or disappear.
|
||||
static let bitBucket = URL(fileURLWithPath: "/dev/null")
|
||||
|
||||
}
|
36
Sources/Persistence/Protocols/Service.swift
Normal file
36
Sources/Persistence/Protocols/Service.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Service.swift
|
||||
// Persistence
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
public protocol Service {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The main managed object context.
|
||||
var viewContext: NSManagedObjectContext { get }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Create a private queue context.
|
||||
/// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use.
|
||||
func makeTaskContext() -> NSManagedObjectContext
|
||||
|
||||
/// Create a child context of the view context.
|
||||
/// - Returns: A generated child `NSManagedObjectContext` context instance ready to use.
|
||||
func makeChildContext() -> NSManagedObjectContext
|
||||
|
||||
/// Save a given context.
|
||||
/// - Parameter context: A `NSManagedObjectContext` context instance to save.
|
||||
func save(context: NSManagedObjectContext) throws
|
||||
|
||||
/// Save a given child context as well as its respective parent context.
|
||||
/// - Parameter context: A child `NSManagedObjectContext` context instance to save.
|
||||
func save(childContext context: NSManagedObjectContext) throws
|
||||
|
||||
}
|
319
Tests/Persistence/Classes/FetcherTests.swift
Normal file
319
Tests/Persistence/Classes/FetcherTests.swift
Normal file
@ -0,0 +1,319 @@
|
||||
//
|
||||
// FetcherTests.swift
|
||||
// PersistenceTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Persistence
|
||||
import XCTest
|
||||
|
||||
final class FetcherTests: XCTestCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private lazy var persistence: TestPersistenceService = .shared
|
||||
private lazy var fetcher: Fetcher<TestEntity> = .init(
|
||||
fetchRequest: .allTestEntities(),
|
||||
managedObjectContext: persistence.viewContext
|
||||
)
|
||||
|
||||
// MARK: Setup
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
try persistence.clean()
|
||||
}
|
||||
|
||||
// MARK: Number of sections tests
|
||||
|
||||
func test_numberOfSections_whenModelIsEmpty() throws {
|
||||
// GIVEN
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN
|
||||
let numberOfSections = fetcher.numberOfSections
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(numberOfSections, 1)
|
||||
}
|
||||
|
||||
func test_numberOfSections_whenModelIsFilled() throws {
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let _ = [
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context)
|
||||
]
|
||||
|
||||
try persistence.save(childContext: context)
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN
|
||||
let numberOfSections = fetcher.numberOfSections
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(numberOfSections, 1)
|
||||
}
|
||||
|
||||
func test_numberOfSections_whenNoFetch() async throws {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
let numberOfSections = fetcher.numberOfSections
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(numberOfSections, 0)
|
||||
}
|
||||
|
||||
// MARK: Number of objects tests
|
||||
|
||||
func test_numberOfObjects_inFirstSection_whenModelIsEmpty() throws {
|
||||
// GIVEN
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN
|
||||
let section = fetcher.numberOfSections - 1
|
||||
let numberOfObjects = try fetcher.numberOfObjects(in: section)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(numberOfObjects, 0)
|
||||
}
|
||||
|
||||
func test_numberOfObjects_inFirstSection_whenModelIsFilled() throws {
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let entities = [
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context)
|
||||
]
|
||||
|
||||
try persistence.save(childContext: context)
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN
|
||||
let section = fetcher.numberOfSections - 1
|
||||
let numberOfObjects = try fetcher.numberOfObjects(in: section)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(numberOfObjects, entities.count)
|
||||
}
|
||||
|
||||
func test_numberOfObjects_inNonExistingSection() throws {
|
||||
// GIVEN
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN & THEN
|
||||
let section = fetcher.numberOfSections
|
||||
|
||||
XCTAssertThrowsError(try fetcher.numberOfObjects(in: section)) { error in
|
||||
XCTAssertEqual(error as? FetcherError, .sectionNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func test_numberOfObjects_whenNoFetch() throws {
|
||||
// GIVEN
|
||||
// WHEN & THEN
|
||||
XCTAssertThrowsError(try fetcher.numberOfObjects(in: 1)) { error in
|
||||
XCTAssertEqual(error as? FetcherError, .fetchNotExecuted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Object at tests
|
||||
|
||||
func test_objectAt_whenModelIsEmpty() throws {
|
||||
// GIVEN
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN & THEN
|
||||
let _ = IndexPath(
|
||||
item: 0,
|
||||
section: fetcher.numberOfSections - 1
|
||||
)
|
||||
|
||||
// TODO: Need to find out how to handle NSInvalidArgumentException in this test.
|
||||
// let object = try fetcher.object(at: indexPath)
|
||||
}
|
||||
|
||||
func test_objectAt_whenModelIsFilled() throws {
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let entities = [TestEntity(context: context)]
|
||||
|
||||
try persistence.save(childContext: context)
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN & THEN
|
||||
let indexPath = IndexPath(
|
||||
item: entities.count - 1,
|
||||
section: fetcher.numberOfSections - 1
|
||||
)
|
||||
|
||||
let object = try fetcher.object(at: indexPath)
|
||||
|
||||
XCTAssertNotNil(object)
|
||||
}
|
||||
|
||||
func test_objectAt_withOutOfBoundsIndexPath() throws {
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let entities = [TestEntity(context: context)]
|
||||
|
||||
try persistence.save(childContext: context)
|
||||
try fetcher.fetch()
|
||||
|
||||
// WHEN & THEN
|
||||
let _ = IndexPath(
|
||||
item: entities.count,
|
||||
section: fetcher.numberOfSections
|
||||
)
|
||||
|
||||
// TODO: Need to find out how to handle NSInvalidArgumentException in this test.
|
||||
// let object = try fetcher.object(at: indexPath)
|
||||
}
|
||||
|
||||
func test_objectAt_whenNoFetch() throws {
|
||||
// GIVEN
|
||||
// WHEN & THEN
|
||||
let indexPath = IndexPath(
|
||||
item: 0,
|
||||
section: 0
|
||||
)
|
||||
|
||||
XCTAssertThrowsError(try fetcher.object(at: indexPath)) { error in
|
||||
XCTAssertEqual(error as? FetcherError, .fetchNotExecuted)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Did change publisher tests
|
||||
|
||||
func test_didChangePublisher_whenModelIsEmpty() throws {
|
||||
let expectation = self.expectation(description: "didChangePublisher when model is filled.")
|
||||
|
||||
var result: [Change]?
|
||||
|
||||
// GIVEN
|
||||
let cancellable = fetcher
|
||||
.didChangePublisher
|
||||
.sink(receiveValue: { value in
|
||||
result = value
|
||||
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
// WHEN
|
||||
try fetcher.fetch()
|
||||
|
||||
// THEN
|
||||
let waiter = XCTWaiter.wait(for: [expectation], timeout: 1.0)
|
||||
|
||||
guard waiter == .timedOut else {
|
||||
XCTFail("Waiter expected to time out.")
|
||||
return
|
||||
}
|
||||
|
||||
cancellable.cancel()
|
||||
|
||||
XCTAssertNil(result)
|
||||
}
|
||||
|
||||
func test_didChangePublisher_whenModelIsFilled() throws {
|
||||
let expectation = self.expectation(description: "didChangePublisher when model is filled.")
|
||||
|
||||
var result: [Change]?
|
||||
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let _ = [
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context)
|
||||
]
|
||||
|
||||
let cancellable = fetcher
|
||||
.didChangePublisher
|
||||
.sink(receiveValue: { value in
|
||||
result = value
|
||||
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
// WHEN
|
||||
try persistence.save(childContext: context)
|
||||
try fetcher.fetch()
|
||||
|
||||
// THEN
|
||||
let waiter = XCTWaiter.wait(for: [expectation], timeout: 1.0)
|
||||
|
||||
guard waiter == .timedOut else {
|
||||
XCTFail("Waiter expected to time out.")
|
||||
return
|
||||
}
|
||||
|
||||
cancellable.cancel()
|
||||
|
||||
XCTAssertNil(result)
|
||||
}
|
||||
|
||||
func test_didChangePublisher_whenModelIsUpdated() throws {
|
||||
let expectation = self.expectation(description: "didChangePublisher when model is updated.")
|
||||
|
||||
var result: [Change]?
|
||||
|
||||
// GIVEN
|
||||
let context = persistence.makeChildContext()
|
||||
let entities = [
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context),
|
||||
TestEntity(context: context)
|
||||
]
|
||||
|
||||
let cancellable = fetcher
|
||||
.didChangePublisher
|
||||
.sink(receiveValue: { value in
|
||||
result = value
|
||||
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
// WHEN
|
||||
try fetcher.fetch()
|
||||
try persistence.save(childContext: context)
|
||||
|
||||
// THEN
|
||||
waitForExpectations(timeout: 1.0)
|
||||
|
||||
cancellable.cancel()
|
||||
|
||||
XCTAssertNotNil(result)
|
||||
XCTAssertEqual(result?.count, entities.count)
|
||||
XCTAssertEqual(result, [
|
||||
.object(.inserted(at: .init(item: 2, section: 0))),
|
||||
.object(.inserted(at: .init(item: 1, section: 0))),
|
||||
.object(.inserted(at: .init(item: 0, section: 0))),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - TestPersistenceService+Functions
|
||||
|
||||
private extension TestPersistenceService {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func clean() throws {
|
||||
let context = makeChildContext()
|
||||
|
||||
try context.performAndWait {
|
||||
try context
|
||||
.fetch(.allTestEntities())
|
||||
.forEach(context.delete)
|
||||
}
|
||||
|
||||
try save(childContext: context)
|
||||
}
|
||||
|
||||
}
|
30
Tests/Persistence/Extensions/URL+DevicesTests.swift
Normal file
30
Tests/Persistence/Extensions/URL+DevicesTests.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// URL+DevicesTests.swift
|
||||
// PersistenceTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Persistence
|
||||
import XCTest
|
||||
|
||||
final class URL_DevicesTests: XCTestCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var url: URL!
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
func test_bitBucket() {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
url = .bitBucket
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(url.absoluteString, "file:///dev/null")
|
||||
}
|
||||
|
||||
}
|
24
Tests/Persistence/Helpers/NSFetchRequest+TestEntity.swift
Normal file
24
Tests/Persistence/Helpers/NSFetchRequest+TestEntity.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// NSFetchRequest+TestEntity.swift
|
||||
// PersistenceTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
extension NSFetchRequest where ResultType == TestEntity {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
static func allTestEntities() -> NSFetchRequest<TestEntity> {
|
||||
let request = TestEntity.fetchRequest()
|
||||
|
||||
request.sortDescriptors = []
|
||||
request.resultType = .managedObjectResultType
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="TestEntity" representedClassName="TestEntity" syncable="YES" codeGenerationType="class"/>
|
||||
</model>
|
127
Tests/Persistence/Helpers/TestPersistenceService.swift
Normal file
127
Tests/Persistence/Helpers/TestPersistenceService.swift
Normal file
@ -0,0 +1,127 @@
|
||||
//
|
||||
// TestPersistenceService.swift
|
||||
// PersistenceTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Persistence
|
||||
|
||||
struct TestPersistenceService {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
static let shared = TestPersistenceService()
|
||||
|
||||
private let container: NSPersistentContainer
|
||||
|
||||
var viewContext: NSManagedObjectContext {
|
||||
container.viewContext
|
||||
}
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init() {
|
||||
guard
|
||||
let modelURL = Bundle.module.url(forResource: .Model.name, withExtension: .Model.extension),
|
||||
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)
|
||||
else {
|
||||
fatalError("Could not load the Core Data model from the module.")
|
||||
}
|
||||
|
||||
self.container = .init(
|
||||
name: .Model.name,
|
||||
managedObjectModel: managedObjectModel
|
||||
)
|
||||
|
||||
setContainer()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Service
|
||||
|
||||
extension TestPersistenceService: Service {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func makeTaskContext() -> NSManagedObjectContext {
|
||||
let taskContext = container.newBackgroundContext()
|
||||
|
||||
taskContext.automaticallyMergesChangesFromParent = true
|
||||
taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
|
||||
return taskContext
|
||||
}
|
||||
|
||||
func makeChildContext() -> NSManagedObjectContext {
|
||||
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
context.parent = container.viewContext
|
||||
context.automaticallyMergesChangesFromParent = true
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
func save(context: NSManagedObjectContext) throws {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
|
||||
func save(childContext context: NSManagedObjectContext) throws {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
}
|
||||
|
||||
try context.save()
|
||||
|
||||
guard
|
||||
let parent = context.parent,
|
||||
parent == container.viewContext
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
try parent.performAndWait {
|
||||
try parent.save()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension TestPersistenceService {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func setContainer() {
|
||||
container.persistentStoreDescriptions = [
|
||||
.init(url: .bitBucket)
|
||||
]
|
||||
|
||||
container.loadPersistentStores { _, error in
|
||||
if let error = error as NSError? {
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
container.viewContext.automaticallyMergesChangesFromParent = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - String+Constants
|
||||
|
||||
private extension String {
|
||||
enum Model {
|
||||
static let name = "TestModel"
|
||||
static let `extension` = "momd"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user