diff --git a/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift b/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift new file mode 100644 index 0000000..c81ab12 --- /dev/null +++ b/Apps/Locations/Libraries/Sources/Persistence/Services/PersistenceService.swift @@ -0,0 +1,133 @@ +// +// PersistenceService.swift +// Persistence +// +// Created by Javier Cicchelli on 10/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import CoreData + +public struct PersistenceService { + + // MARK: Properties + + public static let shared = PersistenceService() + public static let inMemory = PersistenceService(inMemory: true) + + public let container: NSPersistentContainer + + // MARK: Initialisers + + init(inMemory: Bool = false) { + guard + let modelURL = Bundle.module.url(forResource: .Model.name, withExtension: .Model.extension), + let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) + else { + fatalError("Could not load the model from the library.") + } + + container = NSPersistentContainer( + name: .Model.name, + managedObjectModel: managedObjectModel + ) + + setContainer(inMemory) + } + + // MARK: Functions + + /// Create a private queue context. + /// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use. + public func makeTaskContext() -> NSManagedObjectContext { + let taskContext = container.newBackgroundContext() + + taskContext.automaticallyMergesChangesFromParent = true + taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + + return taskContext + } + + /// Create a child context of the view context. + /// - Returns: A generated child `NSManagedObjectContext` context instance ready to use. + public func makeChildContext() -> NSManagedObjectContext { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + context.parent = container.viewContext + context.automaticallyMergesChangesFromParent = true + + return context + } + + /// Save a given context, + /// - Parameter context: A `NSManagedObjectContext` context instance to save. + public func save(context: NSManagedObjectContext) { + guard context.hasChanges else { + return + } + + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + + /// Save a given child context as well as its respective parent context. + /// - Parameter context: A child `NSManagedObjectContext` context instance to save. + public func save(childContext context: NSManagedObjectContext) { + guard context.hasChanges else { + return + } + + do { + try context.save() + + guard + let parent = context.parent, + parent == container.viewContext + else { + return + } + + try parent.performAndWait { + try parent.save() + } + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + +} + +// MARK: - Helpers + +private extension PersistenceService { + func setContainer(_ inMemory: Bool) { + container.persistentStoreDescriptions = [ + NSPersistentStoreDescription(url: + inMemory + ? URL(fileURLWithPath: "/dev/null") + : NSPersistentContainer.defaultDirectoryURL().appending(path: "\(String.Model.name).sqlite") + ) + ] + container.loadPersistentStores { _, error in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + } + container.viewContext.automaticallyMergesChangesFromParent = true + } +} + +// MARK: - String+Constants + +private extension String { + enum Model { + static let name = "Model" + static let `extension` = "momd" + } +} diff --git a/Apps/Locations/Libraries/Tests/PersistenceTests/Services/PersistenceServiceTests.swift b/Apps/Locations/Libraries/Tests/PersistenceTests/Services/PersistenceServiceTests.swift new file mode 100644 index 0000000..152beb2 --- /dev/null +++ b/Apps/Locations/Libraries/Tests/PersistenceTests/Services/PersistenceServiceTests.swift @@ -0,0 +1,104 @@ +// +// PersistenceServiceTests.swift +// PersistenceTests +// +// Created by Javier Cicchelli on 11/04/2023. +// Copyright © 2023 Röck+Cöde. All rights reserved. +// + +import CoreData +import XCTest + +@testable import Persistence + +final class PersistenceServiceTests: XCTestCase { + + // MARK: Properties + + private var persistence: PersistenceService! + + // MARK: Initialiser tests + + func test_initByDefault() { + // GIVEN + // WHEN + persistence = .init() + + // THEN + XCTAssertEqual(persistence.container.persistentStoreDescriptions.count, 1) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.type, NSSQLiteStoreType) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.url?.lastPathComponent, "Model.sqlite") + XCTAssertNotNil(persistence.container.viewContext) + XCTAssertTrue(persistence.container.viewContext.automaticallyMergesChangesFromParent) + } + + func test_initWithInMemory() { + // GIVEN + // WHEN + persistence = .init(inMemory: true) + + // THEN + XCTAssertEqual(persistence.container.persistentStoreDescriptions.count, 1) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.type, NSSQLiteStoreType) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.url?.absoluteString, "file:///dev/null") + XCTAssertNotNil(persistence.container.viewContext) + XCTAssertTrue(persistence.container.viewContext.automaticallyMergesChangesFromParent) + } + + // MARK: Static properties tests + + func test_shared() { + // GIVEN + persistence = .shared + + // WHEN + // THEN + XCTAssertEqual(persistence.container.persistentStoreDescriptions.count, 1) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.type, NSSQLiteStoreType) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.url?.lastPathComponent, "Model.sqlite") + XCTAssertNotNil(persistence.container.viewContext) + XCTAssertTrue(persistence.container.viewContext.automaticallyMergesChangesFromParent) + } + + func test_inMemory() { + // GIVEN + persistence = .inMemory + + // WHEN + // THEN + XCTAssertEqual(persistence.container.persistentStoreDescriptions.count, 1) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.type, NSSQLiteStoreType) + XCTAssertEqual(persistence.container.persistentStoreDescriptions.first?.url?.absoluteString, "file:///dev/null") + XCTAssertNotNil(persistence.container.viewContext) + XCTAssertTrue(persistence.container.viewContext.automaticallyMergesChangesFromParent) + } + + // MARK: Functions tests + + func test_makeTaskContext() { + // GIVEN + persistence = .inMemory + + // WHEN + let context = persistence.makeTaskContext() + + // THEN + XCTAssertTrue(context.automaticallyMergesChangesFromParent) + XCTAssertTrue(context.mergePolicy as AnyObject === NSMergeByPropertyObjectTrumpMergePolicy) + XCTAssertNil(context.parent) + } + + func test_makeChildContext() { + // GIVEN + persistence = .inMemory + + // WHEN + let context = persistence.makeChildContext() + + // THEN + XCTAssertTrue(context.automaticallyMergesChangesFromParent) + XCTAssertTrue(context.mergePolicy as AnyObject === NSMergeByPropertyObjectTrumpMergePolicy) + XCTAssertEqual(context.parent, persistence.container.viewContext) + } + +}