diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index ac0c2c8..acffbfe 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -290,6 +290,11 @@ B56321B41BD6521C006C9394 /* NSManagedObjectContext+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */; }; B56321B51BD6521C006C9394 /* NSManagedObjectModel+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */; }; B56321B61BD6521C006C9394 /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F2D1AFF849C0064E85B /* WeakObject.swift */; }; + B5677D3D1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */; }; + B5677D3E1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */; }; + B5677D3F1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */; }; + B5677D401CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */; }; + B5677D411CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */; }; B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56964D31B22FFAD0075EE4A /* DataStack+Migration.swift */; }; B56965241B356B820075EE4A /* MigrationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56965231B356B820075EE4A /* MigrationResult.swift */; }; B58B22F51C93C1BA00521925 /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F03A53019C5C6DA005002A5 /* CoreStore.framework */; }; @@ -298,6 +303,11 @@ B598514B1C90289F00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59AFF401C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift */; }; B59983491CA54BC100E1A417 /* CSBaseDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5519A581CA2008C002BEF78 /* CSBaseDataTransaction.swift */; }; B59AFF411C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59AFF401C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift */; }; + B59FA0AE1CCBAC95007C9BCA /* ICloudStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */; }; + B59FA0AF1CCBACA6007C9BCA /* ICloudStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */; }; + B59FA0B01CCBACA7007C9BCA /* ICloudStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */; }; + B59FA0B11CCBACA7007C9BCA /* ICloudStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */; }; + B59FA0B21CCBACA8007C9BCA /* ICloudStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */; }; B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A261201B64BFDB006EB6D3 /* MigrationType.swift */; }; B5A5F2661CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; B5A5F2671CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; @@ -652,9 +662,11 @@ B563216F1BD65082006C9394 /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B56321791BD650DE006C9394 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS2.0.sdk/System/Library/Frameworks/CoreData.framework; sourceTree = DEVELOPER_DIR; }; B563217B1BD650E3006C9394 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS2.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudStoreObserver.swift; sourceTree = ""; }; B56964D31B22FFAD0075EE4A /* DataStack+Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataStack+Migration.swift"; sourceTree = ""; }; B56965231B356B820075EE4A /* MigrationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationResult.swift; sourceTree = ""; }; B59AFF401C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSPersistentStoreCoordinator+Setup.swift"; sourceTree = ""; }; + B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudStore.swift; sourceTree = ""; }; B5A261201B64BFDB006EB6D3 /* MigrationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationType.swift; sourceTree = ""; }; B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSelect.swift; sourceTree = ""; }; B5AD60CD1C90141E00F2B2E8 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; @@ -1228,6 +1240,8 @@ B5FE4DA61C84FB4400FA6A91 /* InMemoryStore.swift */, B5FE4DAB1C85D44E00FA6A91 /* SQLiteStore.swift */, B5D3F6441C887C0A00C7492A /* LegacySQLiteStore.swift */, + B59FA0AD1CCBAC95007C9BCA /* ICloudStore.swift */, + B5677D3C1CD3B1E400322BFC /* ICloudStoreObserver.swift */, ); path = StorageInterfaces; sourceTree = ""; @@ -1576,6 +1590,7 @@ B5ECDC291CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, B5E84F121AFF847B0064E85B /* OrderBy.swift in Sources */, B546F9581C99B17400D5AC55 /* CSCoreStore+Setup.swift in Sources */, + B5677D3D1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */, B5E84F361AFF85470064E85B /* NSManagedObjectContext+Setup.swift in Sources */, B5FAD6AE1B518DCB00714891 /* CoreStore+Migration.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, @@ -1633,6 +1648,7 @@ B5E84F101AFF847B0064E85B /* GroupBy.swift in Sources */, B5E84F201AFF84860064E85B /* DataStack+Observing.swift in Sources */, B501FDDD1CA8D05000BE22EF /* CSSectionBy.swift in Sources */, + B59FA0AE1CCBAC95007C9BCA /* ICloudStore.swift in Sources */, B5E84EF81AFF846E0064E85B /* CoreStore+Transaction.swift in Sources */, B5E84F301AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift in Sources */, B546F9691C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -1705,6 +1721,7 @@ B5ECDC2B1CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, 82BA18A11C4BBD1D00A0916E /* CoreStore.swift in Sources */, B546F9591C99B17400D5AC55 /* CSCoreStore+Setup.swift in Sources */, + B5677D3F1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */, 82BA18CF1C4BBD7100A0916E /* Functions.swift in Sources */, 82BA18A31C4BBD2200A0916E /* DataStack.swift in Sources */, 82BA18C81C4BBD5900A0916E /* MigrationChain.swift in Sources */, @@ -1762,6 +1779,7 @@ 82BA18CB1C4BBD6400A0916E /* NSManagedObject+Convenience.swift in Sources */, 82BA18B51C4BBD3F00A0916E /* BaseDataTransaction+Querying.swift in Sources */, B501FDDF1CA8D05000BE22EF /* CSSectionBy.swift in Sources */, + B59FA0B01CCBACA7007C9BCA /* ICloudStore.swift in Sources */, 82BA18D31C4BBD7100A0916E /* NSManagedObjectContext+CoreStore.swift in Sources */, 82BA18AD1C4BBD3100A0916E /* UnsafeDataTransaction.swift in Sources */, B546F96A1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -1803,6 +1821,7 @@ buildActionMask = 2147483647; files = ( B5DBE2D01C9914A900B5CEFA /* CSCoreStore.swift in Sources */, + B5677D411CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */, B52DD1BE1BE1F94300949AFE /* NSProgress+Convenience.swift in Sources */, B5ECDC151CA816E500C7F112 /* CSTweak.swift in Sources */, B546F9761C9C553300D5AC55 /* SetupResult.swift in Sources */, @@ -1870,6 +1889,7 @@ B52DD1CB1BE1F94600949AFE /* WeakObject.swift in Sources */, B52DD1C11BE1F94600949AFE /* Functions.swift in Sources */, B53FBA0F1CAB5E6500F0D40A /* CSCoreStore+Migrating.swift in Sources */, + B59FA0B21CCBACA8007C9BCA /* ICloudStore.swift in Sources */, B52DD19A1BE1F92800949AFE /* CoreStore+Logging.swift in Sources */, B52DD1A71BE1F93200949AFE /* BaseDataTransaction+Querying.swift in Sources */, B546F96C1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -1944,6 +1964,7 @@ B5ECDC2C1CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, B56321911BD65216006C9394 /* BaseDataTransaction+Importing.swift in Sources */, B546F95A1C99B17400D5AC55 /* CSCoreStore+Setup.swift in Sources */, + B5677D401CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */, B56321941BD65216006C9394 /* CoreStore+Querying.swift in Sources */, B56321811BD65216006C9394 /* DataStack.swift in Sources */, B56321A81BD65219006C9394 /* NSManagedObject+Convenience.swift in Sources */, @@ -2001,6 +2022,7 @@ B56321851BD65216006C9394 /* CoreStore+Logging.swift in Sources */, B56321921BD65216006C9394 /* BaseDataTransaction+Querying.swift in Sources */, B501FDE01CA8D05000BE22EF /* CSSectionBy.swift in Sources */, + B59FA0B11CCBACA7007C9BCA /* ICloudStore.swift in Sources */, B56321B11BD6521C006C9394 /* NSManagedObjectContext+CoreStore.swift in Sources */, B563218D1BD65216006C9394 /* CoreStore+Transaction.swift in Sources */, B546F96B1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, @@ -2047,6 +2069,7 @@ B5ECDC3E1CA836BE00C7F112 /* CSCoreStore+Setup.swift in Sources */, B5D9E2F31CA2C317007A9D52 /* CoreStoreError.swift in Sources */, B5D9E2F41CA2C317007A9D52 /* Where.swift in Sources */, + B5677D3E1CD3B1E400322BFC /* ICloudStoreObserver.swift in Sources */, B5D9E2F51CA2C317007A9D52 /* FetchedResultsControllerDelegate.swift in Sources */, B5D9E2F61CA2C317007A9D52 /* MigrationType.swift in Sources */, B5D9E2F71CA2C317007A9D52 /* DataStack+Querying.swift in Sources */, @@ -2132,6 +2155,7 @@ B5D9E3211CA2C317007A9D52 /* CoreStore+Transaction.swift in Sources */, B5D9E3221CA2C317007A9D52 /* NSManagedObjectContext+CoreStore.swift in Sources */, B5D9E3481CA2C6C4007A9D52 /* GCDTimer.swift in Sources */, + B59FA0AF1CCBACA6007C9BCA /* ICloudStore.swift in Sources */, B5D9E3231CA2C317007A9D52 /* CoreStore+Observing.swift in Sources */, B5ECDC3D1CA836BA00C7F112 /* CSError.swift in Sources */, B5D9E3241CA2C317007A9D52 /* BaseDataTransaction+Importing.swift in Sources */, diff --git a/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj b/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj index 73de165..48e8c9e 100644 --- a/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj +++ b/CoreStoreDemo/CoreStoreDemo.xcodeproj/project.pbxproj @@ -459,7 +459,7 @@ INFOPLIST_FILE = CoreStoreDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStoreDemo; + PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.corestore.demo; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -471,7 +471,7 @@ INFOPLIST_FILE = CoreStoreDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStoreDemo; + PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.corestore.demo; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/CoreStoreDemo/CoreStoreDemo/Info.plist b/CoreStoreDemo/CoreStoreDemo/Info.plist index 90a8eac..2febe10 100644 --- a/CoreStoreDemo/CoreStoreDemo/Info.plist +++ b/CoreStoreDemo/CoreStoreDemo/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion en + CFBundleDisplayName + CoreStore CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -20,8 +22,6 @@ ???? CFBundleVersion 1 - CFBundleDisplayName - CoreStore LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/Sources/Internal/NSManagedObjectContext+Setup.swift b/Sources/Internal/NSManagedObjectContext+Setup.swift index 5b57446..0e268d9 100644 --- a/Sources/Internal/NSManagedObjectContext+Setup.swift +++ b/Sources/Internal/NSManagedObjectContext+Setup.swift @@ -68,7 +68,22 @@ internal extension NSManagedObjectContext { context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.undoManager = nil context.setupForCoreStoreWithContextName("com.corestore.rootcontext") - + context.observerForDidImportUbiquitousContentChangesNotification = NotificationObserver( + notificationName: NSPersistentStoreDidImportUbiquitousContentChangesNotification, + object: coordinator, + closure: { [weak context] (note) -> Void in + + context?.performBlock { () -> Void in + + let updatedObjectIDs = (note.userInfo?[NSUpdatedObjectsKey] as? Set) ?? [] + for objectID in updatedObjectIDs { + + context?.objectWithID(objectID).willAccessValueForKey(nil) + } + context?.mergeChangesFromContextDidSaveNotification(note) + } + } + ) return context } @@ -96,7 +111,6 @@ internal extension NSManagedObjectContext { } } ) - return context } @@ -107,6 +121,7 @@ internal extension NSManagedObjectContext { static var parentStack: Void? static var observerForDidSaveNotification: Void? + static var observerForDidImportUbiquitousContentChangesNotification: Void? } @nonobjc @@ -128,4 +143,24 @@ internal extension NSManagedObjectContext { ) } } + + @nonobjc + private var observerForDidImportUbiquitousContentChangesNotification: NotificationObserver? { + + get { + + return getAssociatedObjectForKey( + &PropertyKeys.observerForDidImportUbiquitousContentChangesNotification, + inObject: self + ) + } + set { + + setAssociatedRetainedObject( + newValue, + forKey: &PropertyKeys.observerForDidImportUbiquitousContentChangesNotification, + inObject: self + ) + } + } } diff --git a/Sources/Migrating/DataStack+Migration.swift b/Sources/Migrating/DataStack+Migration.swift index d5827e4..31e7a9f 100644 --- a/Sources/Migrating/DataStack+Migration.swift +++ b/Sources/Migrating/DataStack+Migration.swift @@ -283,6 +283,126 @@ public extension DataStack { } } + /** + Asynchronously adds a `CloudStorage` to the stack. Migrations are also initiated by default. + ``` + try dataStack.addStorage( + ICloudStore( + ubiquitousContentName: "MyAppCloudData", + ubiquitousContentTransactionLogsSubdirectory: "logs/config1", + ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", + ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", + configuration: "Config1", + cloudStorageOptions: .AllowSynchronousLightweightMigration + ), + completion: { result in + switch result { + case .Success(let storage): // ... + case .Failure(let error): // ... + } + } + ) + ``` + + - parameter storage: the cloud storage + - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `SetupResult` argument indicates the result. This closure is NOT executed if an error is thrown, but will be executed with a `.Failure` result if an error occurs asynchronously. Note that the `LocalStorage` associated to the `SetupResult.Success` may not always be the same instance as the parameter argument if a previous `LocalStorage` was already added at the same URL and with the same configuration. + - throws: a `CoreStoreError` value indicating the failure + */ + public func addStorage(storage: T, completion: (SetupResult) -> Void) throws { + + let cacheFileURL = storage.cacheFileURL + try self.coordinator.performSynchronously { + + if let _ = self.persistentStoreForStorage(storage) { + + GCDQueue.Main.async { + + completion(SetupResult(storage)) + } + return + } + + if let persistentStore = self.coordinator.persistentStoreForURL(cacheFileURL) { + + if let existingStorage = persistentStore.storageInterface as? T + where storage.matchesPersistentStore(persistentStore) { + + GCDQueue.Main.async { + + completion(SetupResult(existingStorage)) + } + return + } + + let error = CoreStoreError.DifferentStorageExistsAtURL(existingPersistentStoreURL: cacheFileURL) + CoreStore.log( + error, + "Failed to add \(typeName(storage)) at \"\(cacheFileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists." + ) + throw error + } + + do { + + var cloudStorageOptions = storage.cloudStorageOptions + cloudStorageOptions.remove(.RecreateLocalStoreOnModelMismatch) + + let storeOptions = storage.storeOptionsForOptions(cloudStorageOptions) + do { + + try NSFileManager.defaultManager().createDirectoryAtURL( + cacheFileURL.URLByDeletingLastPathComponent!, + withIntermediateDirectories: true, + attributes: nil + ) + try self.createPersistentStoreFromStorage( + storage, + finalURL: cacheFileURL, + finalStoreOptions: storeOptions + ) + GCDQueue.Main.async { + + completion(SetupResult(storage)) + } + } + catch let error as NSError where storage.cloudStorageOptions.contains(.RecreateLocalStoreOnModelMismatch) && error.isCoreDataMigrationError { + + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType( + storage.dynamicType.storeType, + URL: cacheFileURL, + options: storeOptions + ) + try _ = self.model[metadata].flatMap(storage.eraseStorageAndWait) + + try self.createPersistentStoreFromStorage( + storage, + finalURL: cacheFileURL, + finalStoreOptions: storeOptions + ) + } + } + catch let error as NSError + where error.code == NSFileReadNoSuchFileError && error.domain == NSCocoaErrorDomain { + + try self.addStorageAndWait(storage) + + GCDQueue.Main.async { + + completion(SetupResult(storage)) + } + } + catch { + + let storeError = CoreStoreError(error) + CoreStore.log( + storeError, + "Failed to load \(typeName(NSPersistentStore)) metadata." + ) + throw storeError + } + } + } + /** Migrates a local storage to match the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack. diff --git a/Sources/Observing/ListMonitor.swift b/Sources/Observing/ListMonitor.swift index 9b835bd..f5c5cb8 100644 --- a/Sources/Observing/ListMonitor.swift +++ b/Sources/Observing/ListMonitor.swift @@ -634,19 +634,6 @@ public final class ListMonitor: Hashable { // MARK: Internal - internal var willChangeListKey: Void? - internal var didChangeListKey: Void? - internal var willRefetchListKey: Void? - internal var didRefetchListKey: Void? - - internal var didInsertObjectKey: Void? - internal var didDeleteObjectKey: Void? - internal var didUpdateObjectKey: Void? - internal var didMoveObjectKey: Void? - - internal var didInsertSectionKey: Void? - internal var didDeleteSectionKey: Void? - internal convenience init(dataStack: DataStack, from: From, sectionBy: SectionBy?, applyFetchClauses: (fetchRequest: NSFetchRequest) -> Void) { self.init( @@ -1049,6 +1036,19 @@ public final class ListMonitor: Hashable { // MARK: Private + private var willChangeListKey: Void? + private var didChangeListKey: Void? + private var willRefetchListKey: Void? + private var didRefetchListKey: Void? + + private var didInsertObjectKey: Void? + private var didDeleteObjectKey: Void? + private var didUpdateObjectKey: Void? + private var didMoveObjectKey: Void? + + private var didInsertSectionKey: Void? + private var didDeleteSectionKey: Void? + private let fetchedResultsController: CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate private let sectionIndexTransformer: (sectionName: KeyPath?) -> String? @@ -1320,18 +1320,18 @@ public func == (lhs: ListMonitor, rhs // MARK: - Notification Keys -internal let ListMonitorWillChangeListNotification = "ListMonitorWillChangeListNotification" -internal let ListMonitorDidChangeListNotification = "ListMonitorDidChangeListNotification" -internal let ListMonitorWillRefetchListNotification = "ListMonitorWillRefetchListNotification" -internal let ListMonitorDidRefetchListNotification = "ListMonitorDidRefetchListNotification" +private let ListMonitorWillChangeListNotification = "ListMonitorWillChangeListNotification" +private let ListMonitorDidChangeListNotification = "ListMonitorDidChangeListNotification" +private let ListMonitorWillRefetchListNotification = "ListMonitorWillRefetchListNotification" +private let ListMonitorDidRefetchListNotification = "ListMonitorDidRefetchListNotification" -internal let ListMonitorDidInsertObjectNotification = "ListMonitorDidInsertObjectNotification" -internal let ListMonitorDidDeleteObjectNotification = "ListMonitorDidDeleteObjectNotification" -internal let ListMonitorDidUpdateObjectNotification = "ListMonitorDidUpdateObjectNotification" -internal let ListMonitorDidMoveObjectNotification = "ListMonitorDidMoveObjectNotification" +private let ListMonitorDidInsertObjectNotification = "ListMonitorDidInsertObjectNotification" +private let ListMonitorDidDeleteObjectNotification = "ListMonitorDidDeleteObjectNotification" +private let ListMonitorDidUpdateObjectNotification = "ListMonitorDidUpdateObjectNotification" +private let ListMonitorDidMoveObjectNotification = "ListMonitorDidMoveObjectNotification" -internal let ListMonitorDidInsertSectionNotification = "ListMonitorDidInsertSectionNotification" -internal let ListMonitorDidDeleteSectionNotification = "ListMonitorDidDeleteSectionNotification" +private let ListMonitorDidInsertSectionNotification = "ListMonitorDidInsertSectionNotification" +private let ListMonitorDidDeleteSectionNotification = "ListMonitorDidDeleteSectionNotification" private let UserInfoKeyObject = "UserInfoKeyObject" private let UserInfoKeyIndexPath = "UserInfoKeyIndexPath" diff --git a/Sources/Setup/CoreStore+Setup.swift b/Sources/Setup/CoreStore+Setup.swift index 74b8d02..11debcf 100644 --- a/Sources/Setup/CoreStore+Setup.swift +++ b/Sources/Setup/CoreStore+Setup.swift @@ -131,6 +131,30 @@ public extension CoreStore { return try self.defaultStack.addStorageAndWait(storage) } + /** + Adds a `CloudStorage` to the `defaultStack` and blocks until completion. + ``` + try CoreStore.addStorageAndWait( + ICloudStore( + ubiquitousContentName: "MyAppCloudData", + ubiquitousContentTransactionLogsSubdirectory: "logs/config1", + ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", + ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", + configuration: "Config1", + cloudStorageOptions: .AllowSynchronousLightweightMigration + ) + ) + ``` + + - parameter storage: the local storage + - throws: a `CoreStoreError` value indicating the failure + - returns: the cloud storage added to the stack. Note that this may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. + */ + public static func addStorageAndWait(storage: T) throws -> T { + + return try self.defaultStack.addStorageAndWait(storage) + } + // MARK: Deprecated diff --git a/Sources/Setup/DataStack.swift b/Sources/Setup/DataStack.swift index 8e35795..cc672a0 100644 --- a/Sources/Setup/DataStack.swift +++ b/Sources/Setup/DataStack.swift @@ -238,12 +238,10 @@ public final class DataStack { do { - var storeOptions = storage.storeOptions ?? [:] - if storage.localStorageOptions.contains(.AllowSynchronousLightweightMigration) { - - storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true - storeOptions[NSInferMappingModelAutomaticallyOption] = true - } + var localStorageOptions = storage.localStorageOptions + localStorageOptions.remove(.RecreateStoreOnModelMismatch) + + let storeOptions = storage.storeOptionsForOptions(localStorageOptions) do { try NSFileManager.defaultManager().createDirectoryAtURL( @@ -287,6 +285,100 @@ public final class DataStack { } } + /** + Adds a `CloudStorage` to the stack and blocks until completion. + ``` + try dataStack.addStorageAndWait( + ICloudStore( + ubiquitousContentName: "MyAppCloudData", + ubiquitousContentTransactionLogsSubdirectory: "logs/config1", + ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", + ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", + configuration: "Config1", + cloudStorageOptions: .AllowSynchronousLightweightMigration + ) + ) + ``` + + - parameter storage: the local storage + - throws: a `CoreStoreError` value indicating the failure + - returns: the cloud storage added to the stack. Note that this may not always be the same instance as the parameter argument if a previous `CloudStorage` was already added at the same URL and with the same configuration. + */ + public func addStorageAndWait(storage: T) throws -> T { + + return try self.coordinator.performSynchronously { + + if let _ = self.persistentStoreForStorage(storage) { + + return storage + } + + let cacheFileURL = storage.cacheFileURL + if let persistentStore = self.coordinator.persistentStoreForURL(cacheFileURL) { + + if let existingStorage = persistentStore.storageInterface as? T + where storage.matchesPersistentStore(persistentStore) { + + return existingStorage + } + + let error = CoreStoreError.DifferentStorageExistsAtURL(existingPersistentStoreURL: cacheFileURL) + CoreStore.log( + error, + "Failed to add \(typeName(storage)) at \"\(cacheFileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists." + ) + throw error + } + + do { + + var cloudStorageOptions = storage.cloudStorageOptions + cloudStorageOptions.remove(.RecreateLocalStoreOnModelMismatch) + + let storeOptions = storage.storeOptionsForOptions(cloudStorageOptions) + do { + + try NSFileManager.defaultManager().createDirectoryAtURL( + cacheFileURL.URLByDeletingLastPathComponent!, + withIntermediateDirectories: true, + attributes: nil + ) + try self.createPersistentStoreFromStorage( + storage, + finalURL: cacheFileURL, + finalStoreOptions: storeOptions + ) + return storage + } + catch let error as NSError where storage.cloudStorageOptions.contains(.RecreateLocalStoreOnModelMismatch) && error.isCoreDataMigrationError { + + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType( + storage.dynamicType.storeType, + URL: cacheFileURL, + options: storeOptions + ) + try _ = self.model[metadata].flatMap(storage.eraseStorageAndWait) + + try self.createPersistentStoreFromStorage( + storage, + finalURL: cacheFileURL, + finalStoreOptions: storeOptions + ) + return storage + } + } + catch { + + let storeError = CoreStoreError(error) + CoreStore.log( + storeError, + "Failed to add \(typeName(storage)) to the stack." + ) + throw storeError + } + } + } + // MARK: Internal @@ -407,6 +499,7 @@ public final class DataStack { self.entityConfigurationsMapping[managedObjectClassName]?.insert(configurationName) } } + storage.didAddToDataStack(self) return persistentStore } diff --git a/Sources/Setup/StorageInterfaces/ICloudStore.swift b/Sources/Setup/StorageInterfaces/ICloudStore.swift new file mode 100644 index 0000000..77f79a6 --- /dev/null +++ b/Sources/Setup/StorageInterfaces/ICloudStore.swift @@ -0,0 +1,494 @@ +// +// ICloudStore.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: - ICloudStore + +/** + A storage interface backed by an SQLite database managed by iCloud. + */ +public class ICloudStore: CloudStorage { + + /** + Initializes an iCloud store interface from the given ubiquitous store information. Returns `nil` if the container could not be located or if iCloud storage is unavailable for the current user or device + ``` + try CoreStore.addStorage( + ICloudStore( + ubiquitousContentName: "MyAppCloudData", + ubiquitousContentTransactionLogsSubdirectory: "logs/config1", + ubiquitousContainerID: "iCloud.com.mycompany.myapp.containername", + ubiquitousPeerToken: "9614d658014f4151a95d8048fb717cf0", + configuration: "Config1", + cloudStorageOptions: .AllowSynchronousLightweightMigration + ) + completion: { result in + // ... + } + ) + ``` + + - parameter ubiquitousContentName: the name of the store in iCloud. This is required and should not be empty, and should not contain periods (`.`). + - parameter ubiquitousContentTransactionLogsSubdirectory: an optional subdirectory path for the transaction logs + - parameter ubiquitousContainerID: a container if your app has multiple ubiquity container identifiers in its entitlements + - parameter ubiquitousPeerToken: a per-application salt to allow multiple apps on the same device to share a Core Data store integrated with iCloud + - parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `ubiquitousContentName` explicitly for each of them. + - parameter mappingModelBundles: a list of `NSBundle`s from which to search mapping models for migration. + - parameter cloudStorageOptions: When the `ICloudStore` is passed to the `DataStack`'s `addStorage()` methods, tells the `DataStack` how to setup the persistent store. Defaults to `.None`. + */ + public required init?(ubiquitousContentName: String, ubiquitousContentTransactionLogsSubdirectory: String? = nil, ubiquitousContainerID: String? = nil, ubiquitousPeerToken: String? = nil, configuration: String? = nil, cloudStorageOptions: CloudStorageOptions = nil) { + + CoreStore.assert( + !ubiquitousContentName.isEmpty, + "The ubiquitousContentName cannot be empty." + ) + CoreStore.assert( + !ubiquitousContentName.containsString("."), + "The ubiquitousContentName cannot contain periods." + ) + CoreStore.assert( + ubiquitousContentTransactionLogsSubdirectory?.isEmpty != true, + "The ubiquitousContentURLRelativePath should not be empty if provided." + ) + CoreStore.assert( + ubiquitousPeerToken?.isEmpty != true, + "The ubiquitousPeerToken should not be empty if provided." + ) + + let fileManager = NSFileManager.defaultManager() + guard let cacheFileURL = fileManager.URLForUbiquityContainerIdentifier(ubiquitousContainerID) else { + + return nil + } + + var storeOptions: [String: AnyObject] = [ + NSSQLitePragmasOption: ["journal_mode": "WAL"], + NSPersistentStoreUbiquitousContentNameKey: ubiquitousContentName + ] + storeOptions[NSPersistentStoreUbiquitousContentURLKey] = ubiquitousContentTransactionLogsSubdirectory + storeOptions[NSPersistentStoreUbiquitousContainerIdentifierKey] = ubiquitousContainerID + storeOptions[NSPersistentStoreUbiquitousPeerTokenOption] = ubiquitousPeerToken + + self.cacheFileURL = cacheFileURL + self.configuration = configuration + self.cloudStorageOptions = cloudStorageOptions + self.storeOptions = storeOptions + } + + public func addUbiquitousStoreObserver(observer: T) { + + CoreStore.assert( + NSThread.isMainThread(), + "Attempted to add an observer of type \(typeName(observer)) outside the main thread." + ) + + self.removeUbiquitousStoreObserver(observer) + + self.registerNotification( + &self.willFinishInitialImportKey, + name: ICloudUbiquitousStoreWillFinishInitialImportNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreWillFinishUbiquitousStoreInitialImport(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.didFinishInitialImportKey, + name: ICloudUbiquitousStoreDidFinishInitialImportNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreDidFinishUbiquitousStoreInitialImport(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.willAddAccountKey, + name: ICloudUbiquitousStoreWillAddAccountNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreWillAddAccount(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.didAddAccountKey, + name: ICloudUbiquitousStoreDidAddAccountNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreDidAddAccount(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.willRemoveAccountKey, + name: ICloudUbiquitousStoreWillRemoveAccountNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreWillRemoveAccount(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.didRemoveAccountKey, + name: ICloudUbiquitousStoreDidRemoveAccountNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreDidRemoveAccount(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.willRemoveContentKey, + name: ICloudUbiquitousStoreWillRemoveContentNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreWillRemoveContent(storage: storage, dataStack: dataStack) + } + ) + self.registerNotification( + &self.didRemoveContentKey, + name: ICloudUbiquitousStoreDidRemoveContentNotification, + toObserver: observer, + callback: { (observer, storage, dataStack) in + + observer.iCloudStoreDidRemoveContent(storage: storage, dataStack: dataStack) + } + ) + } + + public func removeUbiquitousStoreObserver(observer: ICloudStoreObserver) { + + CoreStore.assert( + NSThread.isMainThread(), + "Attempted to remove an observer of type \(typeName(observer)) outside the main thread." + ) + let nilValue: AnyObject? = nil + setAssociatedRetainedObject( + nilValue, + forKey: &self.willFinishInitialImportKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.didFinishInitialImportKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.willAddAccountKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.didAddAccountKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.willRemoveAccountKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.didRemoveAccountKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.willRemoveContentKey, + inObject: observer + ) + setAssociatedRetainedObject( + nilValue, + forKey: &self.didRemoveContentKey, + inObject: observer + ) + } + + + // MARK: StorageInterface + + /** + The string identifier for the `NSPersistentStore`'s `type` property. For `SQLiteStore`s, this is always set to `NSSQLiteStoreType`. + */ + public static let storeType = NSSQLiteStoreType + + /** + The configuration name in the model file + */ + public let configuration: String? + + /** + The options dictionary for the `NSPersistentStore`. For `SQLiteStore`s, this is always set to + ``` + [NSSQLitePragmasOption: ["journal_mode": "WAL"]] + ``` + */ + public let storeOptions: [String: AnyObject]? + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didAddToDataStack(dataStack: DataStack) { + + self.didRemoveFromDataStack(dataStack) + + self.dataStack = dataStack + let coordinator = dataStack.coordinator + + setAssociatedRetainedObject( + NotificationObserver( + notificationName: NSPersistentStoreCoordinatorStoresWillChangeNotification, + object: coordinator, + closure: { [weak self, weak dataStack] (note) -> Void in + + guard let `self` = self, + let dataStack = dataStack, + let userInfo = note.userInfo, + let transitionType = userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] as? NSNumber else { + + return + } + + let notification: String + switch NSPersistentStoreUbiquitousTransitionType(rawValue: transitionType.unsignedIntegerValue) { + + case .InitialImportCompleted?: + notification = ICloudUbiquitousStoreWillFinishInitialImportNotification + + case .AccountAdded?: + notification = ICloudUbiquitousStoreWillAddAccountNotification + + case .AccountRemoved?: + notification = ICloudUbiquitousStoreWillRemoveAccountNotification + + case .ContentRemoved?: + notification = ICloudUbiquitousStoreWillRemoveContentNotification + + default: + return + } + NSNotificationCenter.defaultCenter().postNotificationName( + notification, + object: self, + userInfo: [UserInfoKeyDataStack: dataStack] + ) + } + ), + forKey: &Static.persistentStoreCoordinatorWillChangeStores, + inObject: self + ) + setAssociatedRetainedObject( + NotificationObserver( + notificationName: NSPersistentStoreCoordinatorStoresDidChangeNotification, + object: coordinator, + closure: { [weak self, weak dataStack] (note) -> Void in + + guard let `self` = self, + let dataStack = dataStack, + let userInfo = note.userInfo, + let transitionType = userInfo[NSPersistentStoreUbiquitousTransitionTypeKey] as? NSNumber else { + + return + } + + let notification: String + switch NSPersistentStoreUbiquitousTransitionType(rawValue: transitionType.unsignedIntegerValue) { + + case .InitialImportCompleted?: + notification = ICloudUbiquitousStoreDidFinishInitialImportNotification + + case .AccountAdded?: + notification = ICloudUbiquitousStoreDidAddAccountNotification + + case .AccountRemoved?: + notification = ICloudUbiquitousStoreDidRemoveAccountNotification + + case .ContentRemoved?: + notification = ICloudUbiquitousStoreDidRemoveContentNotification + + default: + return + } + NSNotificationCenter.defaultCenter().postNotificationName( + notification, + object: self, + userInfo: [UserInfoKeyDataStack: dataStack] + ) + } + ), + forKey: &Static.persistentStoreCoordinatorDidChangeStores, + inObject: self + ) + } + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didRemoveFromDataStack(dataStack: DataStack) { + + let coordinator = dataStack.coordinator + let nilValue: AnyObject? = nil + setAssociatedRetainedObject( + nilValue, + forKey: &Static.persistentStoreCoordinatorWillChangeStores, + inObject: coordinator + ) + setAssociatedRetainedObject( + nilValue, + forKey: &Static.persistentStoreCoordinatorDidChangeStores, + inObject: coordinator + ) + + self.dataStack = nil + } + + + // MARK: CloudStorage + + /** + The `NSURL` that points to the ubiquity container file + */ + public let cacheFileURL: NSURL + + /** + Options that tell the `DataStack` how to setup the persistent store + */ + public var cloudStorageOptions: CloudStorageOptions + + /** + The options dictionary for the specified `CloudStorageOptions` + */ + public func storeOptionsForOptions(options: CloudStorageOptions) -> [String: AnyObject]? { + + if options == .None { + + return self.storeOptions + } + + var storeOptions = self.storeOptions ?? [:] + if options.contains(.AllowSynchronousLightweightMigration) { + + storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true + storeOptions[NSInferMappingModelAutomaticallyOption] = true + } + if options.contains(.RecreateLocalStoreOnModelMismatch) { + + storeOptions[NSPersistentStoreRebuildFromUbiquitousContentOption] = true + } + return storeOptions + } + + /** + Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. + */ + public func eraseStorageAndWait(soureModel soureModel: NSManagedObjectModel) throws { + + // TODO: check if attached to persistent store + + let cacheFileURL = self.cacheFileURL + try autoreleasepool { + + let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) + let options = [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"], + NSPersistentStoreRemoveUbiquitousMetadataOption: true + ] + let store = try journalUpdatingCoordinator.addPersistentStoreWithType( + self.dynamicType.storeType, + configuration: self.configuration, + URL: cacheFileURL, + options: options + ) + try journalUpdatingCoordinator.removePersistentStore(store) + try NSPersistentStoreCoordinator.removeUbiquitousContentAndPersistentStoreAtURL( + cacheFileURL, + options: options + ) + try NSFileManager.defaultManager().removeItemAtURL(cacheFileURL) + } + } + + + // MARK: Private + + private struct Static { + + private static var persistentStoreCoordinatorWillChangeStores: Void? + private static var persistentStoreCoordinatorDidChangeStores: Void? + } + + private var willFinishInitialImportKey: Void? + private var didFinishInitialImportKey: Void? + private var willAddAccountKey: Void? + private var didAddAccountKey: Void? + private var willRemoveAccountKey: Void? + private var didRemoveAccountKey: Void? + private var willRemoveContentKey: Void? + private var didRemoveContentKey: Void? + + private weak var dataStack: DataStack? + + private func registerNotification(notificationKey: UnsafePointer, name: String, toObserver observer: T, callback: (observer: T, storage: ICloudStore, dataStack: DataStack) -> Void) { + + setAssociatedRetainedObject( + NotificationObserver( + notificationName: name, + object: self, + closure: { [weak self, weak observer] (note) -> Void in + + guard let `self` = self, + let observer = observer, + let dataStack = note.userInfo?[UserInfoKeyDataStack] as? DataStack + where self.dataStack === dataStack else { + + return + } + callback(observer: observer, storage: self, dataStack: dataStack) + } + ), + forKey: notificationKey, + inObject: observer + ) + } +} + + +// MARK: - Notification Keys + +private let ICloudUbiquitousStoreWillFinishInitialImportNotification = "ICloudUbiquitousStoreWillFinishInitialImportNotification" +private let ICloudUbiquitousStoreDidFinishInitialImportNotification = "ICloudUbiquitousStoreDidFinishInitialImportNotification" +private let ICloudUbiquitousStoreWillAddAccountNotification = "ICloudUbiquitousStoreWillAddAccountNotification" +private let ICloudUbiquitousStoreDidAddAccountNotification = "ICloudUbiquitousStoreDidAddAccountNotification" +private let ICloudUbiquitousStoreWillRemoveAccountNotification = "ICloudUbiquitousStoreWillRemoveAccountNotification" +private let ICloudUbiquitousStoreDidRemoveAccountNotification = "ICloudUbiquitousStoreDidRemoveAccountNotification" +private let ICloudUbiquitousStoreWillRemoveContentNotification = "ICloudUbiquitousStoreWillRemoveContentNotification" +private let ICloudUbiquitousStoreDidRemoveContentNotification = "ICloudUbiquitousStoreDidRemoveContentNotification" + +private let UserInfoKeyDataStack = "UserInfoKeyDataStack" diff --git a/Sources/Setup/StorageInterfaces/ICloudStoreObserver.swift b/Sources/Setup/StorageInterfaces/ICloudStoreObserver.swift new file mode 100644 index 0000000..48ecfb3 --- /dev/null +++ b/Sources/Setup/StorageInterfaces/ICloudStoreObserver.swift @@ -0,0 +1,59 @@ +// +// ICloudStoreObserver.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 + + +// MARK: - ICloudStoreObserver + +public protocol ICloudStoreObserver: class { + + func iCloudStoreWillFinishUbiquitousStoreInitialImport(storage storage: ICloudStore, dataStack: DataStack) + func iCloudStoreDidFinishUbiquitousStoreInitialImport(storage storage: ICloudStore, dataStack: DataStack) + + func iCloudStoreWillAddAccount(storage storage: ICloudStore, dataStack: DataStack) + func iCloudStoreDidAddAccount(storage storage: ICloudStore, dataStack: DataStack) + + func iCloudStoreWillRemoveAccount(storage storage: ICloudStore, dataStack: DataStack) + func iCloudStoreDidRemoveAccount(storage storage: ICloudStore, dataStack: DataStack) + + func iCloudStoreWillRemoveContent(storage storage: ICloudStore, dataStack: DataStack) + func iCloudStoreDidRemoveContent(storage storage: ICloudStore, dataStack: DataStack) +} + +public extension ICloudStoreObserver { + + public func iCloudStoreWillFinishUbiquitousStoreInitialImport(storage storage: ICloudStore, dataStack: DataStack) {} + public func iCloudStoreDidFinishUbiquitousStoreInitialImport(storage storage: ICloudStore, dataStack: DataStack) {} + + public func iCloudStoreWillAddAccount(storage storage: ICloudStore, dataStack: DataStack) {} + public func iCloudStoreDidAddAccount(storage storage: ICloudStore, dataStack: DataStack) {} + + public func iCloudStoreWillRemoveAccount(storage storage: ICloudStore, dataStack: DataStack) {} + public func iCloudStoreDidRemoveAccount(storage storage: ICloudStore, dataStack: DataStack) {} + + public func iCloudStoreWillRemoveContent(storage storage: ICloudStore, dataStack: DataStack) {} + public func iCloudStoreDidRemoveContent(storage storage: ICloudStore, dataStack: DataStack) {} +} diff --git a/Sources/Setup/StorageInterfaces/InMemoryStore.swift b/Sources/Setup/StorageInterfaces/InMemoryStore.swift index 30d1a82..ae9a576 100644 --- a/Sources/Setup/StorageInterfaces/InMemoryStore.swift +++ b/Sources/Setup/StorageInterfaces/InMemoryStore.swift @@ -70,4 +70,25 @@ public final class InMemoryStore: StorageInterface, DefaultInitializableStore { The options dictionary for the `NSPersistentStore`. For `InMemoryStore`s, this is always set to `nil`. */ public let storeOptions: [String: AnyObject]? = nil + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didAddToDataStack(dataStack: DataStack) { + + self.dataStack = dataStack + } + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didRemoveFromDataStack(dataStack: DataStack) { + + self.dataStack = nil + } + + + // MARK: Private + + private weak var dataStack: DataStack? } diff --git a/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift b/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift index feae4c5..3d7b5af 100644 --- a/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift +++ b/Sources/Setup/StorageInterfaces/LegacySQLiteStore.swift @@ -88,6 +88,62 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore { } + // MARK: StorageInterface + + /** + The string identifier for the `NSPersistentStore`'s `type` property. For `SQLiteStore`s, this is always set to `NSSQLiteStoreType`. + */ + public static let storeType = NSSQLiteStoreType + + /** + The options dictionary for the specified `LocalStorageOptions` + */ + public func storeOptionsForOptions(options: LocalStorageOptions) -> [String: AnyObject]? { + + if options == .None { + + return self.storeOptions + } + + var storeOptions = self.storeOptions ?? [:] + if options.contains(.AllowSynchronousLightweightMigration) { + + storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true + storeOptions[NSInferMappingModelAutomaticallyOption] = true + } + return storeOptions + } + + /** + The configuration name in the model file + */ + public let configuration: String? + + /** + The options dictionary for the `NSPersistentStore`. For `SQLiteStore`s, this is always set to + ``` + [NSSQLitePragmasOption: ["journal_mode": "WAL"]] + ``` + */ + public let storeOptions: [String: AnyObject]? = [NSSQLitePragmasOption: ["journal_mode": "WAL"]] + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didAddToDataStack(dataStack: DataStack) { + + self.dataStack = dataStack + } + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didRemoveFromDataStack(dataStack: DataStack) { + + self.dataStack = nil + } + + // MAKR: LocalStorage /** @@ -105,27 +161,6 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore { */ public var localStorageOptions: LocalStorageOptions - - // MARK: StorageInterface - - /** - The string identifier for the `NSPersistentStore`'s `type` property. For `SQLiteStore`s, this is always set to `NSSQLiteStoreType`. - */ - public static let storeType = NSSQLiteStoreType - - /** - The configuration name in the model file - */ - public let configuration: String? - - /** - The options dictionary for the `NSPersistentStore`. For `SQLiteStore`s, this is always set to - ``` - [NSSQLitePragmasOption: ["journal_mode": "WAL"]] - ``` - */ - public let storeOptions: [String: AnyObject]? = [NSSQLitePragmasOption: ["journal_mode": "WAL"]] - /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ @@ -168,4 +203,9 @@ public final class LegacySQLiteStore: LocalStorage, DefaultInitializableStore { internal static let defaultFileURL = LegacySQLiteStore.defaultRootDirectory .URLByAppendingPathComponent(DataStack.applicationName, isDirectory: false) .URLByAppendingPathExtension("sqlite") + + + // MARK: Private + + private weak var dataStack: DataStack? } diff --git a/Sources/Setup/StorageInterfaces/SQLiteStore.swift b/Sources/Setup/StorageInterfaces/SQLiteStore.swift index 294e01c..11f9eef 100644 --- a/Sources/Setup/StorageInterfaces/SQLiteStore.swift +++ b/Sources/Setup/StorageInterfaces/SQLiteStore.swift @@ -86,24 +86,6 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore { } - // MAKR: LocalStorage - - /** - The `NSURL` that points to the SQLite file - */ - public let fileURL: NSURL - - /** - The `NSBundle`s from which to search mapping models for migrations - */ - public let mappingModelBundles: [NSBundle] - - /** - Options that tell the `DataStack` how to setup the persistent store - */ - public var localStorageOptions: LocalStorageOptions - - // MARK: StorageInterface /** @@ -123,13 +105,66 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore { ``` */ public let storeOptions: [String: AnyObject]? = [NSSQLitePragmasOption: ["journal_mode": "WAL"]] - + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didAddToDataStack(dataStack: DataStack) { + + self.dataStack = dataStack + } + + /** + Do not call directly. Used by the `DataStack` internally. + */ + public func didRemoveFromDataStack(dataStack: DataStack) { + + self.dataStack = nil + } + + + // MAKR: LocalStorage + + /** + The `NSURL` that points to the SQLite file + */ + public let fileURL: NSURL + + /** + The `NSBundle`s from which to search mapping models for migrations + */ + public let mappingModelBundles: [NSBundle] + + /** + Options that tell the `DataStack` how to setup the persistent store + */ + public var localStorageOptions: LocalStorageOptions + + /** + The options dictionary for the specified `LocalStorageOptions` + */ + public func storeOptionsForOptions(options: LocalStorageOptions) -> [String: AnyObject]? { + + if options == .None { + + return self.storeOptions + } + + var storeOptions = self.storeOptions ?? [:] + if options.contains(.AllowSynchronousLightweightMigration) { + + storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true + storeOptions[NSInferMappingModelAutomaticallyOption] = true + } + return storeOptions + } + /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ public func eraseStorageAndWait(soureModel soureModel: NSManagedObjectModel) throws { - // TODO: check if attached to persistent store + // TODO: check if attached to persistent store let fileURL = self.fileURL try autoreleasepool { @@ -147,7 +182,7 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore { } - // MARK: Private + // MARK: Internal internal static let defaultRootDirectory: NSURL = { @@ -173,4 +208,9 @@ public final class SQLiteStore: LocalStorage, DefaultInitializableStore { isDirectory: false ) .URLByAppendingPathExtension("sqlite") + + + // MARK: Private + + private weak var dataStack: DataStack? } diff --git a/Sources/Setup/StorageInterfaces/StorageInterface.swift b/Sources/Setup/StorageInterfaces/StorageInterface.swift index b6dd6dc..b3d9b76 100644 --- a/Sources/Setup/StorageInterfaces/StorageInterface.swift +++ b/Sources/Setup/StorageInterfaces/StorageInterface.swift @@ -47,6 +47,19 @@ public protocol StorageInterface: class { The options dictionary for the `NSPersistentStore` */ var storeOptions: [String: AnyObject]? { get } + + + // MARK: Internal (Do not call these directly) + + /** + Do not call directly. Used by the `DataStack` internally. + */ + func didAddToDataStack(dataStack: DataStack) + + /** + Do not call directly. Used by the `DataStack` internally. + */ + func didRemoveFromDataStack(dataStack: DataStack) } @@ -92,6 +105,7 @@ public struct LocalStorageOptions: OptionSetType, NilLiteralConvertible { public static let AllowSynchronousLightweightMigration = LocalStorageOptions(rawValue: 1 << 2) + // MARK: OptionSetType public init(rawValue: Int) { @@ -136,15 +150,17 @@ public protocol LocalStorage: StorageInterface { */ var localStorageOptions: LocalStorageOptions { get } + /** + The options dictionary for the specified `LocalStorageOptions` + */ + func storeOptionsForOptions(options: LocalStorageOptions) -> [String: AnyObject]? + /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ func eraseStorageAndWait(soureModel soureModel: NSManagedObjectModel) throws } - -// MARK: Internal - internal extension LocalStorage { internal func matchesPersistentStore(persistentStore: NSPersistentStore) -> Bool { @@ -154,3 +170,105 @@ internal extension LocalStorage { && persistentStore.URL == self.fileURL } } + + +// MARK: - CloudStorageOptions + +/** + The `CloudStorageOptions` provides settings that tells the `DataStack` how to setup the persistent store for `LocalStorage` implementers. + */ +public struct CloudStorageOptions: OptionSetType, NilLiteralConvertible { + + /** + Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch + */ + public static let None = CloudStorageOptions(rawValue: 0) + + /** + Tells the `DataStack` to delete and recreate the local store from the cloud store on model mismatch, otherwise exceptions will be thrown on failure instead + */ + public static let RecreateLocalStoreOnModelMismatch = CloudStorageOptions(rawValue: 1 << 0) + + /** + Tells the `DataStack` to allow lightweight migration for the store when added synchronously + */ + public static let AllowSynchronousLightweightMigration = CloudStorageOptions(rawValue: 1 << 2) + + + // MARK: OptionSetType + + public init(rawValue: Int) { + + self.rawValue = rawValue + } + + + // MARK: RawRepresentable + + public let rawValue: Int + + + // MARK: NilLiteralConvertible + + public init(nilLiteral: ()) { + + self.rawValue = 0 + } +} + + +// MARK: - CloudStorage + +/** + The `CloudStorage` represents `StorageInterface`s that are synchronized from a cloud-based store. + */ +public protocol CloudStorage: StorageInterface { + + /** + The `NSURL` that points to the store file + */ + var cacheFileURL: NSURL { get } + + /** + Options that tell the `DataStack` how to setup the persistent store + */ + var cloudStorageOptions: CloudStorageOptions { get } + + /** + The options dictionary for the specified `CloudStorageOptions` + */ + func storeOptionsForOptions(options: CloudStorageOptions) -> [String: AnyObject]? + + /** + Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (Cloud stores for example, can set the NSPersistentStoreRemoveUbiquitousMetadataOption option before deleting) + */ + func eraseStorageAndWait(soureModel soureModel: NSManagedObjectModel) throws +} + +internal extension CloudStorage { + + internal func matchesPersistentStore(persistentStore: NSPersistentStore) -> Bool { + + guard persistentStore.type == self.dynamicType.storeType + && persistentStore.configurationName == (self.configuration ?? Into.defaultConfigurationName) else { + + return false + } + guard persistentStore.URL == self.cacheFileURL else { + + return false + } + guard let persistentStoreOptions = persistentStore.options, + let storeOptions = self.storeOptions else { + + return persistentStore.options == nil && self.storeOptions == nil + } + return storeOptions.reduce(true) { (isMatch, tuple) in + + let (key, value) = tuple + let obj1 = persistentStoreOptions[key] as? NSObject + let obj2 = value as? NSObject + return isMatch && (obj1 == obj2) + } + } +}