diff --git a/HardcoreData.xcodeproj/project.pbxproj b/HardcoreData.xcodeproj/project.pbxproj index 71fd821..54dd439 100644 --- a/HardcoreData.xcodeproj/project.pbxproj +++ b/HardcoreData.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 2F03A54019C5C6DA005002A5 /* HardcoreDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F03A53F19C5C6DA005002A5 /* HardcoreDataTests.swift */; }; 2F03A54D19C5C872005002A5 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F03A54C19C5C872005002A5 /* CoreData.framework */; }; 2F291E2719C6D3CF007AF63F /* HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F291E2619C6D3CF007AF63F /* HardcoreData.swift */; }; + B5398AA21AA8938D00B66388 /* DetachedDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5398AA11AA8938D00B66388 /* DetachedDataTransaction.swift */; }; B54A9F031AA7640200AFEC05 /* AggregateFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54A9F021AA7640200AFEC05 /* AggregateFunction.swift */; }; B54A9F051AA7644400AFEC05 /* AggregateResultType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54A9F041AA7644400AFEC05 /* AggregateResultType.swift */; }; B54A9F071AA7654400AFEC05 /* DataStack+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54A9F061AA7654400AFEC05 /* DataStack+Querying.swift */; }; @@ -22,7 +23,7 @@ B595CAC81A9A161B009A397F /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B595CAC71A9A161B009A397F /* WeakObject.swift */; }; B5CFD36E1A0775F000B7885F /* SaveResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFD36D1A0775F000B7885F /* SaveResult.swift */; }; B5CFF23E19FD1D1C00D6DFC4 /* NSManagedObjectContext+HardcoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFF23D19FD1D1C00D6DFC4 /* NSManagedObjectContext+HardcoreData.swift */; }; - B5CFF24019FD383100D6DFC4 /* DataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */; }; + B5CFF24019FD383100D6DFC4 /* BaseDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFF23F19FD383100D6DFC4 /* BaseDataTransaction.swift */; }; B5D022661A90CD340070CA63 /* DataStack+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D022651A90CD340070CA63 /* DataStack+Transaction.swift */; }; B5D19BFB1AA14063001D1A99 /* AsynchronousDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D19BFA1AA14063001D1A99 /* AsynchronousDataTransaction.swift */; }; B5D19BFF1AA14351001D1A99 /* SynchronousDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D19BFE1AA14351001D1A99 /* SynchronousDataTransaction.swift */; }; @@ -45,7 +46,7 @@ B5F409E91A8B11CE00A228EA /* HardcoreDataLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409E81A8B11CE00A228EA /* HardcoreDataLogger.swift */; }; B5F409EB1A8B199600A228EA /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409EA1A8B199600A228EA /* DefaultLogger.swift */; }; B5F409ED1A8B200700A228EA /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409EC1A8B200700A228EA /* NSManagedObjectContext+Querying.swift */; }; - B5F409EF1A8B243D00A228EA /* DataTransaction+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409EE1A8B243D00A228EA /* DataTransaction+Querying.swift */; }; + B5F409EF1A8B243D00A228EA /* BaseDataTransaction+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409EE1A8B243D00A228EA /* BaseDataTransaction+Querying.swift */; }; B5F409F11A8B27A600A228EA /* CustomizeQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F409F01A8B27A600A228EA /* CustomizeQuery.swift */; }; /* End PBXBuildFile section */ @@ -82,6 +83,7 @@ 2F03A53F19C5C6DA005002A5 /* HardcoreDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HardcoreDataTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 2F03A54C19C5C872005002A5 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 2F291E2619C6D3CF007AF63F /* HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HardcoreData.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + B5398AA11AA8938D00B66388 /* DetachedDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetachedDataTransaction.swift; sourceTree = ""; }; B54A9F021AA7640200AFEC05 /* AggregateFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunction.swift; sourceTree = ""; }; B54A9F041AA7644400AFEC05 /* AggregateResultType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateResultType.swift; sourceTree = ""; }; B54A9F061AA7654400AFEC05 /* DataStack+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataStack+Querying.swift"; sourceTree = ""; }; @@ -93,7 +95,7 @@ B595CAC71A9A161B009A397F /* WeakObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; B5CFD36D1A0775F000B7885F /* SaveResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveResult.swift; sourceTree = ""; }; B5CFF23D19FD1D1C00D6DFC4 /* NSManagedObjectContext+HardcoreData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+HardcoreData.swift"; sourceTree = ""; }; - B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataTransaction.swift; sourceTree = ""; }; + B5CFF23F19FD383100D6DFC4 /* BaseDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseDataTransaction.swift; sourceTree = ""; }; B5D022651A90CD340070CA63 /* DataStack+Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataStack+Transaction.swift"; sourceTree = ""; }; B5D19BFA1AA14063001D1A99 /* AsynchronousDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsynchronousDataTransaction.swift; sourceTree = ""; }; B5D19BFE1AA14351001D1A99 /* SynchronousDataTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronousDataTransaction.swift; sourceTree = ""; }; @@ -117,7 +119,7 @@ B5F409E81A8B11CE00A228EA /* HardcoreDataLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HardcoreDataLogger.swift; sourceTree = ""; }; B5F409EA1A8B199600A228EA /* DefaultLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLogger.swift; sourceTree = ""; }; B5F409EC1A8B200700A228EA /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = ""; }; - B5F409EE1A8B243D00A228EA /* DataTransaction+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataTransaction+Querying.swift"; sourceTree = ""; }; + B5F409EE1A8B243D00A228EA /* BaseDataTransaction+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BaseDataTransaction+Querying.swift"; sourceTree = ""; }; B5F409F01A8B27A600A228EA /* CustomizeQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeQuery.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -256,7 +258,8 @@ B5D022621A90BCC60070CA63 /* Saving and Processing */ = { isa = PBXGroup; children = ( - B5CFF23F19FD383100D6DFC4 /* DataTransaction.swift */, + B5CFF23F19FD383100D6DFC4 /* BaseDataTransaction.swift */, + B5398AA11AA8938D00B66388 /* DetachedDataTransaction.swift */, B5D19BFE1AA14351001D1A99 /* SynchronousDataTransaction.swift */, B5D19BFA1AA14063001D1A99 /* AsynchronousDataTransaction.swift */, B5CFD36D1A0775F000B7885F /* SaveResult.swift */, @@ -315,7 +318,7 @@ B54A9F041AA7644400AFEC05 /* AggregateResultType.swift */, B582DF811A98B0E7003F09C6 /* HardcoreData+Querying.swift */, B54A9F061AA7654400AFEC05 /* DataStack+Querying.swift */, - B5F409EE1A8B243D00A228EA /* DataTransaction+Querying.swift */, + B5F409EE1A8B243D00A228EA /* BaseDataTransaction+Querying.swift */, B54A9EFF1AA763D100AFEC05 /* Internal */, ); name = "Fetching and Querying"; @@ -462,7 +465,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B5CFF24019FD383100D6DFC4 /* DataTransaction.swift in Sources */, + B5CFF24019FD383100D6DFC4 /* BaseDataTransaction.swift in Sources */, B5D19C011AA15E1F001D1A99 /* HardcoreData+Logging.swift in Sources */, B5D399F519FCF4E0000E91BB /* NSPersistentStoreCoordinator+HardcoreData.swift in Sources */, B5CFD36E1A0775F000B7885F /* SaveResult.swift in Sources */, @@ -473,7 +476,7 @@ B5CFF23E19FD1D1C00D6DFC4 /* NSManagedObjectContext+HardcoreData.swift in Sources */, B54A9F071AA7654400AFEC05 /* DataStack+Querying.swift in Sources */, B5E126571A7DCE5900AD8B39 /* SortedBy.swift in Sources */, - B5F409EF1A8B243D00A228EA /* DataTransaction+Querying.swift in Sources */, + B5F409EF1A8B243D00A228EA /* BaseDataTransaction+Querying.swift in Sources */, 2F291E2719C6D3CF007AF63F /* HardcoreData.swift in Sources */, B5F409E91A8B11CE00A228EA /* HardcoreDataLogger.swift in Sources */, B5D8081A1A3495BD00A44484 /* NSObject+HardcoreData.swift in Sources */, @@ -485,6 +488,7 @@ B595CAC81A9A161B009A397F /* WeakObject.swift in Sources */, B54A9F051AA7644400AFEC05 /* AggregateResultType.swift in Sources */, B5D1E22A19FA9E63003B2874 /* PersistentStoreResult.swift in Sources */, + B5398AA21AA8938D00B66388 /* DetachedDataTransaction.swift in Sources */, B5F409F11A8B27A600A228EA /* CustomizeQuery.swift in Sources */, B5F409EB1A8B199600A228EA /* DefaultLogger.swift in Sources */, B54A9F031AA7640200AFEC05 /* AggregateFunction.swift in Sources */, diff --git a/HardcoreData/AggregateFunction.swift b/HardcoreData/AggregateFunction.swift index d6082fc..1a42639 100644 --- a/HardcoreData/AggregateFunction.swift +++ b/HardcoreData/AggregateFunction.swift @@ -43,7 +43,7 @@ public enum AggregateFunction { // MARK: Internal - internal func toExpression() -> NSExpression { + internal func createExpression() -> NSExpression { switch self { diff --git a/HardcoreData/AsynchronousDataTransaction.swift b/HardcoreData/AsynchronousDataTransaction.swift index 0f3c0a5..e2e221b 100644 --- a/HardcoreData/AsynchronousDataTransaction.swift +++ b/HardcoreData/AsynchronousDataTransaction.swift @@ -28,14 +28,14 @@ import GCDKit /** -The AsynchronousDataTransaction provides an interface for NSManagedObject creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from DataStack.performTransaction(_:), or from HardcoreData.performTransaction(_:). +The AsynchronousDataTransaction provides an interface for NSManagedObject creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from DataStack.beginAsynchronous(_:), or from HardcoreData.beginAsynchronous(_:). */ -public class AsynchronousDataTransaction: DataTransaction { +public final class AsynchronousDataTransaction: BaseDataTransaction { // MARK: Public /** - Saves the transaction changes asynchronously. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Saves the transaction changes asynchronously. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. :param: completion the block executed after the save completes. Success or failure is reported by the SaveResult argument of the block. */ @@ -56,7 +56,7 @@ public class AsynchronousDataTransaction: DataTransaction { } /** - Saves the transaction changes and waits for completion synchronously. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Saves the transaction changes and waits for completion synchronously. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. :returns: a SaveResult value indicating success or failure. */ @@ -70,12 +70,14 @@ public class AsynchronousDataTransaction: DataTransaction { } /** - Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. + Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. :returns: a SaveResult value indicating success or failure, or nil if the transaction was not comitted synchronously */ - public func performTransactionAndWait(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + public func beginSynchronous(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + + HardcoreData.assert(!self.isCommitted, "Attempted to begin a child transaction from an already committed <\(self.dynamicType)>.") return SynchronousDataTransaction( mainContext: self.context, @@ -84,6 +86,57 @@ public class AsynchronousDataTransaction: DataTransaction { } + // MARK: BaseDataTransaction + + /** + Creates a new NSManagedObject with the specified entity type. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. + + :param: entity the NSManagedObject type to be created + :returns: a new NSManagedObject instance of the specified entity type. + */ + public override func create(entity: T.Type) -> T { + + HardcoreData.assert(!self.isCommitted, "Attempted to create an entity of type <\(entity)> from an already committed <\(self.dynamicType)>.") + + return super.create(entity) + } + + /** + Returns an editable proxy of a specified NSManagedObject. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. + + :param: object the NSManagedObject type to be edited + :returns: an editable proxy for the specified NSManagedObject. + */ + public override func fetch(object: T) -> T? { + + HardcoreData.assert(!self.isCommitted, "Attempted to update an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") + + return super.fetch(object) + } + + /** + Deletes a specified NSManagedObject. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. + + :param: object the NSManagedObject type to be deleted + */ + public override func delete(object: NSManagedObject) { + + HardcoreData.assert(!self.isCommitted, "Attempted to delete an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") + + super.delete(object) + } + + /** + Rolls back the transaction by resetting the NSManagedObjectContext. After calling this method, all NSManagedObjects fetched within the transaction will become invalid. This method should not be used after either the commit(_:) or commitAndWait() method was already called once. + */ + public override func rollback() { + + HardcoreData.assert(!self.isCommitted, "Attempted to rollback an already committed <\(self.dynamicType)>.") + + super.rollback() + } + + // MARK: Internal internal init(mainContext: NSManagedObjectContext, queue: GCDQueue, closure: (transaction: AsynchronousDataTransaction) -> Void) { diff --git a/HardcoreData/DataTransaction+Querying.swift b/HardcoreData/BaseDataTransaction+Querying.swift similarity index 97% rename from HardcoreData/DataTransaction+Querying.swift rename to HardcoreData/BaseDataTransaction+Querying.swift index a968cb2..6e64b96 100644 --- a/HardcoreData/DataTransaction+Querying.swift +++ b/HardcoreData/BaseDataTransaction+Querying.swift @@ -1,5 +1,5 @@ // -// DataTransaction+Querying.swift +// BaseDataTransaction+Querying.swift // HardcoreData // // Copyright (c) 2015 John Rommel Estropia @@ -29,7 +29,7 @@ import CoreData // MARK: - DataTransaction -public extension DataTransaction { +public extension BaseDataTransaction { // MARK: Public diff --git a/HardcoreData/DataTransaction.swift b/HardcoreData/BaseDataTransaction.swift similarity index 67% rename from HardcoreData/DataTransaction.swift rename to HardcoreData/BaseDataTransaction.swift index dadefe1..fe77538 100644 --- a/HardcoreData/DataTransaction.swift +++ b/HardcoreData/BaseDataTransaction.swift @@ -1,5 +1,5 @@ // -// DataTransaction.swift +// BaseDataTransaction.swift // HardcoreData // // Copyright (c) 2014 John Rommel Estropia @@ -28,17 +28,22 @@ import CoreData import GCDKit -// MARK: - DataTransaction +// MARK: - BaseDataTransaction /** -The DataTransaction provides an interface for NSManagedObject creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from DataStack.performTransaction(_:), or from HardcoreData.performTransaction(_:). +The BaseDataTransaction is an abstract interface for NSManagedObject creates, updates, and deletes. All BaseDataTransaction subclasses manage a private NSManagedObjectContext which are direct children of the NSPersistentStoreCoordinator's root NSManagedObjectContext. This means that all updates are saved first to the persistent store, and then propagated up to the read-only NSManagedObjectContext. */ -public /*abstract*/ class DataTransaction { +public /*abstract*/ class BaseDataTransaction { // MARK: Object management + var hasChanges: Bool { + + return self.context.hasChanges + } + /** - Creates a new NSManagedObject with the specified entity type. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Creates a new NSManagedObject with the specified entity type. :param: entity the NSManagedObject type to be created :returns: a new NSManagedObject instance of the specified entity type. @@ -46,13 +51,12 @@ public /*abstract*/ class DataTransaction { public func create(entity: T.Type) -> T { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext(), "Attempted to create an entity of type <\(entity)> outside a transaction queue.") - HardcoreData.assert(!self.isCommitted, "Attempted to create an entity of type <\(entity)> from an already committed <\(self.dynamicType)>.") return T.createInContext(self.context) } /** - Returns an editable proxy of a specified NSManagedObject. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Returns an editable proxy of a specified NSManagedObject. :param: object the NSManagedObject type to be edited :returns: an editable proxy for the specified NSManagedObject. @@ -60,20 +64,18 @@ public /*abstract*/ class DataTransaction { public func fetch(object: T) -> T? { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext(), "Attempted to update an entity of type <\(object.dynamicType)> outside a transaction queue.") - HardcoreData.assert(!self.isCommitted, "Attempted to update an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") return object.inContext(self.context) } /** - Deletes a specified NSManagedObject. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Deletes a specified NSManagedObject. :param: object the NSManagedObject type to be deleted */ public func delete(object: NSManagedObject) { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext(), "Attempted to delete an entity of type <\(object.dynamicType)> outside a transaction queue.") - HardcoreData.assert(!self.isCommitted, "Attempted to delete an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") object.deleteFromContext() } @@ -81,12 +83,11 @@ public /*abstract*/ class DataTransaction { // 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. + Rolls back the transaction by resetting the NSManagedObjectContext. After calling this method, all NSManagedObjects fetched within the transaction will become invalid. */ public func rollback() { HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext(), "Attempted to rollback a <\(self.dynamicType)> outside a transaction queue.") - HardcoreData.assert(!self.isCommitted, "Attempted to rollback an already committed <\(self.dynamicType)>.") self.context.reset() } @@ -105,10 +106,13 @@ public /*abstract*/ class DataTransaction { self.transactionQueue = queue - let context = mainContext.temporaryContextInTransaction(nil) + let context = mainContext.temporaryContextInTransactionWithConcurrencyType( + queue == .Main + ? .MainQueueConcurrencyType + : .PrivateQueueConcurrencyType + ) self.context = context - context.retainsRegisteredObjects = true context.parentTransaction = self } } diff --git a/HardcoreData/DataStack+Transaction.swift b/HardcoreData/DataStack+Transaction.swift index 04801f6..5fc81ec 100644 --- a/HardcoreData/DataStack+Transaction.swift +++ b/HardcoreData/DataStack+Transaction.swift @@ -38,7 +38,7 @@ public extension DataStack { :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. */ - public func performTransaction(closure: (transaction: AsynchronousDataTransaction) -> Void) { + public func beginAsynchronous(closure: (transaction: AsynchronousDataTransaction) -> Void) { AsynchronousDataTransaction( mainContext: self.rootSavingContext, @@ -52,11 +52,23 @@ public extension DataStack { :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. :returns: a SaveResult value indicating success or failure, or nil if the transaction was not comitted synchronously */ - public func performTransactionAndWait(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + public func beginSynchronous(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { return SynchronousDataTransaction( mainContext: self.rootSavingContext, queue: self.childTransactionQueue, closure: closure).performAndWait() } + + /** + Begins a non-contiguous transaction where NSManagedObject creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. A detached transaction object should typically be only used from the main queue. + + :returns: a DetachedDataTransaction instance where creates, updates, and deletes can be made. + */ + public func beginDetached() -> DetachedDataTransaction { + + return DetachedDataTransaction( + mainContext: self.rootSavingContext, + queue: .Main) + } } \ No newline at end of file diff --git a/HardcoreData/DataStack.swift b/HardcoreData/DataStack.swift index a92bddb..78801f3 100644 --- a/HardcoreData/DataStack.swift +++ b/HardcoreData/DataStack.swift @@ -40,7 +40,7 @@ private let defaultSQLiteStoreURL = applicationSupportDirectory.URLByAppendingPa /** The DataStack encapsulates the data model for the Core Data stack. Each DataStack can have multiple data stores, usually specified as a "Configuration" in the model editor. Behind the scenes, the DataStack manages its own NSPersistentStoreCoordinator, a root NSManagedObjectContext for disk saves, and a shared NSManagedObjectContext designed as a read-only model interface for NSManagedObjects. */ -public class DataStack { +public final class DataStack { // MARK: Public diff --git a/HardcoreData/DefaultLogger.swift b/HardcoreData/DefaultLogger.swift index 1e4b73d..4d6133b 100644 --- a/HardcoreData/DefaultLogger.swift +++ b/HardcoreData/DefaultLogger.swift @@ -33,14 +33,21 @@ public final class DefaultLogger: HardcoreDataLogger { public func log(#level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { #if DEBUG - Swift.println("[HardcoreData] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") + let levelString: String + switch level { + case .Trace: levelString = "Trace" + case .Notice: levelString = "Notice" + case .Warning: levelString = "Warning" + case .Fatal: levelString = "Fatal" + } + Swift.println("[HardcoreData:\(levelString)] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") #endif } public func handleError(#error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { #if DEBUG - Swift.println("[HardcoreData] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n") + Swift.println("[HardcoreData:Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n") #endif } diff --git a/HardcoreData/DetachedDataTransaction.swift b/HardcoreData/DetachedDataTransaction.swift new file mode 100644 index 0000000..4c75b4c --- /dev/null +++ b/HardcoreData/DetachedDataTransaction.swift @@ -0,0 +1,53 @@ +// +// DetachedDataTransaction.swift +// HardcoreData +// +// Copyright (c) 2015 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 GCDKit + + +/** +The DetachedDataTransaction provides an interface for non-contiguous NSManagedObject creates, updates, and deletes. This is useful for making temporary changes, such as partially filled forms. A detached transaction object should typically be only used from the main queue. +*/ +public final class DetachedDataTransaction: BaseDataTransaction { + + // MARK: Public + + /** + Saves the transaction changes asynchronously. For a DetachedDataTransaction, multiple commits are allowed, although it is the developer's responsibility to ensure a reasonable leeway to prevent blocking the main thread. + + :param: completion the block executed after the save completes. Success or failure is reported by the SaveResult argument of the block. + */ + public func commit(completion: (result: SaveResult) -> Void) { + + HardcoreData.assert(self.transactionQueue.isCurrentExecutionContext(), "Attempted to commit a <\(self.dynamicType)> outside a transaction queue.") + + self.context.saveAsynchronouslyWithCompletion { (result) -> Void in + + self.result = result + completion(result: result) + } + } +} + diff --git a/HardcoreData/HardcoreData+Transaction.swift b/HardcoreData/HardcoreData+Transaction.swift index 8fafe92..754bd53 100644 --- a/HardcoreData/HardcoreData+Transaction.swift +++ b/HardcoreData/HardcoreData+Transaction.swift @@ -37,9 +37,9 @@ public extension HardcoreData { :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. */ - public static func performTransaction(closure: (transaction: AsynchronousDataTransaction) -> Void) { + public static func beginAsynchronous(closure: (transaction: AsynchronousDataTransaction) -> Void) { - self.defaultStack.performTransaction(closure) + self.defaultStack.beginAsynchronous(closure) } /** @@ -48,8 +48,18 @@ public extension HardcoreData { :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. :returns: a SaveResult value indicating success or failure, or nil if the transaction was not comitted synchronously */ - public static func performTransactionAndWait(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + public static func beginSynchronous(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { - return self.defaultStack.performTransactionAndWait(closure) + return self.defaultStack.beginSynchronous(closure) + } + + /** + Using the defaultStack, begins a non-contiguous transaction where NSManagedObject creates, updates, and deletes can be made. This is useful for making temporary changes, such as partially filled forms. A detached transaction object should typically be only used from the main queue. + + :returns: a DetachedDataTransaction instance where creates, updates, and deletes can be made. + */ + public static func beginDetached() -> DetachedDataTransaction { + + return self.defaultStack.beginDetached() } } diff --git a/HardcoreData/NSManagedObjectContext+Querying.swift b/HardcoreData/NSManagedObjectContext+Querying.swift index 664efe1..d263019 100644 --- a/HardcoreData/NSManagedObjectContext+Querying.swift +++ b/HardcoreData/NSManagedObjectContext+Querying.swift @@ -203,7 +203,7 @@ internal extension NSManagedObjectContext { let expressionDescription = NSExpressionDescription() expressionDescription.name = "queryAggregate" expressionDescription.expressionResultType = U.attributeType - expressionDescription.expression = function.toExpression() + expressionDescription.expression = function.createExpression() let fetchRequest = NSFetchRequest() fetchRequest.entity = self.entityDescriptionForEntityClass(entity) diff --git a/HardcoreData/NSManagedObjectContext+Transaction.swift b/HardcoreData/NSManagedObjectContext+Transaction.swift index c287208..15f6beb 100644 --- a/HardcoreData/NSManagedObjectContext+Transaction.swift +++ b/HardcoreData/NSManagedObjectContext+Transaction.swift @@ -34,7 +34,7 @@ internal extension NSManagedObjectContext { // MARK: Internal - internal weak var parentTransaction: DataTransaction? { + internal weak var parentTransaction: BaseDataTransaction? { get { @@ -48,13 +48,14 @@ internal extension NSManagedObjectContext { } } - internal func temporaryContextInTransaction(transaction: DataTransaction?) -> NSManagedObjectContext { + internal func temporaryContextInTransactionWithConcurrencyType(concurrencyType: NSManagedObjectContextConcurrencyType) -> NSManagedObjectContext { - let context = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType) + let context = NSManagedObjectContext(concurrencyType: concurrencyType) context.parentContext = self context.parentStack = self.parentStack context.setupForHardcoreDataWithContextName("com.hardcoredata.temporarycontext") context.shouldCascadeSavesToParent = (self.parentStack?.rootSavingContext == self) + context.retainsRegisteredObjects = true return context } diff --git a/HardcoreData/SynchronousDataTransaction.swift b/HardcoreData/SynchronousDataTransaction.swift index b5fa8bd..1d19746 100644 --- a/HardcoreData/SynchronousDataTransaction.swift +++ b/HardcoreData/SynchronousDataTransaction.swift @@ -9,12 +9,12 @@ import Foundation import GCDKit -public class SynchronousDataTransaction: DataTransaction { +public final class SynchronousDataTransaction: BaseDataTransaction { // MARK: Public /** - Saves the transaction changes and waits for completion synchronously. Note that this method should not be used after either the commit(_:) or commitAndWait() method was already called once. + Saves the transaction changes and waits for completion synchronously. This method should not be used after the commitAndWait() method was already called once. :returns: a SaveResult value indicating success or failure. */ @@ -28,12 +28,14 @@ public class SynchronousDataTransaction: DataTransaction { } /** - Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. + Begins a child transaction synchronously where NSManagedObject creates, updates, and deletes can be made. This method should not be used after the commitAndWait() method was already called once. :param: closure the block where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent NSManagedObjectContext. :returns: a SaveResult value indicating success or failure, or nil if the transaction was not comitted synchronously */ - public func performTransactionAndWait(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + public func beginSynchronous(closure: (transaction: SynchronousDataTransaction) -> Void) -> SaveResult? { + + HardcoreData.assert(!self.isCommitted, "Attempted to begin a child transaction from an already committed <\(self.dynamicType)>.") return SynchronousDataTransaction( mainContext: self.context, @@ -42,6 +44,57 @@ public class SynchronousDataTransaction: DataTransaction { } + // MARK: BaseDataTransaction + + /** + Creates a new NSManagedObject with the specified entity type. This method should not be used after the commitAndWait() method was already called once. + + :param: entity the NSManagedObject type to be created + :returns: a new NSManagedObject instance of the specified entity type. + */ + public override func create(entity: T.Type) -> T { + + HardcoreData.assert(!self.isCommitted, "Attempted to create an entity of type <\(entity)> from an already committed <\(self.dynamicType)>.") + + return super.create(entity) + } + + /** + Returns an editable proxy of a specified NSManagedObject. This method should not be used after the commitAndWait() method was already called once. + + :param: object the NSManagedObject type to be edited + :returns: an editable proxy for the specified NSManagedObject. + */ + public override func fetch(object: T) -> T? { + + HardcoreData.assert(!self.isCommitted, "Attempted to update an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") + + return super.fetch(object) + } + + /** + Deletes a specified NSManagedObject. This method should not be used after the commitAndWait() method was already called once. + + :param: object the NSManagedObject type to be deleted + */ + public override func delete(object: NSManagedObject) { + + HardcoreData.assert(!self.isCommitted, "Attempted to delete an entity of type <\(object.dynamicType)> from an already committed <\(self.dynamicType)>.") + + super.delete(object) + } + + /** + Rolls back the transaction by resetting the NSManagedObjectContext. After calling this method, all NSManagedObjects fetched within the transaction will become invalid. This method should not be used after the commitAndWait() method was already called once. + */ + public override func rollback() { + + HardcoreData.assert(!self.isCommitted, "Attempted to rollback an already committed <\(self.dynamicType)>.") + + super.rollback() + } + + // MARK: Internal internal func performAndWait() -> SaveResult? { diff --git a/HardcoreDataTests/HardcoreDataTests.swift b/HardcoreDataTests/HardcoreDataTests.swift index 2ccf97b..0a461cc 100644 --- a/HardcoreDataTests/HardcoreDataTests.swift +++ b/HardcoreDataTests/HardcoreDataTests.swift @@ -65,8 +65,10 @@ class HardcoreDataTests: XCTestCase { break } + let detachedTransaction = HardcoreData.beginDetached() + let createExpectation = self.expectationWithDescription("Entity creation") - HardcoreData.performTransaction { (transaction) -> Void in + HardcoreData.beginAsynchronous { (transaction) -> Void in let obj1 = transaction.create(TestEntity1) obj1.testEntityID = 1 @@ -93,7 +95,7 @@ class HardcoreDataTests: XCTestCase { obj3.testDate = NSDate() - transaction.performTransactionAndWait { (transaction) -> Void in + transaction.beginSynchronous { (transaction) -> Void in let obj4 = transaction.create(TestEntity2) obj4.testEntityID = 4 @@ -121,10 +123,14 @@ class HardcoreDataTests: XCTestCase { ) XCTAssertNil(objs4test, "objs4test == nil") + let objs5test = detachedTransaction.fetchCount(TestEntity2) + XCTAssertTrue(objs5test == 2, "objs5test == 2") + XCTAssertTrue(NSThread.isMainThread(), "NSThread.isMainThread()") switch result { case .Success(let hasChanges): + XCTAssertTrue(hasChanges, "hasChanges == true") createExpectation.fulfill() case .Failure(let error): @@ -134,7 +140,7 @@ class HardcoreDataTests: XCTestCase { } let queryExpectation = self.expectationWithDescription("Query creation") - HardcoreData.performTransaction { (transaction) -> Void in + HardcoreData.beginAsynchronous { (transaction) -> Void in let obj1 = transaction.fetchOne(TestEntity1) XCTAssertNotNil(obj1, "obj1 != nil") @@ -157,6 +163,7 @@ class HardcoreDataTests: XCTestCase { switch result { case .Success(let hasChanges): + XCTAssertFalse(hasChanges, "hasChanges == false") queryExpectation.fulfill() case .Failure(let error): @@ -180,7 +187,7 @@ class HardcoreDataTests: XCTestCase { ) XCTAssertTrue(max2 == 90, "max == 90 (actual: \(max2))") - HardcoreData.performTransactionAndWait { (transaction) -> Void in + HardcoreData.beginSynchronous { (transaction) -> Void in let numberOfDeletedObjects1 = transaction.deleteAll(TestEntity1) XCTAssertTrue(numberOfDeletedObjects1 == 1, "numberOfDeletedObjects1 == 1 (actual: \(numberOfDeletedObjects1))") @@ -201,6 +208,62 @@ class HardcoreDataTests: XCTestCase { let objs2 = HardcoreData.fetchAll(TestEntity2) XCTAssertNotNil(objs2, "objs2 != nil") XCTAssertTrue(objs2?.count == 1, "objs2?.count == 1") + + let detachedExpectation = self.expectationWithDescription("Query creation") + + let obj5 = detachedTransaction.create(TestEntity1) + obj5.testEntityID = 5 + obj5.testString = "hihihi" + obj5.testNumber = 70 + obj5.testDate = NSDate() + + detachedTransaction.commit { (result) -> Void in + + XCTAssertTrue(NSThread.isMainThread(), "NSThread.isMainThread()") + switch result { + + case .Success(let hasChanges): + XCTAssertTrue(hasChanges, "hasChanges == true") + + let count = HardcoreData.queryAggregate( + TestEntity1.self, + function: .Count("testNumber") + ) + XCTAssertTrue(count == 1, "count == 1 (actual: \(count))") + + let obj6 = detachedTransaction.create(TestEntity1) + obj6.testEntityID = 6 + obj6.testString = "huehuehue" + obj6.testNumber = 130 + obj6.testDate = NSDate() + + detachedTransaction.commit { (result) -> Void in + + XCTAssertTrue(NSThread.isMainThread(), "NSThread.isMainThread()") + switch result { + + case .Success(let hasChanges): + XCTAssertTrue(hasChanges, "hasChanges == true") + + let count = HardcoreData.queryAggregate( + TestEntity1.self, + function: .Count("testNumber") + ) + XCTAssertTrue(count == 2, "count == 2 (actual: \(count))") + + detachedExpectation.fulfill() + + case .Failure(let error): + XCTFail(error.description) + } + } + + case .Failure(let error): + XCTFail(error.description) + } + } + + self.waitForExpectationsWithTimeout(100, handler: nil) } private func deleteStores() {