From 45a65d926214950bcc1853566c43c6c3a0c03269 Mon Sep 17 00:00:00 2001 From: John Rommel Estropia Date: Sat, 13 Dec 2014 10:06:16 +0900 Subject: [PATCH] testing some ways to make querying as elegant as possible --- HardcoreData.xcodeproj/project.pbxproj | 51 ++++- HardcoreData/DataStack.swift | 18 +- HardcoreData/DataTransaction.swift | 66 ++++++- HardcoreData/HardcoreData.swift | 136 ++++++++++--- .../NSManagedObject+HardcoreData.swift | 58 +++++- .../NSManagedObjectContext+HardcoreData.swift | 178 ++++++++++-------- HardcoreData/Query.swift | 123 ++++++++++++ HardcoreData/QueryDescriptor.swift | 30 --- HardcoreData/Queryable.swift | 38 ++++ HardcoreDataTests/HardcoreDataTests.swift | 33 +++- .../Model.xcdatamodel/contents | 12 ++ HardcoreDataTests/TestEntity1.swift | 36 ++++ 12 files changed, 604 insertions(+), 175 deletions(-) create mode 100644 HardcoreData/Query.swift delete mode 100644 HardcoreData/QueryDescriptor.swift create mode 100644 HardcoreData/Queryable.swift create mode 100644 HardcoreDataTests/Model.xcdatamodeld/Model.xcdatamodel/contents create mode 100644 HardcoreDataTests/TestEntity1.swift diff --git a/HardcoreData.xcodeproj/project.pbxproj b/HardcoreData.xcodeproj/project.pbxproj index 897dfae..f263d74 100644 --- a/HardcoreData.xcodeproj/project.pbxproj +++ b/HardcoreData.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ B5CFF24019FD383100D6DFC4 /* DataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */; }; B5D1E22A19FA9E63003B2874 /* PersistentStoreResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D1E22919FA9E63003B2874 /* PersistentStoreResult.swift */; }; B5D1E22C19FA9FBC003B2874 /* NSError+HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D1E22B19FA9FBC003B2874 /* NSError+HardcoreData.swift */; }; + B5D372841A39CD6900F583D9 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5D372821A39CD6900F583D9 /* Model.xcdatamodeld */; }; + B5D372861A39CDDB00F583D9 /* TestEntity1.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D372851A39CDDB00F583D9 /* TestEntity1.swift */; }; B5D399F119FC818E000E91BB /* DataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D399F019FC818E000E91BB /* DataStack.swift */; }; B5D399F519FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D399F419FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift */; }; B5D39A0219FD00C9000E91BB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5D39A0119FD00C9000E91BB /* Foundation.framework */; }; @@ -24,10 +26,18 @@ B5D808161A34947300A44484 /* NotificationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D808151A34947300A44484 /* NotificationObserver.swift */; }; B5D8081A1A3495BD00A44484 /* NSObject+HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D808191A3495BD00A44484 /* NSObject+HardcoreData.swift */; }; B5E209E01A0726460089C9D4 /* NSManagedObject+HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E209DF1A0726460089C9D4 /* NSManagedObject+HardcoreData.swift */; }; - B5F539901A17A6FC00EC763B /* QueryDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F5398F1A17A6FC00EC763B /* QueryDescriptor.swift */; }; + B5E472271A35E84700804CE1 /* Queryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E472261A35E84700804CE1 /* Queryable.swift */; }; + B5F539901A17A6FC00EC763B /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F5398F1A17A6FC00EC763B /* Query.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B5D372871A39CF4D00F583D9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2F03A52719C5C6DA005002A5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2F03A52F19C5C6DA005002A5; + remoteInfo = HardcoreData; + }; B5D808011A34715700A44484 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B5D806C51A34715700A44484 /* GCDKit.xcodeproj */; @@ -58,6 +68,8 @@ B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataTransaction.swift; sourceTree = ""; }; B5D1E22919FA9E63003B2874 /* PersistentStoreResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentStoreResult.swift; sourceTree = ""; }; B5D1E22B19FA9FBC003B2874 /* NSError+HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSError+HardcoreData.swift"; sourceTree = ""; }; + B5D372831A39CD6900F583D9 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + B5D372851A39CDDB00F583D9 /* TestEntity1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestEntity1.swift; sourceTree = ""; }; B5D399F019FC818E000E91BB /* DataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataStack.swift; sourceTree = ""; }; B5D399F419FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSPersistentStoreCoordinator+HardcoreData.swift"; sourceTree = ""; }; B5D39A0119FD00C9000E91BB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -66,8 +78,9 @@ B5D808151A34947300A44484 /* NotificationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationObserver.swift; sourceTree = ""; }; B5D808191A3495BD00A44484 /* NSObject+HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+HardcoreData.swift"; sourceTree = ""; }; B5E209DF1A0726460089C9D4 /* NSManagedObject+HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+HardcoreData.swift"; sourceTree = ""; }; + B5E472261A35E84700804CE1 /* Queryable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queryable.swift; sourceTree = ""; }; B5F3D98419F3EB8E009690A6 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - B5F5398F1A17A6FC00EC763B /* QueryDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryDescriptor.swift; sourceTree = ""; }; + B5F5398F1A17A6FC00EC763B /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -116,6 +129,7 @@ isa = PBXGroup; children = ( B5D399F019FC818E000E91BB /* DataStack.swift */, + B5E472261A35E84700804CE1 /* Queryable.swift */, B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */, 2F03A53519C5C6DA005002A5 /* HardcoreData.h */, 2F291E2619C6D3CF007AF63F /* HardcoreData.swift */, @@ -123,7 +137,7 @@ B5E209DF1A0726460089C9D4 /* NSManagedObject+HardcoreData.swift */, B5CFF23D19FD1D1C00D6DFC4 /* NSManagedObjectContext+HardcoreData.swift */, B5D399F419FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift */, - B5F5398F1A17A6FC00EC763B /* QueryDescriptor.swift */, + B5F5398F1A17A6FC00EC763B /* Query.swift */, B5D1E22919FA9E63003B2874 /* PersistentStoreResult.swift */, B5CFD36D1A0775F000B7885F /* SaveResult.swift */, B5D808141A34945A00A44484 /* Internal */, @@ -145,6 +159,7 @@ isa = PBXGroup; children = ( 2F03A53F19C5C6DA005002A5 /* HardcoreDataTests.swift */, + B5D372851A39CDDB00F583D9 /* TestEntity1.swift */, 2F03A53D19C5C6DA005002A5 /* Supporting Files */, ); path = HardcoreDataTests; @@ -153,6 +168,7 @@ 2F03A53D19C5C6DA005002A5 /* Supporting Files */ = { isa = PBXGroup; children = ( + B5D372821A39CD6900F583D9 /* Model.xcdatamodeld */, 2F03A53E19C5C6DA005002A5 /* Info.plist */, ); name = "Supporting Files"; @@ -245,6 +261,7 @@ buildRules = ( ); dependencies = ( + B5D372881A39CF4D00F583D9 /* PBXTargetDependency */, ); name = HardcoreDataTests; productName = HardcoreDataTests; @@ -332,7 +349,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B5F539901A17A6FC00EC763B /* QueryDescriptor.swift in Sources */, + B5F539901A17A6FC00EC763B /* Query.swift in Sources */, B5CFF24019FD383100D6DFC4 /* DataTransaction.swift in Sources */, B5D399F519FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift in Sources */, B5CFD36E1A0775F000B7885F /* SaveResult.swift in Sources */, @@ -342,6 +359,7 @@ B5D8081A1A3495BD00A44484 /* NSObject+HardcoreData.swift in Sources */, B5E209E01A0726460089C9D4 /* NSManagedObject+HardcoreData.swift in Sources */, B5D1E22A19FA9E63003B2874 /* PersistentStoreResult.swift in Sources */, + B5E472271A35E84700804CE1 /* Queryable.swift in Sources */, B5D399F119FC818E000E91BB /* DataStack.swift in Sources */, B5D808161A34947300A44484 /* NotificationObserver.swift in Sources */, ); @@ -352,11 +370,21 @@ buildActionMask = 2147483647; files = ( 2F03A54019C5C6DA005002A5 /* HardcoreDataTests.swift in Sources */, + B5D372861A39CDDB00F583D9 /* TestEntity1.swift in Sources */, + B5D372841A39CD6900F583D9 /* Model.xcdatamodeld in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + B5D372881A39CF4D00F583D9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2F03A52F19C5C6DA005002A5 /* HardcoreData */; + targetProxy = B5D372871A39CF4D00F583D9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 2F03A54119C5C6DA005002A5 /* Debug */ = { isa = XCBuildConfiguration; @@ -436,6 +464,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.1; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -482,7 +511,6 @@ INFOPLIST_FILE = HardcoreData/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; }; @@ -549,6 +577,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + B5D372821A39CD6900F583D9 /* Model.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + B5D372831A39CD6900F583D9 /* Model.xcdatamodel */, + ); + currentVersion = B5D372831A39CD6900F583D9 /* Model.xcdatamodel */; + path = Model.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 2F03A52719C5C6DA005002A5 /* Project object */; } diff --git a/HardcoreData/DataStack.swift b/HardcoreData/DataStack.swift index 757a58a..f6ced52 100644 --- a/HardcoreData/DataStack.swift +++ b/HardcoreData/DataStack.swift @@ -30,7 +30,7 @@ import GCDKit private let applicationSupportDirectory = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask).first as NSURL -private let applicationName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName") as String +private let applicationName = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleName") as? String ?? "CoreData" /** @@ -45,7 +45,7 @@ public class DataStack: NSObject { */ public convenience override init() { - self.init(managedObjectModel: NSManagedObjectModel.mergedModelFromBundles(nil)!) + self.init(managedObjectModel: NSManagedObjectModel.mergedModelFromBundles(NSBundle.allBundles())!) } /** @@ -207,10 +207,10 @@ public class DataStack: NSObject { fileManager.removeItemAtURL(fileURL, error: nil) fileManager.removeItemAtPath( - fileURL.absoluteString!.stringByAppendingString("-shm"), + fileURL.path!.stringByAppendingString("-shm"), error: nil) fileManager.removeItemAtPath( - fileURL.absoluteString!.stringByAppendingString("-wal"), + fileURL.path!.stringByAppendingString("-wal"), error: nil) var store: NSPersistentStore? @@ -256,11 +256,10 @@ public class DataStack: NSObject { */ public func performTransaction(closure: (transaction: DataTransaction) -> ()) { - let transaction = DataTransaction( + DataTransaction( mainContext: self.mainContext, queue: self.transactionQueue, - closure: closure) - transaction.perform() + closure: closure).perform() } /** @@ -271,11 +270,10 @@ public class DataStack: NSObject { */ public func performTransactionAndWait(closure: (transaction: DataTransaction) -> ()) -> SaveResult { - let transaction = DataTransaction( + return DataTransaction( mainContext: self.mainContext, queue: self.transactionQueue, - closure: closure) - return transaction.performAndWait() + closure: closure).performAndWait() } // MARK: - Internal diff --git a/HardcoreData/DataTransaction.swift b/HardcoreData/DataTransaction.swift index 31433dc..9da58c8 100644 --- a/HardcoreData/DataTransaction.swift +++ b/HardcoreData/DataTransaction.swift @@ -50,7 +50,7 @@ public class DataTransaction { public func create(entity: T.Type) -> T { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to create an NSManagedObject outside a transaction queue.") - HardcoreData.assert(!self.isTransactionCommited, "Attempted to create an NSManagedObject from an already commited DataTransaction.") + HardcoreData.assert(!self.isCommitted, "Attempted to create an NSManagedObject from an already committed DataTransaction.") return T.createInContext(self.context) } @@ -63,7 +63,7 @@ public class DataTransaction { public func update(object: T) -> T? { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to update an NSManagedObject outside a transaction queue.") - HardcoreData.assert(!self.isTransactionCommited, "Attempted to update an NSManagedObject from an already commited DataTransaction.") + HardcoreData.assert(!self.isCommitted, "Attempted to update an NSManagedObject from an already committed DataTransaction.") return object.inContext(self.context) } @@ -75,12 +75,22 @@ public class DataTransaction { public func delete(object: NSManagedObject) { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to delete an NSManagedObject outside a transaction queue.") - HardcoreData.assert(!self.isTransactionCommited, "Attempted to delete an NSManagedObject from an already commited DataTransaction.") + HardcoreData.assert(!self.isCommitted, "Attempted to delete an NSManagedObject from an already committed DataTransaction.") object.deleteFromContext() } // MARK: Saving changes + /** + Rolls back the transaction by resetting the NSManagedObjectContext. Note that after calling this method, all NSManagedObjects fetched within the transaction will become invalid. + */ + public func rollback() { + + HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to rollback a DataTransaction outside a transaction queue.") + HardcoreData.assert(!self.isCommitted, "Attempted to rollback an already committed DataTransaction.") + self.context.reset() + } + /** Saves the transaction changes asynchronously. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. @@ -89,9 +99,9 @@ public class DataTransaction { public func commit(completion: (result: SaveResult) -> ()) { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to commit a DataTransaction outside a transaction queue.") - HardcoreData.assert(!self.isTransactionCommited, "Attempted to commit a DataTransaction more than once.") + HardcoreData.assert(!self.isCommitted, "Attempted to commit a DataTransaction more than once.") - self.isTransactionCommited = true + self.isCommitted = true self.context.saveAsynchronouslyWithCompletion { [weak self] (result) -> () in self?.result = result @@ -107,9 +117,9 @@ public class DataTransaction { public func commitAndWait() -> SaveResult { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext() == true, "Attempted to commit a DataTransaction outside a transaction queue.") - HardcoreData.assert(!self.isTransactionCommited, "Attempted to commit a DataTransaction more than once.") + HardcoreData.assert(!self.isCommitted, "Attempted to commit a DataTransaction more than once.") - self.isTransactionCommited = true + self.isCommitted = true let result = self.context.saveSynchronously() self.result = result return result @@ -131,7 +141,7 @@ public class DataTransaction { self.transactionQueue.barrierAsync { self.closure(transaction: self) - if !self.isTransactionCommited { + if !self.isCommitted { self.commit { (result) -> () in } } @@ -143,7 +153,7 @@ public class DataTransaction { self.transactionQueue.barrierSync { self.closure(transaction: self) - if !self.isTransactionCommited { + if !self.isCommitted { self.commitAndWait() } @@ -154,9 +164,45 @@ public class DataTransaction { // MARK: - Private - private var isTransactionCommited = false + private var isCommitted = false private var result: SaveResult? private let mainContext: NSManagedObjectContext private let transactionQueue: GCDQueue private let closure: (transaction: DataTransaction) -> () +} + + +// MARK: - DataContextProvider + +extension DataTransaction: Queryable { + + public func findFirst(entity: T.Type) -> T? { + + return self.context.findFirst(entity) + } + + public func findFirst(query: Query) -> T? { + + return self.context.findFirst(query) + } + + public func findAll(entity: T.Type) -> [T]? { + + return self.context.findAll(entity) + } + + public func findAll(query: Query) -> [T]? { + + return self.context.findAll(query) + } + + public func count(entity: T.Type) -> Int { + + return self.context.count(entity) + } + + public func count(query: Query) -> Int { + + return self.context.count(query) + } } \ No newline at end of file diff --git a/HardcoreData/HardcoreData.swift b/HardcoreData/HardcoreData.swift index f4afcf4..20f9048 100644 --- a/HardcoreData/HardcoreData.swift +++ b/HardcoreData/HardcoreData.swift @@ -24,6 +24,8 @@ // import CoreData +import GCDKit + /** HardcoreData - Simple, elegant, and smart Core Data management with Swift @@ -35,34 +37,28 @@ public struct HardcoreData { /** The default DataStack instance to be used. If defaultStack is not set before the first time accessed, a default-configured DataStack will be created. - Note that changing the defaultStack is not thread safe. + Changing the defaultStack is thread safe. */ - public static var defaultStack = DataStack() - - /** - The closure that handles all errors that occur within HardcoreData. The default errorHandler logs errors via the logHandler closure. - */ - public static var errorHandler = { (error: NSError, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in - - HardcoreData.logHandler("\(message): \(error)", fileName, lineNumber, functionName) - } - - /** - The closure that handles all assertions that occur within HardcoreData. The default assertHandler calls assert(). - */ - public static var assertHandler = { (condition: @autoclosure() -> Bool, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in + public static var defaultStack: DataStack { - assert(condition, message, file: fileName, line: lineNumber) - } - - /** - The closure that handles all logging that occur within HardcoreData. The default logHandler logs via println() when DEBUG is defined; does nothing otherwise. - */ - public static var logHandler = { (message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in + get { - #if DEBUG - println("[\(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL))] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n\(message)") - #endif + self.defaultStackBarrierQueue.barrierSync { + + if self.defaultStackInstance == nil { + + self.defaultStackInstance = DataStack() + } + } + return self.defaultStackInstance! + } + set { + + self.defaultStackBarrierQueue.barrierAsync { + + self.defaultStackInstance = newValue + } + } } /** @@ -86,14 +82,100 @@ public struct HardcoreData { return self.defaultStack.performTransactionAndWait(closure) } + + public enum LogLevel { + + case Trace + case Notice + case Alert + case Fatal + } + + public typealias LogHandlerType = (level: LogLevel, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () + + public typealias ErrorHandlerType = (error: NSError, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () + + public typealias AssertionHandlerType = (condition: @autoclosure() -> Bool, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () + + + /** + Sets the closure that handles all logging that occur within HardcoreData. The default logHandler logs via println() only when DEBUG is defined. + */ + public static func setLogHandler(logHandler: LogHandlerType) { + + self.logHandler = logHandler + } + + /** + Sets the closure that handles all errors that occur within HardcoreData. The default errorHandler logs via println() only when DEBUG is defined. + */ + public static func setErrorHandler(errorHandler: ErrorHandlerType) { + + self.errorHandler = errorHandler + } + + /** + Sets the closure that handles all assertions that occur within HardcoreData. The default assertHandler calls assert(). + */ + public static func setAssertionHandler(assertionHandler: AssertionHandlerType) { + + self.assertionHandler = assertionHandler + } + + internal static func log(level: LogLevel, message: String, fileName: StaticString = __FILE__, lineNumber: UWord = __LINE__, functionName: StaticString = __FUNCTION__) { + + self.logHandler( + level: level, + message: message, + fileName: fileName, + lineNumber: lineNumber, + functionName: functionName) + } + internal static func handleError(error: NSError, _ message: String, fileName: StaticString = __FILE__, lineNumber: UWord = __LINE__, functionName: StaticString = __FUNCTION__) { - self.errorHandler(error, message, fileName, lineNumber, functionName) + self.errorHandler( + error: error, + message: message, + fileName: fileName, + lineNumber: lineNumber, + functionName: functionName) } internal static func assert(condition: @autoclosure() -> Bool, _ message: String, fileName: StaticString = __FILE__, lineNumber: UWord = __LINE__, functionName: StaticString = __FUNCTION__) { - self.assertHandler(condition, message, fileName, lineNumber, functionName) + self.assertionHandler( + condition: condition, + message: message, + fileName: fileName, + lineNumber: lineNumber, + functionName: functionName) + } + + + private static let defaultStackBarrierQueue = GCDQueue.createConcurrent("com.hardcoredata.defaultstackbarrierqueue") + + private static var defaultStackInstance: DataStack? + + private static var logHandler: LogHandlerType = { (level: LogLevel, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in + + #if DEBUG + println("[HardcoreData] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") + #endif + } + + private static var errorHandler: ErrorHandlerType = { (error: NSError, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in + + #if DEBUG + println("[HardcoreData] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n") + #endif + } + + private static var assertionHandler: AssertionHandlerType = { (condition: @autoclosure() -> Bool, message: String, fileName: StaticString, lineNumber: UWord, functionName: StaticString) -> () in + + #if DEBUG + assert(condition, message, file: fileName, line: lineNumber) + #endif } } diff --git a/HardcoreData/NSManagedObject+HardcoreData.swift b/HardcoreData/NSManagedObject+HardcoreData.swift index bf39c30..33f4d57 100644 --- a/HardcoreData/NSManagedObject+HardcoreData.swift +++ b/HardcoreData/NSManagedObject+HardcoreData.swift @@ -28,18 +28,67 @@ import CoreData public extension NSManagedObject { + // MARK: - Entity Utilities + public class var entityName: String { return NSStringFromClass(self).componentsSeparatedByString(".").last! } - public class func createInContext(context: NSManagedObjectContext) -> Self { + public func inContext(context: NSManagedObjectContext) -> Self? { + + return self.typedObjectInContext(context) + } + + public func deleteFromContext() { + + self.managedObjectContext?.deleteObject(self) + } + + + // MARK: Querying + + public class func WHERE(predicate: NSPredicate) -> Query { + + return Query(entity: self).WHERE(predicate) + } + + public class func WHERE(value: Bool) -> Query { + + return self.WHERE(NSPredicate(value: value)) + } + + public class func WHERE(format: String, _ args: CVarArgType...) -> Query { + + return self.WHERE(NSPredicate(format: format, arguments: withVaList(args, { $0 }))) + } + + public class func WHERE(format: String, argumentArray: [AnyObject]?) -> Query { + + return self.WHERE(NSPredicate(format: format, argumentArray: argumentArray)) + } + + public class func SORTEDBY(order: [SortOrder]) -> Query { + + return Query(entity: self).SORTEDBY(order) + } + + public class func SORTEDBY(order: SortOrder, _ subOrder: SortOrder...) -> Query { + + return self.SORTEDBY([order] + subOrder) + } + + + + // MARK: - Internal + + internal class func createInContext(context: NSManagedObjectContext) -> Self { return self(entity: NSEntityDescription.entityForName(self.entityName, inManagedObjectContext: context)!, insertIntoManagedObjectContext: context) } - public func inContext(context: NSManagedObjectContext) -> T? { + private func typedObjectInContext(context: NSManagedObjectContext) -> T? { let objectID = self.objectID if objectID.temporaryID { @@ -65,9 +114,4 @@ public extension NSManagedObject { "Failed to load existing NSManagedObject in context.") return nil; } - - public func deleteFromContext() { - - self.managedObjectContext?.deleteObject(self) - } } \ No newline at end of file diff --git a/HardcoreData/NSManagedObjectContext+HardcoreData.swift b/HardcoreData/NSManagedObjectContext+HardcoreData.swift index f86851e..d4a99c4 100644 --- a/HardcoreData/NSManagedObjectContext+HardcoreData.swift +++ b/HardcoreData/NSManagedObjectContext+HardcoreData.swift @@ -27,8 +27,6 @@ import Foundation import CoreData import GCDKit -private var _HardcoreData_NSManagedObjectContext_shouldCascadeSavesToParent: Void? - public extension NSManagedObjectContext { // MARK: - Public @@ -46,66 +44,6 @@ public extension NSManagedObjectContext { } - // MARK: Querying - - public func findFirst(entity: T.Type) -> T? { - - return self.findFirst(T.self, predicate: NSPredicate(value: true)) - } - - public func findFirst(entity: T.Type, predicate: NSPredicate) -> T? { - - let fetchRequest = NSFetchRequest() - fetchRequest.entity = NSEntityDescription.entityForName( - entity.entityName, - inManagedObjectContext: self) - fetchRequest.fetchLimit = 1 - - var fetchResults: [T]? - self.performBlockAndWait { - - var error: NSError? - fetchResults = self.executeFetchRequest(fetchRequest, error: &error) as? [T] - if fetchResults == nil { - - HardcoreData.handleError( - error!, - "Failed executing fetch request.") - } - } - - return fetchResults?.first - } - - public func findAll(entity: T.Type) -> [T]? { - - return self.findAll(QueryDescriptor(entity: entity)) - } - - public func findAll(query: QueryDescriptor) -> [T]? { - - let fetchRequest = NSFetchRequest() - fetchRequest.entity = NSEntityDescription.entityForName( - query.entityName, - inManagedObjectContext: self) - - var fetchResults: [T]? - self.performBlockAndWait { - - var error: NSError? - fetchResults = self.executeFetchRequest(fetchRequest, error: &error) as? [T] - if fetchResults == nil { - - HardcoreData.handleError( - error!, - "Failed executing fetch request.") - } - } - - return fetchResults - } - - // MARK: - Internal internal func saveSynchronously() -> SaveResult { @@ -248,23 +186,24 @@ public extension NSManagedObjectContext { // MARK: - Private - private struct ObserverKeys { + private struct PropertyKeys { - static var willSaveNotification: AnyObject? - static var didSaveNotification: AnyObject? + static var observerForWillSaveNotification: Void? + static var observerForDidSaveNotification: Void? + static var shouldCascadeSavesToParent: Void? } private var observerForWillSaveNotification: NotificationObserver? { get { - return self.getAssociatedObjectForKey(&ObserverKeys.willSaveNotification) + return self.getAssociatedObjectForKey(&PropertyKeys.observerForWillSaveNotification) } set { self.setAssociatedRetainedObject( newValue, - forKey: &ObserverKeys.willSaveNotification) + forKey: &PropertyKeys.observerForWillSaveNotification) } } @@ -272,13 +211,13 @@ public extension NSManagedObjectContext { get { - return self.getAssociatedObjectForKey(&ObserverKeys.didSaveNotification) + return self.getAssociatedObjectForKey(&PropertyKeys.observerForDidSaveNotification) } set { self.setAssociatedRetainedObject( newValue, - forKey: &ObserverKeys.didSaveNotification) + forKey: &PropertyKeys.observerForDidSaveNotification) } } @@ -286,19 +225,14 @@ public extension NSManagedObjectContext { get { - if let value = objc_getAssociatedObject(self, &_HardcoreData_NSManagedObjectContext_shouldCascadeSavesToParent) as? NSNumber { - - return value.boolValue - } - return false + let number: NSNumber? = self.getAssociatedObjectForKey(&PropertyKeys.observerForDidSaveNotification) + return number?.boolValue ?? false } set { - objc_setAssociatedObject( - self, - &_HardcoreData_NSManagedObjectContext_shouldCascadeSavesToParent, - newValue, - objc_AssociationPolicy(OBJC_ASSOCIATION_ASSIGN)) + self.setAssociatedCopiedObject( + NSNumber(bool: newValue), + forKey: &PropertyKeys.shouldCascadeSavesToParent) } } @@ -335,4 +269,88 @@ public extension NSManagedObjectContext { } }) } -} \ No newline at end of file +} + + +// MARK: - DataContextProvider + +extension NSManagedObjectContext: Queryable { + + public func findFirst(entity: T.Type) -> T? { + + return self.findFirst(Query(entity: entity)) + } + + public func findFirst(query: Query) -> T? { + + var query = query + query.fetchLimit = 1 + let fetchRequest = query.createFetchRequestInContext(self) + + var fetchResults: [T]? + self.performBlockAndWait { + + var error: NSError? + fetchResults = self.executeFetchRequest(fetchRequest, error: &error) as? [T] + if fetchResults == nil { + + HardcoreData.handleError( + error!, + "Failed executing fetch request.") + } + } + + return fetchResults?.first + } + + public func findAll(entity: T.Type) -> [T]? { + + return self.findAll(Query(entity: entity)) + } + + public func findAll(query: Query) -> [T]? { + + let fetchRequest = query.createFetchRequestInContext(self) + + var fetchResults: [T]? + self.performBlockAndWait { + + var error: NSError? + fetchResults = self.executeFetchRequest(fetchRequest, error: &error) as? [T] + if fetchResults == nil { + + HardcoreData.handleError( + error!, + "Failed executing fetch request.") + } + } + + return fetchResults + } + + public func count(entity: T.Type) -> Int { + + return self.count(Query(entity: entity)) + } + + public func count(query: Query) -> Int { + + let fetchRequest = query.createFetchRequestInContext(self) + + var count = 0 + var error: NSError? + self.performBlockAndWait { + + count = self.countForFetchRequest(fetchRequest, error: &error) + } + if count == NSNotFound { + + HardcoreData.handleError( + error!, + "Failed executing fetch request.") + return 0 + } + + return count + } +} diff --git a/HardcoreData/Query.swift b/HardcoreData/Query.swift new file mode 100644 index 0000000..f241b65 --- /dev/null +++ b/HardcoreData/Query.swift @@ -0,0 +1,123 @@ +// +// Query.swift +// HardcoreData +// +// Copyright (c) 2014 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + + +public typealias AttributeName = Selector + +public enum SortOrder { + + case Ascending(AttributeName) + case Descending(AttributeName) +} + +public class Query { + + public var entityName: String { + + return self.entity.entityName + } + + public func WHERE(predicate: NSPredicate) -> Query { + + if self.predicate != nil { + + } + self.predicate = predicate + return self + } + + public func WHERE(value: Bool) -> Query { + + return self.WHERE(NSPredicate(value: value)) + } + + public func WHERE(format: String, _ args: CVarArgType...) -> Query { + + return self.WHERE(NSPredicate(format: format, arguments: withVaList(args, { $0 }))) + } + + public func WHERE(format: String, argumentArray: [AnyObject]?) -> Query { + + return self.WHERE(NSPredicate(format: format, argumentArray: argumentArray)) + } + + public func SORTEDBY(order: [SortOrder]) -> Query { + + self.sortDescriptors = order.map { sortOrder in + + switch sortOrder { + + case .Ascending(let attributeName): + return NSSortDescriptor( + key: NSStringFromSelector(attributeName), + ascending: true) + + case .Descending(let attributeName): + return NSSortDescriptor( + key: NSStringFromSelector(attributeName), + ascending: false) + } + } + return self + } + + public func SORTEDBY(order: SortOrder, _ subOrder: SortOrder...) -> Query { + + return self.SORTEDBY([order] + subOrder) + } + + public func createFetchRequestInContext(context: NSManagedObjectContext) -> NSFetchRequest { + + let fetchRequest = NSFetchRequest() + fetchRequest.entity = NSEntityDescription.entityForName( + self.entityName, + inManagedObjectContext: context) + fetchRequest.fetchLimit = self.fetchLimit + fetchRequest.fetchOffset = self.fetchOffset + fetchRequest.fetchBatchSize = self.fetchBatchSize + fetchRequest.predicate = self.predicate + + return fetchRequest + } + + // MARK: Internal + + internal init(entity: T.Type) { + + self.entity = entity + } + + + // MARK: Private + private let entity: T.Type + public var fetchLimit: Int = 0 + public var fetchOffset: Int = 0 + public var fetchBatchSize: Int = 0 + public var predicate: NSPredicate? + public var sortDescriptors: [NSSortDescriptor]? +} diff --git a/HardcoreData/QueryDescriptor.swift b/HardcoreData/QueryDescriptor.swift deleted file mode 100644 index c2c3e36..0000000 --- a/HardcoreData/QueryDescriptor.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// QueryDescriptor.swift -// HardcoreData -// -// Created by John Rommel Estropia on 14/11/16. -// Copyright (c) 2014 John Rommel Estropia. All rights reserved. -// - -import Foundation -import CoreData - -public struct QueryDescriptor { - - public var entityName: String { - - return self.entity.entityName - } - - // MARK: Internal - - internal init(entity: T.Type) { - self.entity = entity - } - - - // MARK: Private - private let entity: T.Type - private var predicate: NSPredicate? - private var sortDescriptors: [NSSortDescriptor]? -} \ No newline at end of file diff --git a/HardcoreData/Queryable.swift b/HardcoreData/Queryable.swift new file mode 100644 index 0000000..f5bd378 --- /dev/null +++ b/HardcoreData/Queryable.swift @@ -0,0 +1,38 @@ +// +// Queryable.swift +// HardcoreData +// +// Copyright (c) 2014 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +public protocol Queryable { + + func findFirst(entity: T.Type) -> T? + func findFirst(query: Query) -> T? + + func findAll(entity: T.Type) -> [T]? + func findAll(query: Query) -> [T]? + + func count(entity: T.Type) -> Int + func count(query: Query) -> Int +} diff --git a/HardcoreDataTests/HardcoreDataTests.swift b/HardcoreDataTests/HardcoreDataTests.swift index cfa6bef..b0ac166 100644 --- a/HardcoreDataTests/HardcoreDataTests.swift +++ b/HardcoreDataTests/HardcoreDataTests.swift @@ -2,12 +2,30 @@ // HardcoreDataTests.swift // HardcoreDataTests // -// Created by John Rommel Estropia on 2014/09/14. -// Copyright (c) 2014 John Rommel Estropia. All rights reserved. +// Copyright (c) 2014 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. // import UIKit import XCTest +import HardcoreData class HardcoreDataTests: XCTestCase { @@ -23,7 +41,7 @@ class HardcoreDataTests: XCTestCase { func testExample() { // This is an example of a functional test case. - + NSLog("Test aaaa") #if DEBUG let resetStoreOnMigrationFailure = true #else @@ -43,15 +61,18 @@ class HardcoreDataTests: XCTestCase { HardcoreData.performTransaction { (transaction) -> () in - let obj = transaction.context.findFirst(FlickrPhoto) + let obj = transaction.findFirst( + TestEntity1 + .WHERE(true) + .SORTEDBY(.Ascending("testEntityID"), .Descending("testString"))) transaction.commit { (result) -> () in switch result { case .Success(let hasChanges): - JEDump(hasChanges, "hasChanges") + dump(hasChanges, name: "hasChanges") case .Failure(let error): - JEDump(error, "error") + dump(error, name: "error") } } } diff --git a/HardcoreDataTests/Model.xcdatamodeld/Model.xcdatamodel/contents b/HardcoreDataTests/Model.xcdatamodeld/Model.xcdatamodel/contents new file mode 100644 index 0000000..e6db5ed --- /dev/null +++ b/HardcoreDataTests/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/HardcoreDataTests/TestEntity1.swift b/HardcoreDataTests/TestEntity1.swift new file mode 100644 index 0000000..2cab1ac --- /dev/null +++ b/HardcoreDataTests/TestEntity1.swift @@ -0,0 +1,36 @@ +// +// TestEntity1.swift +// HardcoreData +// +// Copyright (c) 2014 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + +class TestEntity1: NSManagedObject { + + @NSManaged var testEntityID: NSNumber + @NSManaged var testString: String + @NSManaged var testNumber: NSNumber + @NSManaged var testDate: NSDate + +}