diff --git a/Apps/Locations/Libraries/Package.swift b/Apps/Locations/Libraries/Package.swift
index ef3cbc4..396d480 100644
--- a/Apps/Locations/Libraries/Package.swift
+++ b/Apps/Locations/Libraries/Package.swift
@@ -27,6 +27,10 @@ let package = Package(
"APICore"
]
),
+ .target(
+ name: "Persistence",
+ dependencies: []
+ ),
.testTarget(
name: "APICoreTests",
dependencies: [
@@ -40,5 +44,11 @@ let package = Package(
"Locations"
]
),
+ .testTarget(
+ name: "PersistenceTests",
+ dependencies: [
+ "Persistence"
+ ]
+ ),
]
)
diff --git a/Apps/Locations/Resources/Locations.xcdatamodeld/.xccurrentversion b/Apps/Locations/Libraries/Sources/Persistence/Model.xcdatamodeld/.xccurrentversion
similarity index 100%
rename from Apps/Locations/Resources/Locations.xcdatamodeld/.xccurrentversion
rename to Apps/Locations/Libraries/Sources/Persistence/Model.xcdatamodeld/.xccurrentversion
diff --git a/Apps/Locations/Libraries/Sources/Persistence/Model.xcdatamodeld/Locations.xcdatamodel/contents b/Apps/Locations/Libraries/Sources/Persistence/Model.xcdatamodeld/Locations.xcdatamodel/contents
new file mode 100644
index 0000000..bd27564
--- /dev/null
+++ b/Apps/Locations/Libraries/Sources/Persistence/Model.xcdatamodeld/Locations.xcdatamodel/contents
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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)
+ }
+
+}
diff --git a/Apps/Locations/Resources/Locations.xcdatamodeld/Locations.xcdatamodel/contents b/Apps/Locations/Resources/Locations.xcdatamodeld/Locations.xcdatamodel/contents
deleted file mode 100644
index 50d2514..0000000
--- a/Apps/Locations/Resources/Locations.xcdatamodeld/Locations.xcdatamodel/contents
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Apps/Locations/Sources/AppDelegate.swift b/Apps/Locations/Sources/AppDelegate.swift
index d3dc549..b40c8ba 100644
--- a/Apps/Locations/Sources/AppDelegate.swift
+++ b/Apps/Locations/Sources/AppDelegate.swift
@@ -41,50 +41,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
- // MARK: - Core Data stack
-
- lazy var persistentContainer: NSPersistentContainer = {
- /*
- The persistent container for the application. This implementation
- creates and returns a container, having loaded the store for the
- application to it. This property is optional since there are legitimate
- error conditions that could cause the creation of the store to fail.
- */
- let container = NSPersistentContainer(name: "Locations")
- container.loadPersistentStores(completionHandler: { (storeDescription, error) in
- if let error = error as NSError? {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-
- /*
- Typical reasons for an error here include:
- * The parent directory does not exist, cannot be created, or disallows writing.
- * The persistent store is not accessible, due to permissions or data protection when the device is locked.
- * The device is out of space.
- * The store could not be migrated to the current model version.
- Check the error message to determine what the actual problem was.
- */
- fatalError("Unresolved error \(error), \(error.userInfo)")
- }
- })
- return container
- }()
-
- // MARK: - Core Data Saving support
-
- func saveContext () {
- let context = persistentContainer.viewContext
- if context.hasChanges {
- do {
- try context.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nserror = error as NSError
- fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
- }
- }
- }
-
}
diff --git a/Apps/Locations/Sources/SceneDelegate.swift b/Apps/Locations/Sources/SceneDelegate.swift
index b94fe63..c95cee8 100644
--- a/Apps/Locations/Sources/SceneDelegate.swift
+++ b/Apps/Locations/Sources/SceneDelegate.swift
@@ -62,7 +62,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// to restore the scene back to its current state.
// Save changes in the application's managed object context when the application transitions to the background.
- (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
diff --git a/DeepLinking.xcodeproj/project.pbxproj b/DeepLinking.xcodeproj/project.pbxproj
index b324f86..6181e6f 100644
--- a/DeepLinking.xcodeproj/project.pbxproj
+++ b/DeepLinking.xcodeproj/project.pbxproj
@@ -10,7 +10,6 @@
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */; };
46EB331D29E1CE04001D5EAF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331C29E1CE04001D5EAF /* SceneDelegate.swift */; };
46EB331F29E1CE04001D5EAF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331E29E1CE04001D5EAF /* ViewController.swift */; };
- 46EB332529E1CE04001D5EAF /* Locations.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 46EB332329E1CE04001D5EAF /* Locations.xcdatamodeld */; };
46EB332729E1CE05001D5EAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332629E1CE05001D5EAF /* Assets.xcassets */; };
46EB332A29E1CE05001D5EAF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332829E1CE05001D5EAF /* LaunchScreen.storyboard */; };
46EB334429E1D1EC001D5EAF /* Libraries in Frameworks */ = {isa = PBXBuildFile; productRef = 46EB334329E1D1EC001D5EAF /* Libraries */; };
@@ -117,7 +116,6 @@
46EB331A29E1CE04001D5EAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
46EB331C29E1CE04001D5EAF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
46EB331E29E1CE04001D5EAF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
- 46EB332429E1CE04001D5EAF /* Locations.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Locations.xcdatamodel; sourceTree = ""; };
46EB332629E1CE05001D5EAF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
46EB332929E1CE05001D5EAF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
46EB332B29E1CE05001D5EAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
@@ -214,7 +212,6 @@
46EB332629E1CE05001D5EAF /* Assets.xcassets */,
46EB332B29E1CE05001D5EAF /* Info.plist */,
46EB332829E1CE05001D5EAF /* LaunchScreen.storyboard */,
- 46EB332329E1CE04001D5EAF /* Locations.xcdatamodeld */,
);
path = Resources;
sourceTree = "";
@@ -412,7 +409,6 @@
files = (
46EB331F29E1CE04001D5EAF /* ViewController.swift in Sources */,
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */,
- 46EB332529E1CE04001D5EAF /* Locations.xcdatamodeld in Sources */,
46EB331D29E1CE04001D5EAF /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -644,19 +640,6 @@
productName = Shared;
};
/* End XCSwiftPackageProductDependency section */
-
-/* Begin XCVersionGroup section */
- 46EB332329E1CE04001D5EAF /* Locations.xcdatamodeld */ = {
- isa = XCVersionGroup;
- children = (
- 46EB332429E1CE04001D5EAF /* Locations.xcdatamodel */,
- );
- currentVersion = 46EB332429E1CE04001D5EAF /* Locations.xcdatamodel */;
- path = Locations.xcdatamodeld;
- sourceTree = "";
- versionGroupType = wrapper.xcdatamodel;
- };
-/* End XCVersionGroup section */
};
rootObject = 46EB325129E1BBD1001D5EAF /* Project object */;
}