diff --git a/CoreStore.podspec b/CoreStore.podspec index 0174d76..feaabe9 100644 --- a/CoreStore.podspec +++ b/CoreStore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "CoreStore" - s.version = "1.4.4" + s.version = "1.5.0" s.license = "MIT" s.summary = "Unleashing the real power of Core Data with the elegance and safety of Swift" s.homepage = "https://github.com/JohnEstropia/CoreStore" diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 7072c4b..eea6690 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -208,6 +208,12 @@ B56965241B356B820075EE4A /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56965231B356B820075EE4A /* MigrationResult.swift */; }; B59D5C221B5BA34B00453479 /* NSFileManager+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D5C211B5BA34B00453479 /* NSFileManager+Setup.swift */; }; B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A261201B64BFDB006EB6D3 /* MigrationType.swift */; }; + B5C976E31C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; + B5C976E41C6C9F9A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; + B5C976E51C6C9F9B00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */; }; + B5C976E71C6E3A5A00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E61C6E3A5900B1AF90 /* CoreStoreFetchedResultsController.swift */; }; + B5C976E81C6E3A5D00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E61C6E3A5900B1AF90 /* CoreStoreFetchedResultsController.swift */; }; + B5C976E91C6E3A5E00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C976E61C6E3A5900B1AF90 /* CoreStoreFetchedResultsController.swift */; }; B5D1E22C19FA9FBC003B2874 /* NSError+CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D1E22B19FA9FBC003B2874 /* NSError+CoreStore.swift */; }; B5D372841A39CD6900F583D9 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B5D372821A39CD6900F583D9 /* Model.xcdatamodeld */; }; B5D372861A39CDDB00F583D9 /* TestEntity1.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D372851A39CDDB00F583D9 /* TestEntity1.swift */; }; @@ -317,6 +323,8 @@ B5A261201B64BFDB006EB6D3 /* MigrationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationType.swift; sourceTree = ""; }; B5BDC91A1C202269008147CD /* CartFile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CartFile; sourceTree = ""; }; B5BDC9271C2024F2008147CD /* .travis.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .travis.yml; sourceTree = SOURCE_ROOT; }; + B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UnsafeDataTransaction+Observing.swift"; sourceTree = ""; }; + B5C976E61C6E3A5900B1AF90 /* CoreStoreFetchedResultsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreFetchedResultsController.swift; sourceTree = ""; }; B5D1E22B19FA9FBC003B2874 /* NSError+CoreStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSError+CoreStore.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 = ""; }; @@ -627,6 +635,7 @@ B56007131B3F6C2800A9A8F9 /* SectionBy.swift */, B5E84F1A1AFF84860064E85B /* DataStack+Observing.swift */, B5E84F1B1AFF84860064E85B /* CoreStore+Observing.swift */, + B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */, B5E84F1C1AFF84860064E85B /* ObjectMonitor.swift */, B5E84F1F1AFF84860064E85B /* ObjectObserver.swift */, B5E84F1D1AFF84860064E85B /* ListMonitor.swift */, @@ -654,6 +663,7 @@ B5FAD6AB1B51285300714891 /* MigrationManager.swift */, B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */, B59D5C211B5BA34B00453479 /* NSFileManager+Setup.swift */, + B5C976E61C6E3A5900B1AF90 /* CoreStoreFetchedResultsController.swift */, B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */, B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */, B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */, @@ -943,6 +953,7 @@ buildActionMask = 2147483647; files = ( B5E84F221AFF84860064E85B /* ObjectMonitor.swift in Sources */, + B5C976E71C6E3A5A00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */, B5F1DA901B9AA991007C5CBB /* ImportableUniqueObject.swift in Sources */, B504D0D61B02362500B2BBB1 /* CoreStore+Setup.swift in Sources */, B5D1E22C19FA9FBC003B2874 /* NSError+CoreStore.swift in Sources */, @@ -952,6 +963,7 @@ B5E84F141AFF847B0064E85B /* DataStack+Querying.swift in Sources */, B56007141B3F6C2800A9A8F9 /* SectionBy.swift in Sources */, B5E84F371AFF85470064E85B /* NSManagedObjectContext+Transaction.swift in Sources */, + B5C976E31C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */, B56007161B4018AB00A9A8F9 /* MigrationChain.swift in Sources */, B5E84F0E1AFF847B0064E85B /* Tweak.swift in Sources */, B5E84F121AFF847B0064E85B /* OrderBy.swift in Sources */, @@ -1018,6 +1030,7 @@ buildActionMask = 2147483647; files = ( 82BA18B61C4BBD3F00A0916E /* DataStack+Querying.swift in Sources */, + B5C976E81C6E3A5D00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */, 82BA18A21C4BBD1D00A0916E /* NSError+CoreStore.swift in Sources */, 82BA18B21C4BBD3900A0916E /* ImportableObject.swift in Sources */, 82BA18AE1C4BBD3100A0916E /* DataStack+Transaction.swift in Sources */, @@ -1027,6 +1040,7 @@ 82BA18C21C4BBD5300A0916E /* ObjectMonitor.swift in Sources */, 82BA18A51C4BBD2200A0916E /* CoreStore+Setup.swift in Sources */, 82BA18BD1C4BBD4A00A0916E /* GroupBy.swift in Sources */, + B5C976E41C6C9F9A00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */, 82BA18B31C4BBD3900A0916E /* ImportableUniqueObject.swift in Sources */, 82BA18A11C4BBD1D00A0916E /* CoreStore.swift in Sources */, 82BA18CF1C4BBD7100A0916E /* Functions.swift in Sources */, @@ -1159,6 +1173,7 @@ buildActionMask = 2147483647; files = ( B56321A91BD65219006C9394 /* NSProgress+Convenience.swift in Sources */, + B5C976E91C6E3A5E00B1AF90 /* CoreStoreFetchedResultsController.swift in Sources */, B56321801BD65216006C9394 /* NSError+CoreStore.swift in Sources */, B56321AD1BD6521C006C9394 /* MigrationManager.swift in Sources */, B563219D1BD65216006C9394 /* DataStack+Observing.swift in Sources */, @@ -1168,6 +1183,7 @@ B563219E1BD65216006C9394 /* CoreStore+Observing.swift in Sources */, B56321891BD65216006C9394 /* AsynchronousDataTransaction.swift in Sources */, B56321831BD65216006C9394 /* CoreStore+Setup.swift in Sources */, + B5C976E51C6C9F9B00B1AF90 /* UnsafeDataTransaction+Observing.swift in Sources */, B563217F1BD65216006C9394 /* CoreStore.swift in Sources */, B56321911BD65216006C9394 /* BaseDataTransaction+Importing.swift in Sources */, B56321941BD65216006C9394 /* CoreStore+Querying.swift in Sources */, diff --git a/CoreStore/Convenience Helpers/NSFetchedResultsController+Convenience.swift b/CoreStore/Convenience Helpers/NSFetchedResultsController+Convenience.swift index a6ed59b..2894bd4 100644 --- a/CoreStore/Convenience Helpers/NSFetchedResultsController+Convenience.swift +++ b/CoreStore/Convenience Helpers/NSFetchedResultsController+Convenience.swift @@ -31,20 +31,31 @@ import CoreData public extension NSFetchedResultsController { - public convenience init(dataStack: DataStack, fetchRequest: NSFetchRequest, from: From? = nil, sectionBy: SectionBy? = nil, fetchClauses: [FetchClause]) { + + // MARK: Public + + public func createForStack(dataStack: DataStack, fetchRequest: NSFetchRequest, from: From? = nil, sectionBy: SectionBy? = nil, fetchClauses: [FetchClause]) -> NSFetchedResultsController { - let context = dataStack.mainContext - from?.applyToFetchRequest(fetchRequest, context: context) - for clause in fetchClauses { - - clause.applyToFetchRequest(fetchRequest) - } - - self.init( + return CoreStoreFetchedResultsController( + context: dataStack.mainContext, fetchRequest: fetchRequest, - managedObjectContext: context, - sectionNameKeyPath: sectionBy?.sectionKeyPath, - cacheName: nil + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses + ) + } + + + // MARK: Internal + + internal func createFromContext(context: NSManagedObjectContext, fetchRequest: NSFetchRequest, from: From? = nil, sectionBy: SectionBy? = nil, fetchClauses: [FetchClause]) -> NSFetchedResultsController { + + return CoreStoreFetchedResultsController( + context: context, + fetchRequest: fetchRequest, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses ) } } diff --git a/CoreStore/Fetching and Querying/Concrete Clauses/From.swift b/CoreStore/Fetching and Querying/Concrete Clauses/From.swift index 05612e5..2eb8877 100644 --- a/CoreStore/Fetching and Querying/Concrete Clauses/From.swift +++ b/CoreStore/Fetching and Querying/Concrete Clauses/From.swift @@ -36,22 +36,25 @@ public struct From { // MARK: Public + /** + Initializes a `From` clause with the specified entity type and configuration. + Sample Usage: + + let person = transaction.fetchOne(From()) + */ public init(){ - self.entityClass = T.self - self.findPersistentStores = { _ in nil } + self.init(entityClass: T.self) } public init(_ entity: T.Type) { - self.entityClass = entity - self.findPersistentStores = { _ in nil } + self.init(entityClass: entity) } public init(_ entityClass: AnyClass) { - self.entityClass = entityClass - self.findPersistentStores = { _ in nil } + self.init(entityClass: entityClass) } public init(_ configuration: String?, otherConfigurations: String?...) { @@ -147,10 +150,20 @@ public struct From { // MARK: Internal - internal func applyToFetchRequest(fetchRequest: NSFetchRequest, context: NSManagedObjectContext) { + internal func applyToFetchRequest(fetchRequest: NSFetchRequest, context: NSManagedObjectContext, applyAffectedStores: Bool = true) { fetchRequest.entity = context.entityDescriptionForEntityClass(self.entityClass) - fetchRequest.affectedStores = self.findPersistentStores(context: context) + if applyAffectedStores { + + self.applyAffectedStoresForFetchedRequest(fetchRequest, context: context) + } + } + + internal func applyAffectedStoresForFetchedRequest(fetchRequest: NSFetchRequest, context: NSManagedObjectContext) -> Bool { + + let stores = self.findPersistentStores(context: context) + fetchRequest.affectedStores = stores + return stores?.isEmpty == false } @@ -160,6 +173,15 @@ public struct From { private let findPersistentStores: (context: NSManagedObjectContext) -> [NSPersistentStore]? + private init(entityClass: AnyClass) { + + self.entityClass = entityClass + self.findPersistentStores = { (context: NSManagedObjectContext) -> [NSPersistentStore]? in + + return context.parentStack?.persistentStoresForEntityClass(entityClass) + } + } + private init(entityClass: AnyClass, configurations: [String?]) { let configurationsSet = Set(configurations.map { $0 ?? Into.defaultConfigurationName }) diff --git a/CoreStore/Info.plist b/CoreStore/Info.plist index e22a0c5..beed82a 100644 --- a/CoreStore/Info.plist +++ b/CoreStore/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.4.4 + 1.5.0 CFBundleSignature ???? CFBundleVersion diff --git a/CoreStore/Internal/CoreStoreFetchedResultsController.swift b/CoreStore/Internal/CoreStoreFetchedResultsController.swift new file mode 100644 index 0000000..8c35457 --- /dev/null +++ b/CoreStore/Internal/CoreStoreFetchedResultsController.swift @@ -0,0 +1,101 @@ +// +// CoreStoreFetchedResultsController.swift +// CoreStore +// +// Copyright © 2016 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 + + +// MARK: - CoreStoreFetchedResultsController + +@available(OSX, unavailable) +internal final class CoreStoreFetchedResultsController: NSFetchedResultsController { + + + // MARK: Internal + + internal convenience init(dataStack: DataStack, fetchRequest: NSFetchRequest, from: From? = nil, sectionBy: SectionBy? = nil, fetchClauses: [FetchClause]) { + + self.init( + context: dataStack.mainContext, + fetchRequest: fetchRequest, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses + ) + } + + internal init(context: NSManagedObjectContext, fetchRequest: NSFetchRequest, from: From? = nil, sectionBy: SectionBy? = nil, fetchClauses: [FetchClause]) { + + from?.applyToFetchRequest(fetchRequest, context: context, applyAffectedStores: false) + for clause in fetchClauses { + + clause.applyToFetchRequest(fetchRequest) + } + + if let from = from { + + self.reapplyAffectedStores = { + + return from.applyAffectedStoresForFetchedRequest(fetchRequest, context: context) + } + } + else { + + guard let from = (fetchRequest.entity.flatMap { $0.managedObjectClassName }).flatMap(NSClassFromString).flatMap(From.init) else { + + fatalError("Attempted to create an \(typeName(NSFetchedResultsController)) without a From clause or an NSEntityDescription.") + } + + self.reapplyAffectedStores = { + + return from.applyAffectedStoresForFetchedRequest(fetchRequest, context: context) + } + } + + super.init( + fetchRequest: fetchRequest, + managedObjectContext: context, + sectionNameKeyPath: sectionBy?.sectionKeyPath, + cacheName: nil + ) + } + + internal func performFetchFromSpecifiedStores() throws { + + if !self.reapplyAffectedStores() { + + CoreStore.log( + .Warning, + message: "Attempted to perform a fetch on an \(typeName(NSFetchedResultsController)) but could not find any persistent store for the entity \(typeName(self.fetchRequest.entityName))" + ) + } + try self.performFetch() + } + + + // MARK: Private + + private let reapplyAffectedStores: () -> Bool +} diff --git a/CoreStore/Observing/ListMonitor.swift b/CoreStore/Observing/ListMonitor.swift index 80ed8d2..d221d3b 100644 --- a/CoreStore/Observing/ListMonitor.swift +++ b/CoreStore/Observing/ListMonitor.swift @@ -882,12 +882,15 @@ public final class ListMonitor { "Attempted to refetch a \(typeName(self)) outside the main thread." ) - self.isPendingRefetch = true - - NSNotificationCenter.defaultCenter().postNotificationName( - ListMonitorWillRefetchListNotification, - object: self - ) + if !self.isPendingRefetch { + + self.isPendingRefetch = true + + NSNotificationCenter.defaultCenter().postNotificationName( + ListMonitorWillRefetchListNotification, + object: self + ) + } self.taskGroup.notify(.Main) { [weak self] () -> Void in @@ -904,14 +907,14 @@ public final class ListMonitor { clause.applyToFetchRequest(fetchRequest) } - strongSelf.parentStack?.childTransactionQueue.async { + strongSelf.transactionQueue.async { guard let strongSelf = self else { return } - try! strongSelf.fetchedResultsController.performFetch() + try! strongSelf.fetchedResultsController.performFetchFromSpecifiedStores() GCDQueue.Main.async { () -> Void in @@ -938,36 +941,54 @@ public final class ListMonitor { internal convenience init(dataStack: DataStack, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause]) { self.init( - dataStack: dataStack, + context: dataStack.mainContext, + transactionQueue: dataStack.childTransactionQueue, from: from, sectionBy: sectionBy, fetchClauses: fetchClauses, - prepareFetch: { _, performFetch in performFetch() } + createAsynchronously: nil ) } internal convenience init(dataStack: DataStack, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause], createAsynchronously: (ListMonitor) -> Void) { + let queue = dataStack.childTransactionQueue self.init( - dataStack: dataStack, + context: dataStack.mainContext, + transactionQueue: queue, from: from, sectionBy: sectionBy, fetchClauses: fetchClauses, - prepareFetch: { listMonitor, performFetch in - - dataStack.childTransactionQueue.async { - - performFetch() - GCDQueue.Main.async { - - createAsynchronously(listMonitor) - } - } - } + createAsynchronously: createAsynchronously ) } - private init(dataStack: DataStack, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause], prepareFetch: (ListMonitor, () -> Void) -> Void) { + internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause]) { + + self.init( + context: unsafeTransaction.context, + transactionQueue: unsafeTransaction.transactionQueue, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses, + createAsynchronously: nil + ) + } + + internal convenience init(unsafeTransaction: UnsafeDataTransaction, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause], createAsynchronously: (ListMonitor) -> Void) { + + let queue = unsafeTransaction.transactionQueue + self.init( + context: unsafeTransaction.context, + transactionQueue: queue, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses, + createAsynchronously: createAsynchronously + ) + } + + private init(context: NSManagedObjectContext, transactionQueue: GCDQueue, from: From, sectionBy: SectionBy?, fetchClauses: [FetchClause], createAsynchronously: ((ListMonitor) -> Void)?) { let fetchRequest = NSFetchRequest() fetchRequest.fetchLimit = 0 @@ -976,8 +997,8 @@ public final class ListMonitor { fetchRequest.includesPendingChanges = false fetchRequest.shouldRefreshRefetchedObjects = true - let fetchedResultsController = NSFetchedResultsController( - dataStack: dataStack, + let fetchedResultsController = CoreStoreFetchedResultsController( + context: context, fetchRequest: fetchRequest, from: from, sectionBy: sectionBy, @@ -988,7 +1009,6 @@ public final class ListMonitor { self.fetchedResultsController = fetchedResultsController self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate - self.parentStack = dataStack if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer { @@ -998,26 +1018,118 @@ public final class ListMonitor { self.sectionIndexTransformer = { $0 } } + self.transactionQueue = transactionQueue fetchedResultsControllerDelegate.handler = self fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController - prepareFetch(self, { try! fetchedResultsController.performFetch() }) + guard let coordinator = context.parentStack?.coordinator else { + + return + } + + self.observerForWillChangePersistentStore = NotificationObserver( + notificationName: NSPersistentStoreCoordinatorStoresWillChangeNotification, + object: coordinator, + closure: { [weak self] (note) -> Void in + + guard let `self` = self else { + + return + } + + self.isPersistentStoreChanging = true + + guard let removedStores = (note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore]).flatMap(Set.init) + where !Set(self.fetchedResultsController.fetchRequest.affectedStores ?? []).intersect(removedStores).isEmpty else { + + return + } + self.refetch(fetchClauses) + } + ) + + self.observerForDidChangePersistentStore = NotificationObserver( + notificationName: NSPersistentStoreCoordinatorStoresDidChangeNotification, + object: coordinator, + closure: { [weak self] (note) -> Void in + + guard let `self` = self else { + + return + } + + if !self.isPendingRefetch { + + let previousStores = Set(self.fetchedResultsController.fetchRequest.affectedStores ?? []) + let currentStores = previousStores + .subtract(note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore] ?? []) + .union(note.userInfo?[NSAddedPersistentStoresKey] as? [NSPersistentStore] ?? []) + + if previousStores != currentStores { + + self.refetch(fetchClauses) + } + } + + self.isPersistentStoreChanging = false + } + ) + + if let createAsynchronously = createAsynchronously { + + transactionQueue.async { + + try! fetchedResultsController.performFetchFromSpecifiedStores() + self.taskGroup.notify(.Main) { + + createAsynchronously(self) + } + } + } + else { + + try! fetchedResultsController.performFetchFromSpecifiedStores() + } } deinit { self.fetchedResultsControllerDelegate.fetchedResultsController = nil + self.isPersistentStoreChanging = false + } + + private var isPersistentStoreChanging: Bool = false { + + didSet { + + let newValue = self.isPersistentStoreChanging + guard newValue != oldValue else { + + return + } + + if newValue { + + self.taskGroup.enter() + } + else { + + self.taskGroup.leave() + } + } } // MARK: Private - private let fetchedResultsController: NSFetchedResultsController + private let fetchedResultsController: CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate private let sectionIndexTransformer: (sectionName: KeyPath?) -> String? + private var observerForWillChangePersistentStore: NotificationObserver! + private var observerForDidChangePersistentStore: NotificationObserver! private let taskGroup = GCDGroup() - private weak var parentStack: DataStack? + private let transactionQueue: GCDQueue private var willChangeListKey: Void? private var didChangeListKey: Void? diff --git a/CoreStore/Observing/ObjectMonitor.swift b/CoreStore/Observing/ObjectMonitor.swift index 47b5591..cacc22a 100644 --- a/CoreStore/Observing/ObjectMonitor.swift +++ b/CoreStore/Observing/ObjectMonitor.swift @@ -165,7 +165,17 @@ public final class ObjectMonitor { // MARK: Internal - internal init(dataStack: DataStack, object: T) { + internal convenience init(dataStack: DataStack, object: T) { + + self.init(context: dataStack.mainContext, object: object) + } + + internal convenience init(unsafeTransaction: UnsafeDataTransaction, object: T) { + + self.init(context: unsafeTransaction.context, object: object) + } + + private init(context: NSManagedObjectContext, object: T) { let fetchRequest = NSFetchRequest() fetchRequest.entity = object.entity @@ -175,8 +185,8 @@ public final class ObjectMonitor { fetchRequest.includesPendingChanges = false fetchRequest.shouldRefreshRefetchedObjects = true - let fetchedResultsController = NSFetchedResultsController( - dataStack: dataStack, + let fetchedResultsController = CoreStoreFetchedResultsController( + context: context, fetchRequest: fetchRequest, fetchClauses: [Where("SELF", isEqualTo: object.objectID)] ) @@ -185,11 +195,10 @@ public final class ObjectMonitor { self.fetchedResultsController = fetchedResultsController self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate - self.parentStack = dataStack fetchedResultsControllerDelegate.handler = self fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController - try! fetchedResultsController.performFetch() + try! fetchedResultsController.performFetchFromSpecifiedStores() self.lastCommittedAttributes = (self.object?.committedValuesForKeys(nil) as? [String: NSObject]) ?? [:] } @@ -202,10 +211,9 @@ public final class ObjectMonitor { // MARK: Private - private let fetchedResultsController: NSFetchedResultsController + private let fetchedResultsController: CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate private var lastCommittedAttributes = [String: NSObject]() - private weak var parentStack: DataStack? private var willChangeObjectKey: Void? private var didDeleteObjectKey: Void? diff --git a/CoreStore/Observing/UnsafeDataTransaction+Observing.swift b/CoreStore/Observing/UnsafeDataTransaction+Observing.swift new file mode 100644 index 0000000..6b0c28b --- /dev/null +++ b/CoreStore/Observing/UnsafeDataTransaction+Observing.swift @@ -0,0 +1,201 @@ +// +// UnsafeDataTransaction+Observing.swift +// CoreStore +// +// Copyright © 2016 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 +#if USE_FRAMEWORKS + import GCDKit +#endif + + +// MARK: - UnsafeDataTransaction + +@available(OSX, unavailable) +public extension UnsafeDataTransaction { + + + // MARK: Public + + /** + Creates a `ObjectMonitor` for the specified `NSManagedObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `NSManagedObject`. + + - parameter object: the `NSManagedObject` to observe changes from + - returns: a `ObjectMonitor` that monitors changes to `object` + */ + @warn_unused_result + public func monitorObject(object: T) -> ObjectMonitor { + + return ObjectMonitor( + unsafeTransaction: self, + object: object + ) + } + + /** + Creates a `ListMonitor` for a list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. + + - parameter from: a `From` clause indicating the entity type + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + - returns: a `ListMonitor` instance that monitors changes to the list + */ + @warn_unused_result + public func monitorList(from: From, _ fetchClauses: FetchClause...) -> ListMonitor { + + return self.monitorList(from, fetchClauses) + } + + /** + Creates a `ListMonitor` for a list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. + + - parameter from: a `From` clause indicating the entity type + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + - returns: a `ListMonitor` instance that monitors changes to the list + */ + @warn_unused_result + public func monitorList(from: From, _ fetchClauses: [FetchClause]) -> ListMonitor { + + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) + + return ListMonitor( + unsafeTransaction: self, + from: from, + sectionBy: nil, + fetchClauses: fetchClauses + ) + } + + /** + Asynchronously creates a `ListMonitor` for a list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. Since `NSFetchedResultsController` greedily locks the persistent store on initial fetch, you may prefer this method instead of the synchronous counterpart to avoid deadlocks while background updates/saves are being executed. + + - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance + - parameter from: a `From` clause indicating the entity type + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + */ + public func monitorList(createAsynchronously createAsynchronously: (ListMonitor) -> Void, _ from: From, _ fetchClauses: FetchClause...) { + + self.monitorList(createAsynchronously: createAsynchronously, from, fetchClauses) + } + + /** + Asynchronously creates a `ListMonitor` for a list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. Since `NSFetchedResultsController` greedily locks the persistent store on initial fetch, you may prefer this method instead of the synchronous counterpart to avoid deadlocks while background updates/saves are being executed. + + - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance + - parameter from: a `From` clause indicating the entity type + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + */ + public func monitorList(createAsynchronously createAsynchronously: (ListMonitor) -> Void, _ from: From, _ fetchClauses: [FetchClause]) { + + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) + + _ = ListMonitor( + unsafeTransaction: self, + from: from, + sectionBy: nil, + fetchClauses: fetchClauses, + createAsynchronously: createAsynchronously + ) + } + + /** + Creates a `ListMonitor` for a sectioned list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. + + - parameter from: a `From` clause indicating the entity type + - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + - returns: a `ListMonitor` instance that monitors changes to the list + */ + @warn_unused_result + public func monitorSectionedList(from: From, _ sectionBy: SectionBy, _ fetchClauses: FetchClause...) -> ListMonitor { + + return self.monitorSectionedList(from, sectionBy, fetchClauses) + } + + /** + Creates a `ListMonitor` for a sectioned list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. + + - parameter from: a `From` clause indicating the entity type + - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + - returns: a `ListMonitor` instance that monitors changes to the list + */ + @warn_unused_result + public func monitorSectionedList(from: From, _ sectionBy: SectionBy, _ fetchClauses: [FetchClause]) -> ListMonitor { + + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) + + return ListMonitor( + unsafeTransaction: self, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses + ) + } + + /** + Asynchronously creates a `ListMonitor` for a sectioned list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. Since `NSFetchedResultsController` greedily locks the persistent store on initial fetch, you may prefer this method instead of the synchronous counterpart to avoid deadlocks while background updates/saves are being executed. + + - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance + - parameter from: a `From` clause indicating the entity type + - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + */ + public func monitorSectionedList(createAsynchronously createAsynchronously: (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: FetchClause...) { + + self.monitorSectionedList(createAsynchronously: createAsynchronously, from, sectionBy, fetchClauses) + } + + /** + Asynchronously creates a `ListMonitor` for a sectioned list of `NSManagedObject`s that satisfy the specified fetch clauses. Multiple `ListObserver`s may then register themselves to be notified when changes are made to the list. Since `NSFetchedResultsController` greedily locks the persistent store on initial fetch, you may prefer this method instead of the synchronous counterpart to avoid deadlocks while background updates/saves are being executed. + + - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance + - parameter from: a `From` clause indicating the entity type + - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. + - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + */ + public func monitorSectionedList(createAsynchronously createAsynchronously: (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: [FetchClause]) { + + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) + + _ = ListMonitor( + unsafeTransaction: self, + from: from, + sectionBy: sectionBy, + fetchClauses: fetchClauses, + createAsynchronously: createAsynchronously + ) + } +} diff --git a/CoreStore/Setting Up/DataStack.swift b/CoreStore/Setting Up/DataStack.swift index d2378d1..5d5c516 100644 --- a/CoreStore/Setting Up/DataStack.swift +++ b/CoreStore/Setting Up/DataStack.swift @@ -367,11 +367,12 @@ public final class DataStack { self.configurationStoreMapping[configurationName] = persistentStore for entityDescription in (self.coordinator.managedObjectModel.entitiesForConfiguration(configurationName) ?? []) { - if self.entityConfigurationsMapping[entityDescription.managedObjectClassName] == nil { + let managedObjectClassName = entityDescription.managedObjectClassName + if self.entityConfigurationsMapping[managedObjectClassName] == nil { - self.entityConfigurationsMapping[entityDescription.managedObjectClassName] = [] + self.entityConfigurationsMapping[managedObjectClassName] = [] } - self.entityConfigurationsMapping[entityDescription.managedObjectClassName]?.insert(configurationName) + self.entityConfigurationsMapping[managedObjectClassName]?.insert(configurationName) } } } diff --git a/README.md b/README.md index 229bf40..02482ca 100644 --- a/README.md +++ b/README.md @@ -714,7 +714,7 @@ class func uniqueIDFromImportSource(source: ImportSource, inTransaction transact ``` For `ImportableUniqueObject`, the extraction and assignment of values should be implemented from the `updateFromImportSource(...)` method. The `didInsertFromImportSource(...)` by default calls `updateFromImportSource(...)`, but you can separate the implementation for inserts and updates if needed. -You can then call create/update an object by calling a transaction's `importUniqueObject(...)` method: +You can then create/update an object by calling a transaction's `importUniqueObject(...)` method: ```swift CoreStore.beginAsynchronous { (transaction) -> Void in let json: [String: AnyObject] = // ...