diff --git a/Example/CoreDataManager.xcodeproj/project.pbxproj b/Example/CoreDataManager.xcodeproj/project.pbxproj index 12e3380..0445d05 100644 --- a/Example/CoreDataManager.xcodeproj/project.pbxproj +++ b/Example/CoreDataManager.xcodeproj/project.pbxproj @@ -16,11 +16,15 @@ 607FACEC1AFB9204008FA782 /* SetupTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* SetupTestCase.swift */; }; A41E80C01BA5AA1500FEA2B9 /* SerializerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41E80BF1BA5AA1500FEA2B9 /* SerializerTestCase.swift */; }; A4758E7B1B9B226800D6CBCF /* CoreDataManager.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = A4758E791B9B226800D6CBCF /* CoreDataManager.xcdatamodeld */; }; - A4758EC61B9BA21300D6CBCF /* Click.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4758EC51B9BA21300D6CBCF /* Click.swift */; }; - A4758ECF1B9C622B00D6CBCF /* Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4758ECE1B9C622B00D6CBCF /* Batch.swift */; }; A4758ED51B9C932500D6CBCF /* ContextTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4758ED01B9C919B00D6CBCF /* ContextTestCase.swift */; }; A4758ED61B9C932500D6CBCF /* ManagedObjectManagerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4758ED21B9C925A00D6CBCF /* ManagedObjectManagerTestCase.swift */; }; EEF720F06FDCAA15029A3DE3 /* Pods_CoreDataManager_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD20E1BD1E6D716F413BF29F /* Pods_CoreDataManager_Tests.framework */; }; + F85D4950276F836B007AFA5A /* Click+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4947276F8306007AFA5A /* Click+CoreDataProperties.swift */; }; + F85D4951276F836B007AFA5A /* Batch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4945276F8306007AFA5A /* Batch+CoreDataProperties.swift */; }; + F85D4952276F836B007AFA5A /* Batch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4944276F8306007AFA5A /* Batch+CoreDataClass.swift */; }; + F85D4953276F836B007AFA5A /* Point+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4946276F8306007AFA5A /* Point+CoreDataClass.swift */; }; + F85D4954276F836B007AFA5A /* Point+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4948276F8306007AFA5A /* Point+CoreDataProperties.swift */; }; + F85D4955276F836B007AFA5A /* Click+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4949276F8306007AFA5A /* Click+CoreDataClass.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +54,6 @@ 607FACEB1AFB9204008FA782 /* SetupTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTestCase.swift; sourceTree = ""; }; A41E80BF1BA5AA1500FEA2B9 /* SerializerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerializerTestCase.swift; sourceTree = ""; }; A4758E7A1B9B226800D6CBCF /* CoreDataManager.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataManager.xcdatamodel; sourceTree = ""; }; - A4758EC51B9BA21300D6CBCF /* Click.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Click.swift; sourceTree = ""; }; - A4758ECE1B9C622B00D6CBCF /* Batch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Batch.swift; path = Models/Batch.swift; sourceTree = ""; }; A4758ED01B9C919B00D6CBCF /* ContextTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextTestCase.swift; sourceTree = ""; }; A4758ED21B9C925A00D6CBCF /* ManagedObjectManagerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedObjectManagerTestCase.swift; sourceTree = ""; }; C289BEA78384DD3893AB5CDE /* CoreDataManager.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = CoreDataManager.podspec; path = ../CoreDataManager.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; @@ -59,6 +61,12 @@ D74A19219C9FE0096BC6F3D9 /* Pods_CoreDataManager_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CoreDataManager_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D86CA9C21793D2D2696FBE74 /* Pods-CoreDataManager_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataManager_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataManager_Example/Pods-CoreDataManager_Example.debug.xcconfig"; sourceTree = ""; }; DBB95AD69E69A34BAADE25EB /* Pods-CoreDataManager_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreDataManager_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CoreDataManager_Tests/Pods-CoreDataManager_Tests.debug.xcconfig"; sourceTree = ""; }; + F85D4944276F8306007AFA5A /* Batch+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Batch+CoreDataClass.swift"; path = "Models/Batch+CoreDataClass.swift"; sourceTree = ""; }; + F85D4945276F8306007AFA5A /* Batch+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Batch+CoreDataProperties.swift"; path = "Models/Batch+CoreDataProperties.swift"; sourceTree = ""; }; + F85D4946276F8306007AFA5A /* Point+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Point+CoreDataClass.swift"; path = "Models/Point+CoreDataClass.swift"; sourceTree = ""; }; + F85D4947276F8306007AFA5A /* Click+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Click+CoreDataProperties.swift"; path = "Models/Click+CoreDataProperties.swift"; sourceTree = ""; }; + F85D4948276F8306007AFA5A /* Point+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Point+CoreDataProperties.swift"; path = "Models/Point+CoreDataProperties.swift"; sourceTree = ""; }; + F85D4949276F8306007AFA5A /* Click+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Click+CoreDataClass.swift"; path = "Models/Click+CoreDataClass.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -159,8 +167,12 @@ A4758EC71B9BA21A00D6CBCF /* Models */ = { isa = PBXGroup; children = ( - A4758ECE1B9C622B00D6CBCF /* Batch.swift */, - A4758EC51B9BA21300D6CBCF /* Click.swift */, + F85D4944276F8306007AFA5A /* Batch+CoreDataClass.swift */, + F85D4945276F8306007AFA5A /* Batch+CoreDataProperties.swift */, + F85D4949276F8306007AFA5A /* Click+CoreDataClass.swift */, + F85D4947276F8306007AFA5A /* Click+CoreDataProperties.swift */, + F85D4946276F8306007AFA5A /* Point+CoreDataClass.swift */, + F85D4948276F8306007AFA5A /* Point+CoreDataProperties.swift */, ); name = Models; sourceTree = ""; @@ -352,10 +364,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A4758EC61B9BA21300D6CBCF /* Click.swift in Sources */, 607FACD81AFB9204008FA782 /* ClickViewController.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, - A4758ECF1B9C622B00D6CBCF /* Batch.swift in Sources */, + F85D4955276F836B007AFA5A /* Click+CoreDataClass.swift in Sources */, + F85D4953276F836B007AFA5A /* Point+CoreDataClass.swift in Sources */, + F85D4950276F836B007AFA5A /* Click+CoreDataProperties.swift in Sources */, + F85D4951276F836B007AFA5A /* Batch+CoreDataProperties.swift in Sources */, + F85D4952276F836B007AFA5A /* Batch+CoreDataClass.swift in Sources */, + F85D4954276F836B007AFA5A /* Point+CoreDataProperties.swift in Sources */, A4758E7B1B9B226800D6CBCF /* CoreDataManager.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -449,7 +465,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -498,7 +514,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/Example/CoreDataManager/Click.swift b/Example/CoreDataManager/Click.swift deleted file mode 100644 index 49dd473..0000000 --- a/Example/CoreDataManager/Click.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Click.swift -// CoreDataManager -// -// Created by Taavi Teska on 06/09/15. -// Copyright (c) 2015 CocoaPods. All rights reserved. -// - -import Foundation -import CoreData - -open class Click: NSManagedObject { - - @NSManaged open var clickID: NSNumber - @NSManaged open var timeStamp: Date - - @NSManaged open var batch: Batch - -} diff --git a/Example/CoreDataManager/ClickViewController.swift b/Example/CoreDataManager/ClickViewController.swift index 07d0e18..4127cbf 100644 --- a/Example/CoreDataManager/ClickViewController.swift +++ b/Example/CoreDataManager/ClickViewController.swift @@ -10,74 +10,84 @@ import CoreData import CoreDataManager import UIKit -class ClickViewController: UITableViewController, NSFetchedResultsControllerDelegate { - +class ClickViewController: UITableViewController { private let cdm = CoreDataManager.sharedInstance - private var thisBatchID: NSNumber! + private lazy var thisBatchID: Int = { + let lastBatchID = (cdm.mainContext.managerFor(Batch.self).max("id") as? Int) ?? 0 + return lastBatchID + 1 + }() + // MARK: - Fetched results controller + + lazy var fetchedResultsController: NSFetchedResultsController = { + let fetchRequest = NSFetchRequest(entityName: "Click") + let sortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false) + fetchRequest.sortDescriptors = [sortDescriptor] + let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: cdm.mainContext, sectionNameKeyPath: "batch.name", cacheName: nil) + fetchedResultsController.delegate = self + try? fetchedResultsController.performFetch() + return fetchedResultsController + }() + override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. - self.navigationItem.leftBarButtonItem = self.editButtonItem + navigationItem.leftBarButtonItem = editButtonItem let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(ClickViewController.insertNewClick(sender:))) - self.navigationItem.rightBarButtonItem = addButton - - let lastBatchID = (self.cdm.mainContext.managerFor(Batch.self).max("id") as? NSNumber) ?? 0 - self.thisBatchID = NSNumber(integerLiteral: lastBatchID.intValue + 1) + navigationItem.rightBarButtonItem = addButton } @objc func insertNewClick(sender: AnyObject) { - - let context = self.cdm.backgroundContext - context.perform { - let clickManager = context.managerFor(Click.self) + let backgroundContext = cdm.backgroundContext + backgroundContext.perform { + let clickManager = backgroundContext.managerFor(Click.self) let lastClickID = (clickManager.max("clickID") as? Int) ?? 0 - let newClick = NSEntityDescription.insertNewObject(forEntityName: "Click", into: context) as! Click - newClick.timeStamp = Date() - newClick.clickID = NSNumber(integerLiteral: lastClickID + 1) + let newClick = Click.insert(into: backgroundContext) + newClick?.timeStamp = Date() + newClick?.clickID = NSNumber(value: lastClickID + 1) - if let batch = context.managerFor(Batch.self).filter(format: "id = %d", self.thisBatchID).first { - newClick.batch = batch + if let batch = backgroundContext.managerFor(Batch.self).filter(format: "id = %d", self.thisBatchID).first { + newClick?.batch = batch } else { - let newBatch = NSEntityDescription.insertNewObject(forEntityName: "Batch", into: context) as! Batch - - newBatch.id = self.thisBatchID - newBatch.name = "Batch \(self.thisBatchID.intValue)" - newClick.batch = newBatch + let newBatch = Batch.insert(into: backgroundContext) + newBatch?.id = NSNumber(value: self.thisBatchID) + newBatch?.name = "Batch \(self.thisBatchID)" + newClick?.batch = newBatch } - try! context.saveIfChanged() + try? backgroundContext.saveIfChanged() } } // MARK: - Private private func configureCell(cell: UITableViewCell, atIndexPath indexPath: IndexPath) { - let object = self.fetchedResultsController.object(at: indexPath) - cell.textLabel!.text = object.timeStamp.description + let object = fetchedResultsController.object(at: indexPath) + cell.textLabel?.text = object.timeStamp?.description } // MARK: - Table View + override func numberOfSections(in tableView: UITableView) -> Int { - return self.fetchedResultsController.sections?.count ?? 0 + return fetchedResultsController.sections?.count ?? 0 } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let firstClick = self.fetchedResultsController.object(at: IndexPath(row: 0, section: section)) + let firstClick = fetchedResultsController.object(at: IndexPath(row: 0, section: section)) - return firstClick.batch.name + return firstClick.batch?.name } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let sectionInfo = self.fetchedResultsController.sections![section] + let sectionInfo = fetchedResultsController.sections![section] return sectionInfo.numberOfObjects } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ClickCell", for: indexPath) - self.configureCell(cell: cell, atIndexPath: indexPath) + configureCell(cell: cell, atIndexPath: indexPath) return cell } @@ -88,50 +98,29 @@ class ClickViewController: UITableViewController, NSFetchedResultsControllerDele override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { - let click = self.fetchedResultsController.object(at: indexPath) + let click = fetchedResultsController.object(at: indexPath) let clickID = click.clickID - let context = self.cdm.backgroundContext + let context = cdm.backgroundContext context.perform { - _ = context.managerFor(Click.self).filter(format: "clickID = %@", clickID).delete() - try! context.saveIfChanged() + _ = context.managerFor(Click.self).filter(format: "clickID = %@", clickID as CVarArg).delete() + try? context.saveIfChanged() } } } - - // MARK: - Fetched results controller - - var fetchedResultsController: NSFetchedResultsController { - if _fetchedResultsController != nil { - return _fetchedResultsController! - } - - let fetchRequest = NSFetchRequest(entityName: "Click") - - let sortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false) - - fetchRequest.sortDescriptors = [sortDescriptor] - - let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.cdm.mainContext, sectionNameKeyPath: "batch.name", cacheName: nil) - aFetchedResultsController.delegate = self - _fetchedResultsController = aFetchedResultsController - - try! _fetchedResultsController!.performFetch() - - return _fetchedResultsController! - } - var _fetchedResultsController: NSFetchedResultsController? = nil - +} + +extension ClickViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - self.tableView.beginUpdates() + tableView.beginUpdates() } func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { switch type { case .insert: - self.tableView.insertSections(IndexSet([sectionIndex]), with: .fade) + tableView.insertSections(IndexSet([sectionIndex]), with: .fade) case .delete: - self.tableView.deleteSections(IndexSet([sectionIndex]), with: .fade) + tableView.deleteSections(IndexSet([sectionIndex]), with: .fade) default: return } @@ -140,22 +129,20 @@ class ClickViewController: UITableViewController, NSFetchedResultsControllerDele func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: - self.tableView.insertRows(at: [newIndexPath!], with: .fade) + tableView.insertRows(at: [newIndexPath!], with: .fade) case .delete: - self.tableView.deleteRows(at: [indexPath!], with: .fade) + tableView.deleteRows(at: [indexPath!], with: .fade) case .update: - self.configureCell(cell: self.tableView.cellForRow(at: indexPath!)!, atIndexPath: indexPath!) + configureCell(cell: tableView.cellForRow(at: indexPath!)!, atIndexPath: indexPath!) case .move: - self.tableView.deleteRows(at: [indexPath!], with: .fade) - self.tableView.insertRows(at: [newIndexPath!], with: .fade) + tableView.deleteRows(at: [indexPath!], with: .fade) + tableView.insertRows(at: [newIndexPath!], with: .fade) @unknown default: fatalError("Unknown value of NSFetchedResultsChangeType") } } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - self.tableView.endUpdates() + tableView.endUpdates() } - } - diff --git a/Example/CoreDataManager/CoreDataManager.xcdatamodeld/CoreDataManager.xcdatamodel/contents b/Example/CoreDataManager/CoreDataManager.xcdatamodeld/CoreDataManager.xcdatamodel/contents index 2e8c8f7..3328f16 100644 --- a/Example/CoreDataManager/CoreDataManager.xcdatamodeld/CoreDataManager.xcdatamodel/contents +++ b/Example/CoreDataManager/CoreDataManager.xcdatamodeld/CoreDataManager.xcdatamodel/contents @@ -1,17 +1,25 @@ - - - - - + + + + + - - - - + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/Example/CoreDataManager/Models/Batch+CoreDataClass.swift b/Example/CoreDataManager/Models/Batch+CoreDataClass.swift new file mode 100644 index 0000000..022bfa4 --- /dev/null +++ b/Example/CoreDataManager/Models/Batch+CoreDataClass.swift @@ -0,0 +1,16 @@ +// +// Batch+CoreDataClass.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +public class Batch: NSManagedObject { + +} diff --git a/Example/CoreDataManager/Models/Batch+CoreDataProperties.swift b/Example/CoreDataManager/Models/Batch+CoreDataProperties.swift new file mode 100644 index 0000000..fe160d0 --- /dev/null +++ b/Example/CoreDataManager/Models/Batch+CoreDataProperties.swift @@ -0,0 +1,24 @@ +// +// Batch+CoreDataProperties.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +extension Batch { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) + } + + @NSManaged public var id: NSNumber + @NSManaged public var name: String? + @NSManaged public var clicks: NSSet? + +} diff --git a/Example/CoreDataManager/Models/Batch.swift b/Example/CoreDataManager/Models/Batch.swift deleted file mode 100644 index 39e3811..0000000 --- a/Example/CoreDataManager/Models/Batch.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Batch.swift -// CoreDataManager -// -// Created by Taavi Teska on 06/09/15. -// Copyright (c) 2015 CocoaPods. All rights reserved. -// - -import Foundation -import CoreData - -open class Batch: NSManagedObject { - - @NSManaged open var name: String - @NSManaged open var id: NSNumber - @NSManaged open var clicks: NSSet - -} diff --git a/Example/CoreDataManager/Models/Click+CoreDataClass.swift b/Example/CoreDataManager/Models/Click+CoreDataClass.swift new file mode 100644 index 0000000..8a2aa39 --- /dev/null +++ b/Example/CoreDataManager/Models/Click+CoreDataClass.swift @@ -0,0 +1,16 @@ +// +// Click+CoreDataClass.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +public class Click: NSManagedObject { + +} diff --git a/Example/CoreDataManager/Models/Click+CoreDataProperties.swift b/Example/CoreDataManager/Models/Click+CoreDataProperties.swift new file mode 100644 index 0000000..8f24ba4 --- /dev/null +++ b/Example/CoreDataManager/Models/Click+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// Click+CoreDataProperties.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +extension Click { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) + } + + @NSManaged public var clickID: NSNumber + @NSManaged public var timeStamp: Date? + @NSManaged public var batch: Batch? + @NSManaged public var point: Point? + +} diff --git a/Example/CoreDataManager/Models/Point+CoreDataClass.swift b/Example/CoreDataManager/Models/Point+CoreDataClass.swift new file mode 100644 index 0000000..aa8c878 --- /dev/null +++ b/Example/CoreDataManager/Models/Point+CoreDataClass.swift @@ -0,0 +1,16 @@ +// +// Point+CoreDataClass.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +public class Point: NSManagedObject { + +} diff --git a/Example/CoreDataManager/Models/Point+CoreDataProperties.swift b/Example/CoreDataManager/Models/Point+CoreDataProperties.swift new file mode 100644 index 0000000..5dd5b30 --- /dev/null +++ b/Example/CoreDataManager/Models/Point+CoreDataProperties.swift @@ -0,0 +1,25 @@ +// +// Point+CoreDataProperties.swift +// CoreDataManager_Example +// +// Created by Nguyen Truong Luu on 12/19/21. +// Copyright © 2021 CocoaPods. All rights reserved. +// +// + +import Foundation +import CoreData + + +extension Point { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: entityName) + } + + @NSManaged public var id: NSNumber + @NSManaged public var x: NSNumber? + @NSManaged public var y: NSNumber? + @NSManaged public var click: Click? + +} diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 01bd158..55c0627 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - CoreDataManager (0.8.2): + - CoreDataManager (0.9.0): - SwiftyJSON (~> 5.0.0) - SwiftyJSON (5.0.0) @@ -15,9 +15,9 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - CoreDataManager: 59ef4a49284903a22af3dd04825b004d91de157e + CoreDataManager: 241c4ecb43c8661b19d5e2376c5cd27701ee85c9 SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 PODFILE CHECKSUM: a82935d36bdc45389bbb85c6f3dbd947c6a24ed2 -COCOAPODS: 1.8.4 +COCOAPODS: 1.11.2 diff --git a/Example/Tests/ContextTestCase.swift b/Example/Tests/ContextTestCase.swift index 1fd4ae4..71d02c2 100644 --- a/Example/Tests/ContextTestCase.swift +++ b/Example/Tests/ContextTestCase.swift @@ -22,12 +22,12 @@ class ContextTestCase: XCTestCase { func testMainContext() { let context = self.cdm.mainContext - XCTAssertEqual(context.concurrencyType, NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType, "Main contexts is not main queue concurrency type") + XCTAssertEqual(context.concurrencyType, .mainQueueConcurrencyType, "Main contexts is not main queue concurrency type") } func testBackgroundContext() { let context = self.cdm.backgroundContext - XCTAssertEqual(context.concurrencyType, NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType, "Background contexts is not private queue concurrency type") + XCTAssertEqual(context.concurrencyType, .privateQueueConcurrencyType, "Background contexts is not private queue concurrency type") } } diff --git a/Example/Tests/ManagedObjectManagerTestCase.swift b/Example/Tests/ManagedObjectManagerTestCase.swift index 4c58737..9c5b7af 100644 --- a/Example/Tests/ManagedObjectManagerTestCase.swift +++ b/Example/Tests/ManagedObjectManagerTestCase.swift @@ -9,32 +9,48 @@ class ManagedObjectManagerTestCase: XCTestCase { override func setUp() { super.setUp() + + try? FileManager.default.removeItem(at: databaseURL) + // Put setup code here. This method is called before the invocation of each test method in the class. self.cdm = CoreDataManager() self.cdm.setupInMemoryWithModel("CoreDataManager") self.cdm.mainContext.performAndWait { () -> Void in - let batch = NSEntityDescription.insertNewObject(forEntityName: "Batch", into: self.cdm.mainContext) as! Batch - batch.id = 100 - batch.name = "Batch 100" + let batch = NSEntityDescription.insertNewObject(forEntityName: "Batch", into: self.cdm.mainContext) as? Batch + batch?.id = 100 + batch?.name = "Batch 100" for i in 0...4 { - let click = NSEntityDescription.insertNewObject(forEntityName: "Click", into: self.cdm.mainContext) as! Click - click.clickID = NSNumber(integerLiteral: i) - click.timeStamp = NSDate() as Date - click.batch = batch + let click = NSEntityDescription.insertNewObject(forEntityName: "Click", into: self.cdm.mainContext) as? Click + click?.clickID = NSNumber(value: i) + click?.timeStamp = NSDate() as Date + if let batch = batch { + click?.batch = batch + } } - try! self.cdm.mainContext.saveIfChanged() + try? self.cdm.mainContext.saveIfChanged() } } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. + try? FileManager.default.removeItem(at: databaseURL) super.tearDown() } + private lazy var databaseURL: URL = { + let fileManager = FileManager.default + let documentDirs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsURL = documentDirs[documentDirs.count-1] + let testingURL = documentsURL.appendingPathComponent("Testing") + let databaseURL = testingURL.appendingPathComponent("TestDatabase.sqlite") + try? fileManager.createDirectory(atPath: testingURL.path, withIntermediateDirectories: true, attributes: nil) + return databaseURL + }() + func testCreatingManager() { let manager = self.cdm.mainContext.managerFor(Click.self) XCTAssertEqual(manager.entityName(), "Click", "Managed object manager's entity is wrong") @@ -55,49 +71,35 @@ class ManagedObjectManagerTestCase: XCTestCase { // TODO: We use SQLite here because in-memory store has a bug // http://stackoverflow.com/questions/4387403/nscfnumber-count-unrecognized-selector - let fileManager = FileManager.default - let documentDirs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - let documentsURL = documentDirs[documentDirs.count-1] - let testingURL = documentsURL.appendingPathComponent("Testing") - let databaseURL = testingURL.appendingPathComponent("TestDatabase.sqlite") let SQLiteCDM = CoreDataManager() - - try! fileManager.createDirectory(atPath: testingURL.path, withIntermediateDirectories: true, attributes: nil) - SQLiteCDM.setupWithModel("CoreDataManager", andFileURL: databaseURL) - - let SQLiteManager = SQLiteCDM.mainContext.managerFor(Click.self) - SQLiteCDM.mainContext.performAndWait { () -> Void in - let batch = NSEntityDescription.insertNewObject(forEntityName: "Batch", into: SQLiteCDM.mainContext) as! Batch - batch.id = 100 - batch.name = "Batch 100" + SQLiteCDM.setupWithModel("CoreDataManager", andDatabaseURL: databaseURL) + let testContext = SQLiteCDM.mainContext + let SQLiteManager = SQLiteCDM.backgroundContext.managerFor(Click.self) + testContext.performAndWait { () -> Void in + let batch = NSEntityDescription.insertNewObject(forEntityName: "Batch", into: testContext) as? Batch + batch?.id = 100 + batch?.name = "Batch 100" for i in 0...4 { - let click = NSEntityDescription.insertNewObject(forEntityName: "Click", into: SQLiteCDM.mainContext) as! Click - click.clickID = NSNumber(integerLiteral: i) - click.timeStamp = Date() - click.batch = batch + if let click = NSEntityDescription.insertNewObject(forEntityName: "Click", into: testContext) as? Click { + click.clickID = NSNumber(value: i) + click.timeStamp = Date() + if let batch = batch { + click.batch = batch + } + } } - try! SQLiteCDM.mainContext.saveIfChanged() + try? testContext.saveIfChanged() XCTAssertEqual(SQLiteManager.min("clickID") as? Int, 0, "Aggregation MIN is wrong") - + XCTAssertEqual(SQLiteManager.max("clickID") as? Int, 4, "Aggregation MAX is wrong") - + XCTAssertEqual(SQLiteManager.sum("clickID") as? Int, 10, "Aggregation SUM is wrong") - XCTAssertEqual(SQLiteManager.aggregate("average", forKeyPath: "clickID") as? Int, 2, "Aggregation AVG is wrong") - } - - - // Clear documents directory - let fileNames = try! fileManager.contentsOfDirectory(atPath: documentsURL.path) - - // For each file in the directory, create full path and delete the file - for fileName in fileNames { - if fileName.hasPrefix("Test") { - try! fileManager.removeItem(at: documentsURL.appendingPathComponent(fileName)) - } + XCTAssertEqual(SQLiteManager.average("clickID") as? Int, 2, "Aggregation AVG is wrong") + } } diff --git a/Example/Tests/SerializerTestCase.swift b/Example/Tests/SerializerTestCase.swift index ac7829d..fb4ff06 100644 --- a/Example/Tests/SerializerTestCase.swift +++ b/Example/Tests/SerializerTestCase.swift @@ -6,35 +6,48 @@ // Copyright (c) 2015 CocoaPods. All rights reserved. // +import CoreData import CoreDataManager import CoreDataManager_Example import SwiftyJSON import XCTest class SerializerTestCase: XCTestCase { - - var cdm:CoreDataManager! + var cdm: CoreDataManager! override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. - self.cdm = CoreDataManager() - self.cdm.setupInMemoryWithModel("CoreDataManager") + cdm = CoreDataManager() + //cdm.setupInMemoryWithModel("CoreDataManager") + cdm.setupWithModel("CoreDataManager", andDatabaseURL: databaseURL) } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + cdm.mainContext.performAndWait { + cdm.mainContext.managerFor(Batch.self).delete() + try? cdm.mainContext.saveIfChanged() + } super.tearDown() } + private lazy var databaseURL: URL = { + let fileManager = FileManager.default + let documentDirs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsURL = documentDirs[documentDirs.count - 1] + let testingURL = documentsURL.appendingPathComponent("Testing") + let databaseURL = testingURL.appendingPathComponent("TestDatabase.sqlite") + try? fileManager.createDirectory(atPath: testingURL.path, withIntermediateDirectories: true, attributes: nil) + return databaseURL + }() + func testDataSync() { - let serializer = CDMSerializer() serializer.identifiers = ["id"] serializer.mapping = [ "id": CDMAttributeNumber(["id"]), - "name": CDMAttributeString(["batch", "name"]), + "name": CDMAttributeString(["batch", "name"]) ] let jsonData = JSON([ @@ -43,17 +56,17 @@ class SerializerTestCase: XCTestCase { "batch": [ "name": "Batch 1" ] - ],[ + ], [ "id": 4, "batch": [ "name": "Batch 4" - ], + ] ] - ]) + ]) - let expectation = self.expectation(description: "Syncing data didn't complete") + let expectation = expectation(description: "Syncing data didn't complete") - self.cdm.backgroundContext.syncData(jsonData, withSerializer: serializer) { (error) -> Void in + cdm.backgroundContext.syncData(jsonData, withSerializer: serializer) { (error) -> Void in XCTAssertNil(error) let batches = self.cdm.mainContext.managerFor(Batch.self).orderBy("-id").array @@ -68,67 +81,171 @@ class SerializerTestCase: XCTestCase { expectation.fulfill() } - self.waitForExpectations(timeout: 1) + waitForExpectations(timeout: 10) } - func testNestedDataSync() { + func testNestedDataArraySync() { + let serializer = CDMSerializer() + serializer.identifiers = ["id"] + serializer.mapping = [ + "id": CDMAttributeNumber(["id"]), + "name": CDMAttributeString(["batch", "name"]), + "clicks": CDMAttributeToMany(["clicked"], serializerCallback: { _ in + ClickSerializer(inverseRelationship: InverseRelationship(keyPath: "batch", parentSerializer: serializer)) + }) + ] + let jsonData = JSON( + [ + [ + "id": 1, + "batch": [ + "name": "Batch 1" + ], + "clicked": [ + ["id": 1, + "time": "2014-07-06T07:59:00Z", + "point": [ + "id": 1, + "x": 1, + "y": 1] + ], + ["id": 2, + "time": "2014-07-06T07:59:00Z", + "point": [ + "id": 2, + "x": 2, + "y": 2] + ] + ] + ], + [ + "id": 4, + "batch": [ + "name": "Batch 4" + ], + "clicked": [ + ["id": 1, + "time": "2014-07-06T07:59:00Z", + "point": [ + "id": 1, + "x": 1, + "y": 1] + ] + ] + ] + ] + ) + + let expectation = expectation(description: "Syncing data didn't complete") + + cdm.backgroundContext.syncData(jsonData, withSerializer: serializer) { (error) -> Void in + XCTAssertNil(error) + let allBatchs = self.cdm.mainContext.managerFor(Batch.self).orderBy("id").array + let allClicks = self.cdm.mainContext.managerFor(Click.self).orderBy("clickID").array + let allPoints = self.cdm.mainContext.managerFor(Point.self).orderBy("id").array + + XCTAssertEqual(allBatchs.count, 2, "Batchs count after sync doesn't match") + XCTAssertEqual(allClicks.count, 3, "Clicks count after sync doesn't match") + XCTAssertEqual(allPoints.count, 3, "Points count after sync doesn't match") + + let batch1Clicks = self.cdm.mainContext.managerFor(Click.self).filter(format: "batch.id = %d", 1).orderBy("clickID").array + XCTAssertEqual(batch1Clicks.count, 2, "Clicks count after sync doesn't match") + + let click1 = batch1Clicks.first + let click2 = batch1Clicks.last + + XCTAssertEqual(click1?.batch?.id, 1, "First click's batchID doesn't match") + XCTAssertEqual(click1?.clickID, 1, "First click's ID doesn't match") + XCTAssertEqual(click1?.point?.id, 1, "First point's ID doesn't match") + + XCTAssertEqual(click2?.batch?.id, 1, "Second click's batchID doesn't match") + XCTAssertEqual(click2?.clickID, 2, "Second click's ID doesn't match") + XCTAssertEqual(click2?.point?.id, 2, "Second point's ID doesn't match") + + let batch4Clicks = self.cdm.mainContext.managerFor(Click.self).filter(format: "batch.id = %d", 4).orderBy("clickID").array + XCTAssertEqual(batch4Clicks.count, 1, "Clicks count after sync doesn't match") + + let click4 = batch4Clicks.first + + XCTAssertEqual(click4?.batch?.id, 4, "First click's batchID doesn't match") + XCTAssertEqual(click4?.clickID, 1, "First click's ID doesn't match") + XCTAssertEqual(click4?.point?.id, 1, "Second point's ID doesn't match") + + expectation.fulfill() + } + + waitForExpectations(timeout: 1000) + } + + func testNestedDataSync() { let serializer = CDMSerializer() serializer.identifiers = ["id"] serializer.mapping = [ "id": CDMAttributeNumber(["id"]), "name": CDMAttributeString(["batch", "name"]), - "clicks": CDMAttributeToMany(["clicked"], serializerCallback: {_ in - return ClickSerializer() - }), + "clicks": CDMAttributeToMany(["clicked"], serializerCallback: { _ in + ClickSerializer(inverseRelationship: InverseRelationship(keyPath: "batch", parentSerializer: serializer)) + }) ] - let jsonData = JSON([ + let jsonData = JSON( [ "id": 1, "batch": [ "name": "Batch 1" ], "clicked": [ - ["id": 2, "time": "2014-07-06T07:59:00Z"], - ["id": 3, "time": "2014-07-06T07:59:00Z"], + ["id": 1, + "time": "2014-07-06T07:59:00Z", + "point": [ + "id": 1, + "x": 1, + "y": 1] + ], + ["id": 2, + "time": "2014-07-06T07:59:00Z", + "point": [ + "id": 2, + "x": 2, + "y": 2] + ] ] - ],[ - "id": 4, - "batch": [ - "name": "Batch 4" - ], ] - ]) + ) - let expectation = self.expectation(description: "Syncing data didn't complete") + let expectation = expectation(description: "Syncing data didn't complete") - self.cdm.backgroundContext.syncData(jsonData, withSerializer: serializer) { (error) -> Void in + cdm.backgroundContext.syncData(jsonData, withSerializer: serializer) { (error) -> Void in XCTAssertNil(error) + let allClicks = self.cdm.mainContext.managerFor(Click.self).orderBy("clickID").array - let clicks = self.cdm.mainContext.managerFor(Click.self).filter(format: "batch.id = %d", 1).orderBy("clickID").array + let allPoints = self.cdm.mainContext.managerFor(Point.self).orderBy("id").array - XCTAssertEqual(clicks.count, 2, "Clicks count after sync doesn't match") + XCTAssertEqual(allClicks.count, 2, "Clicks count after sync doesn't match") - XCTAssertEqual(clicks[0].clickID, 2, "Second click's ID doesn't match") + XCTAssertEqual(allPoints.count, 2, "Points count after sync doesn't match") - XCTAssertEqual(clicks[1].clickID, 3, "First click's ID doesn't match") + let cilck2Points = self.cdm.mainContext.managerFor(Point.self).filter(format: "click.clickID = %d", 2).orderBy("id").array + + XCTAssertEqual(cilck2Points.count, 1, "Points count after sync doesn't match") + + XCTAssertEqual(cilck2Points.first?.id, 2, "First point's ID doesn't match") expectation.fulfill() } - self.waitForExpectations(timeout: 1) + waitForExpectations(timeout: 10) } func testInsert() { + let expectation = expectation(description: "Inserting batch didn't complete") - let expectation = self.expectation(description: "Inserting batch didn't complete") - - XCTAssertEqual(self.cdm.mainContext.managerFor(Batch.self).count, 0, "Batches count before insert doesn't match") + XCTAssertEqual(cdm.mainContext.managerFor(Batch.self).count, 0, "Batches count before insert doesn't match") let json = JSON(["id": 10, "name": "Batch 10"]) - self.cdm.backgroundContext.insert(Batch.self, withJSON: json) { (error) -> Void in + cdm.backgroundContext.insert(Batch.self, withJSON: json) { (error) -> Void in XCTAssertNil(error) XCTAssertEqual(self.cdm.mainContext.managerFor(Batch.self).count, 1, "Batches count after first insert doesn't match") @@ -140,18 +257,17 @@ class SerializerTestCase: XCTestCase { }) } - self.waitForExpectations(timeout: 1) + waitForExpectations(timeout: 10) } func testInsertOrUpdate() { + let expectation = expectation(description: "InsertOrUpdating batch didn't complete") - let expectation = self.expectation(description: "InsertOrUpdating batch didn't complete") - - XCTAssertEqual(self.cdm.mainContext.managerFor(Batch.self).count, 0, "Batches count before insertOrUpdate doesn't match") + XCTAssertEqual(cdm.mainContext.managerFor(Batch.self).count, 0, "Batches count before insertOrUpdate doesn't match") let json = JSON(["id": 15, "name": "Batch 15"]) - self.cdm.backgroundContext.insertOrUpdate(Batch.self, withJSON: json, andIdentifiers: ["id"]) { (error) -> Void in + cdm.backgroundContext.insertOrUpdate(Batch.self, withJSON: json, andIdentifiers: ["id"]) { (error) -> Void in XCTAssertNil(error) XCTAssertEqual(self.cdm.mainContext.managerFor(Batch.self).count, 1, "Batches count after first insert or update doesn't match") @@ -163,20 +279,32 @@ class SerializerTestCase: XCTestCase { } } - self.waitForExpectations(timeout: 1) + waitForExpectations(timeout: 10) } - } -class ClickSerializer: CDMSerializer { - - override init() { - super.init() - +class ClickSerializer: CDMSerializer { + override init(inverseRelationship: InverseRelationship?) { + super.init(inverseRelationship: inverseRelationship) self.identifiers = ["clickID"] self.mapping = [ "clickID": CDMAttributeNumber(["id"]), "timeStamp": CDMAttributeISODate(["time"]), + "point": CDMAttributeToOne(["point"], serializerCallback: { _ in + PointSerializer(inverseRelationship: InverseRelationship(keyPath: "click", parentSerializer: self)) + }) + ] + } +} + +class PointSerializer: CDMSerializer { + override init(inverseRelationship: InverseRelationship?) { + super.init(inverseRelationship: inverseRelationship) + self.identifiers = ["id"] + self.mapping = [ + "id": CDMAttributeNumber(["id"]), + "x": CDMAttributeNumber(["x"]), + "y": CDMAttributeNumber(["y"]) ] } } diff --git a/Example/Tests/SetupTestCase.swift b/Example/Tests/SetupTestCase.swift index 3a1cc64..c2c117f 100644 --- a/Example/Tests/SetupTestCase.swift +++ b/Example/Tests/SetupTestCase.swift @@ -101,7 +101,7 @@ class SetupTestCase: XCTestCase { let cdm = CoreDataManager() let testDatabaseURL = self.documentsURLForTesting(forTesting: true).appendingPathComponent("TestDatabase.sqlite") - cdm.setupWithModel("CoreDataManager", andFileURL: testDatabaseURL) + cdm.setupWithModel("CoreDataManager", andDatabaseURL: testDatabaseURL) if let modelName = cdm.modelName { XCTAssertEqual(modelName, "CoreDataManager", "CoreDataManager model name is incorrect") diff --git a/Pod/Classes/Attributes.swift b/Pod/Classes/Attributes.swift index 7d38fdb..fdbd832 100644 --- a/Pod/Classes/Attributes.swift +++ b/Pod/Classes/Attributes.swift @@ -9,80 +9,71 @@ import CoreData import SwiftyJSON - open class CDMAttribute { - - fileprivate var key:[JSONSubscriptType] - + fileprivate var key: [JSONSubscriptType] + open var needsContext = false - + public init(_ key: [JSONSubscriptType]) { self.key = key } - + public convenience init(_ args: JSONSubscriptType...) { self.init(args) } - + open func valueAsJSON(_ attributes: JSON? = nil) -> JSON? { return attributes?[key] } - + open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.object + return valueAsJSON(attributes)?.object } - + open func valueFrom(_ attributes: JSON? = nil, inContext context: NSManagedObjectContext) -> Any? { fatalError("Attributes which don't need context need to use valueFrom(attributes: JSON?)") } } - -open class CDMAttributeString:CDMAttribute { +open class CDMAttributeString: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.string + return valueAsJSON(attributes)?.string } } - -open class CDMAttributeBool:CDMAttribute { +open class CDMAttributeBool: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.bool + return valueAsJSON(attributes)?.bool } } - -open class CDMAttributeInt:CDMAttribute { +open class CDMAttributeInt: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.int + return valueAsJSON(attributes)?.int } } - -open class CDMAttributeNumber:CDMAttribute { +open class CDMAttributeNumber: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.number + return valueAsJSON(attributes)?.number } } - -open class CDMAttributeDouble:CDMAttribute { +open class CDMAttributeDouble: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.double + return valueAsJSON(attributes)?.double } } - -open class CDMAttributeFloat:CDMAttribute { +open class CDMAttributeFloat: CDMAttribute { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - return self.valueAsJSON(attributes)?.float + return valueAsJSON(attributes)?.float } } - -open class CDMAttributeISODate:CDMAttributeString { +open class CDMAttributeISODate: CDMAttributeString { override open func valueFrom(_ attributes: JSON? = nil) -> Any? { - if let dateString = super.valueFrom(attributes) as? String , dateString != "" { + if let dateString = super.valueFrom(attributes) as? String, dateString != "" { let formats = ["yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", "yyyy-MM-dd'T'HH:mm:ssZZZZZ"] for format in formats { let dateFormatter = DateFormatter() @@ -98,49 +89,44 @@ open class CDMAttributeISODate:CDMAttributeString { } } - -open class CDMAttributeToMany:CDMAttribute { - +open class CDMAttributeToMany: CDMAttribute { fileprivate var serializerCallback: (JSON) -> (CDMSerializer) - + public init(_ key: [JSONSubscriptType], serializerCallback: @escaping (JSON) -> (CDMSerializer)) { self.serializerCallback = serializerCallback super.init(key) - - self.needsContext = true + + needsContext = true } - + override open func valueFrom(_ attributes: JSON? = nil) -> Any? { fatalError("Managed object attributes need to have a context where to take the value") } - - override open func valueFrom(_ attributes: JSON? = nil, inContext context: NSManagedObjectContext) -> Any? { - if let data = self.valueAsJSON(attributes) { - let serializer = self.serializerCallback(attributes!) + + override open func valueFrom(_ json: JSON? = nil, inContext context: NSManagedObjectContext) -> Any? { + if let json = json, let data = valueAsJSON(json) { + let serializer: CDMSerializer = serializerCallback(json) do { return NSSet(array: try context.syncDataArray(data, withSerializer: serializer, andSave: false)) } catch { return nil } } - + return nil } } - -open class CDMAttributeToOne:CDMAttributeToMany { - - public override init(_ key: [JSONSubscriptType], serializerCallback: @escaping (JSON) -> (CDMSerializer)) { +open class CDMAttributeToOne: CDMAttributeToMany { + override public init(_ key: [JSONSubscriptType], serializerCallback: @escaping (JSON) -> (CDMSerializer)) { super.init(key, serializerCallback: serializerCallback) } - + override open func valueFrom(_ attributes: JSON? = nil, inContext context: NSManagedObjectContext) -> Any? { - - if let objects = super.valueFrom(attributes, inContext: context) as? NSSet , objects.count == 1 { - return objects.allObjects[0] + if let objects = super.valueFrom(attributes, inContext: context) as? NSSet, objects.count == 1 { + return objects.allObjects.first } - + return nil } } diff --git a/Pod/Classes/CoreDataManager.swift b/Pod/Classes/CoreDataManager.swift index 7882351..6b59595 100644 --- a/Pod/Classes/CoreDataManager.swift +++ b/Pod/Classes/CoreDataManager.swift @@ -8,164 +8,125 @@ import CoreData - -open class CoreDataManager:NSObject { - +open class CoreDataManager: NSObject { public static let sharedInstance = CoreDataManager() - fileprivate(set) open var modelName: String? - fileprivate(set) open var databaseURL: URL? + open fileprivate(set) var modelName: String? + open fileprivate(set) var databaseURL: URL? - fileprivate var storeCoordinator: NSPersistentStoreCoordinator? + public var persistentContainer: NSPersistentContainer { + guard let privatePersistentContainer = privatePersistentContainer else { + fatalError("CoreDataManager not set up. Use CoreDataManager.setupWithModel() or CoreDataManager.setupInMemoryWithModel()") + } + return privatePersistentContainer + } + + public private(set) var privatePersistentContainer: NSPersistentContainer? - override public init(){ + override public init() { super.init() - - NotificationCenter.default.addObserver(self, selector: #selector(CoreDataManager.contextDidSaveContext(_:)), name: NSNotification.Name.NSManagedObjectContextDidSave, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } - // MARK: Lazy contexts open lazy var mainContext: NSManagedObjectContext = { - self.getManagedObjectContextWithType(NSManagedObjectContextConcurrencyType.mainQueueConcurrencyType) - }() + return getMainManagedObjectContext() + }() open lazy var backgroundContext: NSManagedObjectContext = { - self.getManagedObjectContextWithType(NSManagedObjectContextConcurrencyType.privateQueueConcurrencyType) - }() - + return getBackgroundManagedObjectContext() + }() // MARK: Other lazy variables fileprivate lazy var applicationDocumentsDirectory: URL = { let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - return urls[urls.count-1] - }() - - fileprivate lazy var managedObjectModel: NSManagedObjectModel = { - // The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model. - if let modelName = self.modelName { - if let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") { - if let model = NSManagedObjectModel(contentsOf: modelURL) { - return model - } - - fatalError("Couldn't find the managed object model with name \(modelName)\nChecked url: \(modelURL)") - } - - fatalError("Couldn't find the managed object model with name \(modelName) in main bundle") - } - - fatalError("CoreDataManager not set up. Use CoreDataManager.setupWithModel() or CoreDataManager.setupInMemoryWithModel()") - }() + return urls[urls.count - 1] + }() } - // MARK: - Setup -extension CoreDataManager { - - public func setupWithModel(_ model: String) { - self.setupWithModel(model, andFileName: model + ".sqlite") +public extension CoreDataManager { + func setupWithModel(_ modelName: String) { + setupWithModel(modelName, andFileName: modelName + ".sqlite") } - public func setupWithModel(_ model: String, andFileName fileName: String) { - let url = self.applicationDocumentsDirectory.appendingPathComponent(fileName) - self.setupWithModel(model, andFileURL: url) + func setupWithModel(_ modelName: String, andFileName fileName: String) { + let url = applicationDocumentsDirectory.appendingPathComponent(fileName) + setupWithModel(modelName, andDatabaseURL: url) } - public func setupWithModel(_ model: String, andFileURL url: URL) { - self.modelName = model + func setupWithModel(_ modelName: String, andDatabaseURL url: URL) { + self.modelName = modelName self.databaseURL = url - - self.setupPersistentStoreCoordinator() + setupPersistentContainer(modelName: modelName, databaseURL: url) } - public func setupInMemoryWithModel(_ model: String) { - self.modelName = model + func setupInMemoryWithModel(_ modelName: String) { + self.modelName = modelName - self.setupInMemoryStoreCoordinator() - } - -} - - -// MARK: - Context saving notifications - -extension CoreDataManager { - - // call back function by saveContext, support multi-thread - @objc func contextDidSaveContext(_ notification: Notification) { - let sender = notification.object as! NSManagedObjectContext - if sender === self.mainContext { - self.backgroundContext.perform { - self.backgroundContext.mergeChanges(fromContextDidSave: notification) - } - } else if sender === self.backgroundContext { - self.mainContext.perform { - self.mainContext.mergeChanges(fromContextDidSave: notification) - } - } + setupInMemoryPersistentContainer(modelName: modelName) } } - // MARK: - Private -extension CoreDataManager { +private extension CoreDataManager { + func getMainManagedObjectContext() -> NSManagedObjectContext { + return persistentContainer.viewContext + } + + func getBackgroundManagedObjectContext() -> NSManagedObjectContext { + let backgroundContext = persistentContainer.newBackgroundContext() + backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + backgroundContext.automaticallyMergesChangesFromParent = true + return backgroundContext + } - fileprivate func getManagedObjectContextWithType(_ type: NSManagedObjectContextConcurrencyType) -> NSManagedObjectContext { - if let coordinator = self.storeCoordinator { - let managedObjectContext = NSManagedObjectContext(concurrencyType: type) - managedObjectContext.persistentStoreCoordinator = coordinator - return managedObjectContext - } else { - fatalError("Store coordinator not set up. Use one of the CoreDataManager.setup() methods") - } + func setupPersistentContainer(modelName: String, databaseURL: URL) { + let container = NSPersistentContainer(name: modelName) + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.automaticallyMergesChangesFromParent = true + container.persistentStoreDescriptions = [getPersistentStoreDescription(databaseURL: databaseURL)] + container.loadPersistentStores(completionHandler: { _, error in + if let error = error as NSError? { + debugPrint("Unresolved error \(error), \(error.userInfo)") + } + }) + privatePersistentContainer = container } - // The persistent store coordinator for the application. This implementation creates a coordinator, having added the store for the application to it. - fileprivate func setupPersistentStoreCoordinator() { - if self.storeCoordinator != nil { - return - } - - let coordinator: NSPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) - if let databaseURL = self.databaseURL { - - do { - let options: [AnyHashable: Any] = [ - NSMigratePersistentStoresAutomaticallyOption : true, - NSInferMappingModelAutomaticallyOption : true, - ] - try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: databaseURL, options: options) - } catch { - fatalError("There was an error adding persistent SQLite store on url \(databaseURL)") + func setupInMemoryPersistentContainer(modelName: String) { + let container = NSPersistentContainer(name: modelName) + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.automaticallyMergesChangesFromParent = true + container.persistentStoreDescriptions = [getMemoryStoreDescription()] + container.loadPersistentStores(completionHandler: { _, error in + if let error = error as NSError? { + debugPrint("Unresolved error \(error), \(error.userInfo)") } - - self.storeCoordinator = coordinator - } else { - fatalError("CoreDataManager not set up. Use CoreDataManager.setupWithModel()") - } - + }) + privatePersistentContainer = container } - fileprivate func setupInMemoryStoreCoordinator() { - if self.storeCoordinator != nil { - return - } - - let coordinator: NSPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) - do { - try coordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil) - self.storeCoordinator = coordinator - } catch { - fatalError("There was an error adding in-memory store") - } + func getPersistentStoreDescription(databaseURL: URL) -> NSPersistentStoreDescription { + let storeDescription = NSPersistentStoreDescription(url: databaseURL) + storeDescription.type = NSSQLiteStoreType + storeDescription.shouldInferMappingModelAutomatically = true + storeDescription.shouldMigrateStoreAutomatically = true + return storeDescription + } + + func getMemoryStoreDescription() -> NSPersistentStoreDescription { + let storeDescription = NSPersistentStoreDescription() + storeDescription.type = NSInMemoryStoreType + storeDescription.shouldInferMappingModelAutomatically = true + storeDescription.shouldMigrateStoreAutomatically = true + return storeDescription } } diff --git a/Pod/Classes/Extensions/NSManagedObject+Extensions.swift b/Pod/Classes/Extensions/NSManagedObject+Extensions.swift new file mode 100644 index 0000000..a9b43b3 --- /dev/null +++ b/Pod/Classes/Extensions/NSManagedObject+Extensions.swift @@ -0,0 +1,23 @@ +// +// NSManagedObject.swift +// Pods +// +// Created by Taavi Teska on 06/09/15. +// +// + +import CoreData + +public extension NSManagedObject { + static var entityName: String { + return NSStringFromClass(self).components(separatedBy: ".").last ?? "" + } + + func delete() { + managedObjectContext?.delete(self) + } + + class func insert(into context: NSManagedObjectContext) -> Self? { + return NSEntityDescription.insertNewObject(forEntityName: Self.entityName, into: context) as? Self + } +} diff --git a/Pod/Classes/Extensions/NSManagedObject.swift b/Pod/Classes/Extensions/NSManagedObject.swift deleted file mode 100644 index aa34280..0000000 --- a/Pod/Classes/Extensions/NSManagedObject.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NSManagedObject.swift -// Pods -// -// Created by Taavi Teska on 06/09/15. -// -// - -import CoreData - - -extension NSManagedObject { - - public func delete() { - if let context = self.managedObjectContext { - context.delete(self) - } - } -} diff --git a/Pod/Classes/Extensions/NSManagedObjectContext+Extensions.swift b/Pod/Classes/Extensions/NSManagedObjectContext+Extensions.swift new file mode 100644 index 0000000..692c1a1 --- /dev/null +++ b/Pod/Classes/Extensions/NSManagedObjectContext+Extensions.swift @@ -0,0 +1,197 @@ +// +// NSManagedObjectContext.swift +// Pods +// +// Created by Taavi Teska on 06/09/15. +// +// + +import CoreData +import SwiftyJSON + +public extension NSManagedObjectContext { + func managerFor(_ entity: T.Type) -> ManagedObjectManager { + return ManagedObjectManager(context: self) + } + + func saveIfChanged() throws { + if self.hasChanges { + try self.save() + } + } + + func insert(_ entity: T.Type, withJSON json: JSON, complete: ((NSError?) -> Void)? = nil) { + let serializer = CDMSerializer() + serializer.forceInsert = true + serializer.deleteMissing = false + + serializer.mapping = [String: CDMAttribute]() + if let dictionary = json.dictionary { + for attr in dictionary.keys { + serializer.mapping[attr] = CDMAttribute(attr) + } + } + + self.syncData(json, withSerializer: serializer, complete: complete) + } + + func insertOrUpdate(_ entity: T.Type, withJSON json: JSON, andIdentifiers identifiers: [String], complete: ((NSError?) -> Void)? = nil) { + let serializer = CDMSerializer() + serializer.deleteMissing = false + serializer.identifiers = identifiers + + serializer.mapping = [String: CDMAttribute]() + if let dictionary = json.dictionary { + for attr in dictionary.keys { + serializer.mapping[attr] = CDMAttribute(attr) + } + } + self.syncData(json, withSerializer: serializer, complete: complete) + } + + func syncData(_ json: JSON, withSerializer serializer: CDMSerializer, complete: ((NSError?) -> Void)? = nil) { + self.perform { () -> Void in + do { + _ = try self.syncDataArray(json, withSerializer: serializer, andSave: true) + + DispatchQueue.main.async { () -> Void in + complete?(nil) + } + } catch let error as NSError { + DispatchQueue.main.async { () -> Void in + complete?(error) + } + } + } + } + + internal func syncDataArray(_ json: JSON, withSerializer serializer: CDMSerializer, andSave save: Bool) throws -> [T] { + if json == JSON.null { + return [] + } + + // Validate data + var validData = json.array != nil ? json : JSON([json]) + + for validator in serializer.getValidators() { + var _dataArray = [JSON]() + for (_, object) in validData { + if let validObject = validator(object), validObject.type != .null { + _dataArray.append(validObject) + } + } + validData = JSON(_dataArray) + } + + if validData.isEmpty, !serializer.deleteMissing { + return [] + } + + // MARK: Delete missing objects + + if serializer.deleteMissing { + var currentKeys = Set() + + for (_, jsonData) in validData { + let objectKey = serializer.identifiers.compactMap { identifier in + if let key = serializer.mapping[identifier]?.valueFrom(jsonData) { + return "\(identifier):\(key)-" + } + return nil + }.joined(separator: "") + currentKeys.insert(objectKey) + } + + var predicates: [NSPredicate] = serializer.parentPredicates() + predicates.append(contentsOf: serializer.getGroupers()) + + let existingObjects = managerFor(T.self).filter(NSCompoundPredicate(andPredicateWithSubpredicates: predicates)).array + for existingObject in existingObjects { + let objectKey = serializer.identifiers.compactMap { identifier in + if let key = existingObject.value(forKeyPath: identifier) { + return "\(identifier):\(key)-" + } + return nil + }.joined(separator: "") + if !currentKeys.contains(objectKey) { + self.delete(existingObject) + } + } + } + + if save { + try self.saveIfChanged() + } + + // MARK: Update or insert objects + + var resultingObjects = [T]() + + for (_, jsonData) in validData { + if serializer.forceInsert, let object = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T { + // TODO: Move insert logic to one place + serializer.addAttributes(jsonData, toObject: object) + resultingObjects.append(object) + } else { + var predicates: [NSPredicate] = serializer.parentPredicates() + for identifier in serializer.identifiers { + if let entityID = serializer.mapping[identifier]?.valueFrom(jsonData) as? Int { + predicates.append(NSPredicate(format: "\(identifier) == %d", entityID)) + } else if let entityID = serializer.mapping[identifier]?.valueFrom(jsonData) as? String { + predicates.append(NSPredicate(format: "\(identifier) == %@", entityID)) + } + } + predicates.append(contentsOf: serializer.getGroupers()) + + let existingObjects = self.managerFor(T.self).filter(NSCompoundPredicate(andPredicateWithSubpredicates: predicates)).array + + if existingObjects.isEmpty { + if serializer.insertMissing, let object = NSEntityDescription.insertNewObject(forEntityName: T.entityName, into: self) as? T { + serializer.addAttributes(jsonData, toObject: object) + resultingObjects.append(object) + } + } else { + for object in existingObjects { + if serializer.updateExisting { + serializer.addAttributes(jsonData, toObject: object) + } + resultingObjects.append(object) + } + } + } + + if save { + try self.save() + } + } + + return resultingObjects + } +} + +private extension CDMSerializer { + func parentPredicates() -> [NSPredicate] { + var predicates: [NSPredicate] = [] + + var parentSerializer: CDMSerializerProtocol? = inverseRelationship?.parentSerializer + var currentSerializer: CDMSerializerProtocol? = self + var inverseRelationship: String = "" + var parentJson = parentSerializer?.json + while let serializer = parentSerializer { + if let currentInverseRelationship = currentSerializer?.inverseRelationship?.keyPath { + inverseRelationship += "\(currentInverseRelationship)." + for identifier in serializer.identifiers { + let parentIdentifier = "\(inverseRelationship)\(identifier)" + if let key: Any = serializer.mapping[identifier]?.valueFrom(parentJson) { + predicates.append(NSPredicate(format: "\(parentIdentifier) == \(key)")) + } + } + } + currentSerializer = parentSerializer + parentSerializer = parentSerializer?.inverseRelationship?.parentSerializer + parentJson = parentSerializer?.json + } + + return predicates + } +} diff --git a/Pod/Classes/Extensions/NSManagedObjectContext.swift b/Pod/Classes/Extensions/NSManagedObjectContext.swift deleted file mode 100644 index 95fbe3c..0000000 --- a/Pod/Classes/Extensions/NSManagedObjectContext.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// NSManagedObjectContext.swift -// Pods -// -// Created by Taavi Teska on 06/09/15. -// -// - -import CoreData -import SwiftyJSON - - -extension NSManagedObjectContext { - - public func managerFor(_ entity:T.Type) -> ManagedObjectManager { - return ManagedObjectManager(context: self) - } - - public func saveIfChanged() throws { - if self.hasChanges { - try self.save() - } - } - - public func insert(_ entity: T.Type, withJSON json: JSON, complete: ((NSError?) -> Void)? = nil) { - let serializer = CDMSerializer() - serializer.forceInsert = true - serializer.deleteMissing = false - - serializer.mapping = [String: CDMAttribute]() - for attr in json.dictionary!.keys { - serializer.mapping[attr] = CDMAttribute(attr) - } - - self.syncData(json, withSerializer: serializer, complete: complete) - } - - public func insertOrUpdate(_ entity: T.Type, withJSON json: JSON, andIdentifiers identifiers: [String], complete: ((NSError?) -> Void)? = nil) { - let serializer = CDMSerializer() - serializer.deleteMissing = false - serializer.identifiers = identifiers - - serializer.mapping = [String: CDMAttribute]() - for attr in json.dictionary!.keys { - serializer.mapping[attr] = CDMAttribute(attr) - } - - self.syncData(json, withSerializer: serializer, complete: complete) - } - - public func syncData(_ json: JSON, withSerializer serializer: CDMSerializer, complete: ((NSError?) -> Void)? = nil) { - self.perform({ () -> Void in - do { - _ = try self.syncDataArray(json, withSerializer: serializer, andSave: true) - - DispatchQueue.main.async(execute: { () -> Void in - complete?(nil) - }) - } catch let error as NSError { - DispatchQueue.main.async(execute: { () -> Void in - complete?(error) - }) - } - }) - } - - func syncDataArray(_ json: JSON, withSerializer serializer: CDMSerializer, andSave save: Bool) throws -> Array { - - if json == JSON.null { - return [] - } - - // Validate data - var validData = json.array != nil ? json : JSON([json]) - - for validator in serializer.getValidators() { - var _dataArray = [JSON]() - for (_, object) in validData { - if let validObject = validator(object) , validObject.type != .null { - _dataArray.append(validObject) - } - } - validData = JSON(_dataArray) - } - - if validData.isEmpty && !serializer.deleteMissing { - return [] - } - - // MARK: Delete missing objects - - if serializer.deleteMissing { - var currentKeys = Set() - - for (_, attributes) in validData { - var currentKey = "" - for identifier in serializer.identifiers { - if let key: Any = serializer.mapping[identifier]!.valueFrom(attributes) { - currentKey += "\(key)-" - } - } - currentKeys.insert(currentKey) - } - - let existingObjects = self.managerFor(T.self).filter(NSCompoundPredicate(andPredicateWithSubpredicates: serializer.getGroupers())).array - for existingObject in existingObjects { - var objectKey = "" - - for identifier in serializer.identifiers { - if let key = existingObject.value(forKeyPath: identifier) { - objectKey += "\(key)-" - } - } - - if !currentKeys.contains(objectKey) { - self.delete(existingObject) - } - } - } - - if save { - try self.saveIfChanged() - } - - - // MARK: Update or insert objects - - var resultingObjects = [T]() - - for (_, attributes) in validData { - if serializer.forceInsert { - // TODO: Move insert logic to one place - let object = NSEntityDescription.insertNewObject(forEntityName: self.managerFor(T.self).entityName(), into: self) as! T - serializer.addAttributes(attributes, toObject: object) - resultingObjects.append(object) - } else { - var predicates = [NSPredicate]() - - for identifier in serializer.identifiers { - if let entityID = serializer.mapping[identifier]!.valueFrom(attributes) as? Int { - predicates.append(NSPredicate(format: "\(identifier) == %d", entityID)) - } else if let entityID = serializer.mapping[identifier]!.valueFrom(attributes) as? String { - predicates.append(NSPredicate(format: "\(identifier) == %@", entityID)) - } - } - - predicates.append(contentsOf: serializer.getGroupers()) - let existingObjects = self.managerFor(T.self).filter(NSCompoundPredicate(andPredicateWithSubpredicates: predicates)).array - - if existingObjects.isEmpty { - if serializer.insertMissing { - let object = NSEntityDescription.insertNewObject(forEntityName: self.managerFor(T.self).entityName(), into: self) as! T - serializer.addAttributes(attributes, toObject: object) - resultingObjects.append(object) - } - } else { - for object in existingObjects { - if serializer.updateExisting { - serializer.addAttributes(attributes, toObject: object) - } - resultingObjects.append(object) - } - } - } - - if save { - try self.save() - } - } - - return resultingObjects - } -} diff --git a/Pod/Classes/ManagedObjectManager.swift b/Pod/Classes/ManagedObjectManager.swift index 8ac4a59..2c5543b 100644 --- a/Pod/Classes/ManagedObjectManager.swift +++ b/Pod/Classes/ManagedObjectManager.swift @@ -8,9 +8,8 @@ import CoreData -open class ManagedObjectManager { - - fileprivate var context: NSManagedObjectContext! +open class ManagedObjectManager { + fileprivate let context: NSManagedObjectContext fileprivate var managerPredicate: NSPredicate? fileprivate var managerFetchLimit: Int? @@ -19,108 +18,84 @@ open class ManagedObjectManager { init(context: NSManagedObjectContext) { self.context = context } - - - // MARK: Entity - + open func entityName() -> String { - - return NSStringFromClass(T.self).components(separatedBy: ".").last! - + return T.entityName } - } -//MARK: - Filtering +// MARK: - Filtering -extension ManagedObjectManager { - - public func filter(format predicateFormat: String, _ args: CVarArg...) -> ManagedObjectManager { - +public extension ManagedObjectManager { + func filter(format predicateFormat: String, _ args: CVarArg...) -> ManagedObjectManager { return withVaList(args) { - self.filter(format: predicateFormat, arguments: $0) + filter(format: predicateFormat, arguments: $0) } - } - public func filter(format predicateFormat: String, argumentArray arguments: [AnyObject]?) -> ManagedObjectManager { - - return self.filter(NSPredicate(format: predicateFormat, argumentArray: arguments)) - + func filter(format predicateFormat: String, argumentArray arguments: [AnyObject]?) -> ManagedObjectManager { + return filter(NSPredicate(format: predicateFormat, argumentArray: arguments)) } - public func filter(format predicateFormat: String, arguments argList: CVaListPointer) -> ManagedObjectManager { - - return self.filter(NSPredicate(format: predicateFormat, arguments: argList)) - + func filter(format predicateFormat: String, arguments argList: CVaListPointer) -> ManagedObjectManager { + return filter(NSPredicate(format: predicateFormat, arguments: argList)) } - public func filter(_ predicate: NSPredicate) -> ManagedObjectManager { - + func filter(_ predicate: NSPredicate) -> ManagedObjectManager { if let currentPredicate = managerPredicate { - self.managerPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [currentPredicate, predicate]) + managerPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [currentPredicate, predicate]) } else { - self.managerPredicate = predicate + managerPredicate = predicate } return self - } - } -//MARK: - Ordering +// MARK: - Ordering -extension ManagedObjectManager { - - public func orderBy(_ argument: String) -> ManagedObjectManager { - +public extension ManagedObjectManager { + func orderBy(_ argument: String) -> ManagedObjectManager { let isAscending = !argument.hasPrefix("-") let key = isAscending ? argument : String(argument[argument.index(argument.startIndex, offsetBy: 1)...]) - - self.managerSortDescriptors.append(NSSortDescriptor(key: key, ascending: isAscending)) - + managerSortDescriptors.append(NSSortDescriptor(key: key, ascending: isAscending)) return self - } - public func orderBy(_ arguments: [String]) -> ManagedObjectManager { - + func orderBy(_ arguments: [String]) -> ManagedObjectManager { for arg in arguments { - _ = self.orderBy(arg) + _ = orderBy(arg) } return self - } - } -//MARK: - Aggregation +// MARK: - Aggregation -extension ManagedObjectManager { +public extension ManagedObjectManager { + var count: Int? { + let fetchRequest = createFetchRequest() + return try? context.count(for: fetchRequest) + } - public var count: Int? { - get { - let fetchRequest = self.fetchRequest() - - return try? self.context.count(for: fetchRequest) - } + func min(_ keyPath: String) -> Any? { + return aggregate("min", forKeyPath: keyPath) } - public func min(_ keyPath: String) -> Any? { - return self.aggregate("min", forKeyPath: keyPath) + func max(_ keyPath: String) -> Any? { + return aggregate("max", forKeyPath: keyPath) } - public func max(_ keyPath: String) -> Any? { - return self.aggregate("max", forKeyPath: keyPath) + func sum(_ keyPath: String) -> Any? { + return aggregate("sum", forKeyPath: keyPath) } - public func sum(_ keyPath: String) -> Any? { - return self.aggregate("sum", forKeyPath: keyPath) + func average(_ keyPath: String) -> Any? { + return aggregate("average", forKeyPath: keyPath) } - public func aggregate(_ functionName: String, forKeyPath keyPath: String) -> Any? { + func aggregate(_ functionName: String, forKeyPath keyPath: String) -> Any? { let expression = NSExpression(forFunction: functionName + ":", arguments: [NSExpression(forKeyPath: keyPath)]) let expressionName = functionName + keyPath @@ -128,80 +103,62 @@ extension ManagedObjectManager { expressionDescription.name = expressionName expressionDescription.expression = expression - var entityDescription = NSEntityDescription.entity(forEntityName: self.entityName(), in: self.context)! + guard var entityDescription = NSEntityDescription.entity(forEntityName: self.entityName(), in: context) else { return nil } + var keyPathArray = keyPath.components(separatedBy: ".") let lastKey = keyPathArray.removeLast() for key in keyPathArray { - let relationshipDesc = entityDescription.propertiesByName[key] as! NSRelationshipDescription - entityDescription = relationshipDesc.destinationEntity! + if let destinationEntity = (entityDescription.propertiesByName[key] as? NSRelationshipDescription)?.destinationEntity { + entityDescription = destinationEntity + } } - let entityAttributeDesc = entityDescription.attributesByName[lastKey]! - expressionDescription.expressionResultType = entityAttributeDesc.attributeType - - // Since we are changing expressionResultType we also need to check if there are any objects returned - - var fetchRequest = self.fetchRequest() - guard let objectCount = self.count else { - return nil + if let entityAttributeDesc = entityDescription.attributesByName[lastKey] { + expressionDescription.expressionResultType = entityAttributeDesc.attributeType } - if objectCount == 0 { - return nil - } + // Since we are changing expressionResultType we also need to check if there are any objects returned - fetchRequest = self.fetchRequest() - fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType + let fetchRequest = createFetchRequest() + guard let objectCount = count, objectCount > 0 else { return nil } + fetchRequest.resultType = .dictionaryResultType fetchRequest.propertiesToFetch = [expressionDescription] do { - let results = try self.context.fetch(fetchRequest) as [AnyObject] - if results.count > 0 { - return results[0].value(forKey: expressionName) - } + let results = try context.fetch(fetchRequest) as [AnyObject] + return results.first?.value(forKey: expressionName) } catch { return nil } - - return nil } - } -//MARK: - Fetching +// MARK: - Fetching -extension ManagedObjectManager { - - public var array:[T] { - get { - return self.resultsWithPredicate(self.managerPredicate, andSortDescriptors: self.managerSortDescriptors, withFetchLimit: self.managerFetchLimit) - } +public extension ManagedObjectManager { + var array: [T] { + return resultsWithPredicate(managerPredicate, andSortDescriptors: managerSortDescriptors, withFetchLimit: managerFetchLimit) } - public var first: T? { - get { - return self.resultsWithPredicate(self.managerPredicate, andSortDescriptors: self.managerSortDescriptors, withFetchLimit: 1).first - } + var first: T? { + return resultsWithPredicate(managerPredicate, andSortDescriptors: managerSortDescriptors, withFetchLimit: 1).first } - public var last: T? { - get { - var sortDescriptors = [NSSortDescriptor]() - for sortDescriptor in self.managerSortDescriptors { - sortDescriptors.append(NSSortDescriptor(key: sortDescriptor.key!, ascending: !sortDescriptor.ascending)) - } - return self.resultsWithPredicate(self.managerPredicate, andSortDescriptors: sortDescriptors, withFetchLimit: 1).first + var last: T? { + var sortDescriptors = [NSSortDescriptor]() + for sortDescriptor in managerSortDescriptors { + sortDescriptors.append(NSSortDescriptor(key: sortDescriptor.key!, ascending: !sortDescriptor.ascending)) } + return resultsWithPredicate(managerPredicate, andSortDescriptors: sortDescriptors, withFetchLimit: 1).first } - } -//MARK: - Deleting +// MARK: - Deleting -extension ManagedObjectManager { - - public func delete() -> Int { - let results = self.resultsWithPredicate(self.managerPredicate, andSortDescriptors: self.managerSortDescriptors, withFetchLimit: self.managerFetchLimit) +public extension ManagedObjectManager { + @discardableResult + func delete() -> Int { + let results = resultsWithPredicate(managerPredicate, andSortDescriptors: managerSortDescriptors, withFetchLimit: managerFetchLimit) let resultsCount = results.count for result in results { @@ -210,25 +167,19 @@ extension ManagedObjectManager { return resultsCount } - } -//MARK: - Private methods +// MARK: - Private methods -extension ManagedObjectManager { - - +private extension ManagedObjectManager { // MARK: Fetch requests - fileprivate func fetchRequest() -> NSFetchRequest { - - return self.fetchRequestWithPredicate(self.managerPredicate, andSortDescriptors: self.managerSortDescriptors, withFetchLimit: self.managerFetchLimit) - + func createFetchRequest() -> NSFetchRequest { + return fetchRequestWithPredicate(managerPredicate, andSortDescriptors: managerSortDescriptors, withFetchLimit: managerFetchLimit) } - fileprivate func fetchRequestWithPredicate(_ predicate: NSPredicate?, andSortDescriptors sortDescriptors: [NSSortDescriptor], withFetchLimit limit: Int?) -> NSFetchRequest { - - let fetchRequest = NSFetchRequest(entityName: self.entityName()) + func fetchRequestWithPredicate(_ predicate: NSPredicate?, andSortDescriptors sortDescriptors: [NSSortDescriptor], withFetchLimit limit: Int?) -> NSFetchRequest { + let fetchRequest = NSFetchRequest(entityName: entityName()) fetchRequest.predicate = predicate fetchRequest.sortDescriptors = sortDescriptors @@ -237,32 +188,16 @@ extension ManagedObjectManager { } return fetchRequest - } - // MARK: Results - fileprivate func results() -> [T] { - - do { - return try self.context.fetch(self.fetchRequest()) as! [T] - } catch { - return [] - } - + func results() -> [T] { + return (try? context.fetch(createFetchRequest()) as? [T]) ?? [] } - fileprivate func resultsWithPredicate(_ predicate: NSPredicate?, andSortDescriptors sortDescriptors: [NSSortDescriptor], withFetchLimit limit: Int?) -> [T] { - - let fetchRequest = self.fetchRequestWithPredicate(predicate, andSortDescriptors: sortDescriptors, withFetchLimit: limit) - - do { - return try self.context.fetch(fetchRequest) as! [T] - } catch { - return [] - } - + func resultsWithPredicate(_ predicate: NSPredicate?, andSortDescriptors sortDescriptors: [NSSortDescriptor], withFetchLimit limit: Int?) -> [T] { + let fetchRequest = fetchRequestWithPredicate(predicate, andSortDescriptors: sortDescriptors, withFetchLimit: limit) + return (try? context.fetch(fetchRequest) as? [T]) ?? [] } - } diff --git a/Pod/Classes/Serializers.swift b/Pod/Classes/Serializers.swift index 9f81fb1..62e5ca1 100644 --- a/Pod/Classes/Serializers.swift +++ b/Pod/Classes/Serializers.swift @@ -9,47 +9,62 @@ import CoreData import SwiftyJSON +public typealias CDMValidator = (_ data: JSON) -> JSON? -public typealias CDMValidator = (_ data:JSON) -> JSON? +open class InverseRelationship { + public let keyPath: String? + public let parentSerializer: CDMSerializerProtocol? + + public init(keyPath: String?, parentSerializer: CDMSerializerProtocol?) { + self.keyPath = keyPath + self.parentSerializer = parentSerializer + } +} +public protocol CDMSerializerProtocol { + var mapping: [String: CDMAttribute] { get set } + var identifiers: [String] { get set } + var inverseRelationship: InverseRelationship? { get set } + var json: JSON? { get set } +} -open class CDMSerializer { - +open class CDMSerializer: CDMSerializerProtocol { + public var inverseRelationship: InverseRelationship? open var identifiers = [String]() open var forceInsert = false open var insertMissing = true open var updateExisting = true open var deleteMissing = true open var mapping: [String: CDMAttribute] - - public init() { + public var json: JSON? + + public init(inverseRelationship: InverseRelationship? = nil) { self.mapping = [String: CDMAttribute]() + self.inverseRelationship = inverseRelationship } - + open func getValidators() -> [CDMValidator] { return [CDMValidator]() } - + open func getGroupers() -> [NSPredicate] { return [NSPredicate]() } - - open func addAttributes(_ attributes: JSON, toObject object: NSManagedObject) { + + open func addAttributes(_ json: JSON, toObject object: NSManagedObject) { + self.json = json for (key, attribute) in self.mapping { var newValue: Any? if attribute.needsContext { if let context = object.managedObjectContext { - newValue = attribute.valueFrom(attributes, inContext: context) + newValue = attribute.valueFrom(json, inContext: context) } else { fatalError("Object has to have a managed object context") } } else { - newValue = attribute.valueFrom(attributes) + newValue = attribute.valueFrom(json) } object.setValue(newValue, forKey: key) } } } - - -