diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 54aa22a..3c6b808 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ B56007161B4018AB00A9A8F9 /* MigrationChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56007151B4018AB00A9A8F9 /* MigrationChain.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 */; }; + B59D5C221B5BA34B00453479 /* NSFileManager+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59D5C211B5BA34B00453479 /* NSFileManager+Setup.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 */; }; @@ -114,6 +115,7 @@ B56007151B4018AB00A9A8F9 /* MigrationChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationChain.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 = ""; }; + B59D5C211B5BA34B00453479 /* NSFileManager+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFileManager+Setup.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 = ""; }; @@ -401,6 +403,7 @@ B5E84F2A1AFF849C0064E85B /* AssociatedObjects.swift */, B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */, B5FAD6AB1B51285300714891 /* MigrationManager.swift */, + B59D5C211B5BA34B00453479 /* NSFileManager+Setup.swift */, B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */, B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */, B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */, @@ -577,6 +580,7 @@ B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */, B5E84EFB1AFF846E0064E85B /* SaveResult.swift in Sources */, B5E84F0F1AFF847B0064E85B /* From.swift in Sources */, + B59D5C221B5BA34B00453479 /* NSFileManager+Setup.swift in Sources */, B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */, B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */, B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */, diff --git a/CoreStore/Internal/NSFileManager+Setup.swift b/CoreStore/Internal/NSFileManager+Setup.swift new file mode 100644 index 0000000..e5a1181 --- /dev/null +++ b/CoreStore/Internal/NSFileManager+Setup.swift @@ -0,0 +1,38 @@ +// +// NSFileManager+Setup.swift +// CoreStore +// +// Created by John Rommel Estropia on 2015/07/19. +// Copyright © 2015 John Rommel Estropia. All rights reserved. +// + +import Foundation + + +// MARK: - NSFileManager + +internal extension NSFileManager { + + // MARK: Internal + + internal func removeSQLiteStoreAtURL(fileURL: NSURL) { + + do { + + try self.removeItemAtURL(fileURL) + } + catch _ { } + + do { + + try self.removeItemAtPath(fileURL.path!.stringByAppendingString("-shm")) + } + catch _ { } + + do { + + try self.removeItemAtPath(fileURL.path!.stringByAppendingString("-wal")) + } + catch _ { } + } +} \ No newline at end of file diff --git a/CoreStore/Migrating/DataStack+Migration.swift b/CoreStore/Migrating/DataStack+Migration.swift index cfa0145..1da675b 100644 --- a/CoreStore/Migrating/DataStack+Migration.swift +++ b/CoreStore/Migrating/DataStack+Migration.swift @@ -32,16 +32,58 @@ import GCDKit public extension DataStack { + /** + Asynchronously adds an in-memory store to the stack. + + - parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`. + - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result. + */ + public func addInMemoryStore(configuration configuration: String? = nil, completion: (PersistentStoreResult) -> Void) { + + self.coordinator.performBlock { + + do { + + let store = try self.coordinator.addPersistentStoreWithType( + NSInMemoryStoreType, + configuration: configuration, + URL: nil, + options: nil + ) + self.updateMetadataForPersistentStore(store) + + GCDQueue.Main.async { + + completion(PersistentStoreResult(store)) + } + } + catch { + + let storeError = error as NSError + CoreStore.handleError( + storeError, + "Failed to add in-memory \(typeName(NSPersistentStore)) to the stack." + ) + + GCDQueue.Main.async { + + completion(PersistentStoreResult(storeError)) + } + } + } + } + /** Asynchronously adds to the stack an SQLite store from the given SQLite file name. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.) - parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory. A new SQLite file will be created if it does not exist. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them. - 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 `fileName` explicitly for each of them. - parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`. + - parameter resetStoreOnMigrationFailure: Set to true to delete the store on migration failure; or set to false to report failure instead. Typically should only be set to true when debugging, or if the persistent store can be recreated easily. If not specified, defaults to false. - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` 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. - returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required */ - public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (PersistentStoreResult) -> Void) throws -> NSProgress? { + public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, resetStoreOnMigrationFailure: Bool = false, completion: (PersistentStoreResult) -> Void) throws -> NSProgress? { return try self.addSQLiteStore( fileURL: applicationSupportDirectory.URLByAppendingPathComponent( @@ -50,6 +92,7 @@ public extension DataStack { ), configuration: configuration, mappingModelBundles: mappingModelBundles, + resetStoreOnMigrationFailure: resetStoreOnMigrationFailure, completion: completion ) } @@ -60,10 +103,11 @@ public extension DataStack { - parameter fileURL: the local file URL for the SQLite persistent store. A new SQLite file will be created if it does not exist. If not specified, defaults to a file URL pointing to a ".sqlite" file in the "Application Support" directory. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them. - 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 `fileURL` explicitly for each of them. - parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`. + - parameter resetStoreOnMigrationFailure: Set to true to delete the store on migration failure; or set to false to report failure instead. Typically should only be set to true when debugging, or if the persistent store can be recreated easily. If not specified, defaults to false. - parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` 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. - returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required */ - public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = NSBundle.allBundles(), completion: (PersistentStoreResult) -> Void) throws -> NSProgress? { + public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = NSBundle.allBundles(), resetStoreOnMigrationFailure: Bool = false, completion: (PersistentStoreResult) -> Void) throws -> NSProgress? { CoreStore.assert( fileURL.fileURL, @@ -94,9 +138,10 @@ public extension DataStack { throw error } + let fileManager = NSFileManager.defaultManager() do { - try NSFileManager.defaultManager().createDirectoryAtURL( + try fileManager.createDirectoryAtURL( fileURL.URLByDeletingLastPathComponent!, withIntermediateDirectories: true, attributes: nil @@ -120,6 +165,30 @@ public extension DataStack { if case .Failure(let error) = result { + if resetStoreOnMigrationFailure && error.isCoreDataMigrationError { + + fileManager.removeSQLiteStoreAtURL(fileURL) + do { + + let store = try self.addSQLiteStoreAndWait( + fileURL: fileURL, + configuration: configuration, + automigrating: false, + resetStoreOnMigrationFailure: false + ) + + GCDQueue.Main.async { + + completion(PersistentStoreResult(store)) + } + } + catch { + + completion(PersistentStoreResult(error as NSError)) + } + return + } + completion(PersistentStoreResult(error)) return } diff --git a/CoreStore/NSError+CoreStore.swift b/CoreStore/NSError+CoreStore.swift index 6c1ae13..df04e3b 100644 --- a/CoreStore/NSError+CoreStore.swift +++ b/CoreStore/NSError+CoreStore.swift @@ -86,4 +86,13 @@ public extension NSError { code: coreStoreErrorCode.rawValue, userInfo: userInfo) } + + internal var isCoreDataMigrationError: Bool { + + let code = self.code + return (code == NSPersistentStoreIncompatibleVersionHashError + || code == NSMigrationMissingSourceModelError + || code == NSMigrationError) + && self.domain == NSCocoaErrorDomain + } } diff --git a/CoreStore/Setting Up/CoreStore+Setup.swift b/CoreStore/Setting Up/CoreStore+Setup.swift index 20fccda..499c715 100644 --- a/CoreStore/Setting Up/CoreStore+Setup.swift +++ b/CoreStore/Setting Up/CoreStore+Setup.swift @@ -38,9 +38,9 @@ public extension CoreStore { - parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`. - returns: the `NSPersistentStore` added to the stack. */ - public static func addInMemoryStore(configuration configuration: String? = nil) throws -> NSPersistentStore { + public static func addInMemoryStoreAndWait(configuration configuration: String? = nil) throws -> NSPersistentStore { - return try self.defaultStack.addInMemoryStore(configuration: configuration) + return try self.defaultStack.addInMemoryStoreAndWait(configuration: configuration) } /** diff --git a/CoreStore/Setting Up/DataStack.swift b/CoreStore/Setting Up/DataStack.swift index 47ffd78..2cde3ab 100644 --- a/CoreStore/Setting Up/DataStack.swift +++ b/CoreStore/Setting Up/DataStack.swift @@ -87,7 +87,7 @@ public final class DataStack { - parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`. - returns: the `NSPersistentStore` added to the stack. */ - public func addInMemoryStore(configuration configuration: String? = nil) throws -> NSPersistentStore { + public func addInMemoryStoreAndWait(configuration configuration: String? = nil) throws -> NSPersistentStore { let coordinator = self.coordinator; @@ -224,31 +224,9 @@ public final class DataStack { } if let error = storeError - where ( - resetStoreOnMigrationFailure - && (error.code == NSPersistentStoreIncompatibleVersionHashError - || error.code == NSMigrationMissingSourceModelError - || error.code == NSMigrationError) - && error.domain == NSCocoaErrorDomain - ) { + where (resetStoreOnMigrationFailure && error.isCoreDataMigrationError) { - do { - - try fileManager.removeItemAtURL(fileURL) - } - catch _ { } - - do { - - try fileManager.removeItemAtPath(fileURL.path!.stringByAppendingString("-shm")) - } - catch _ { } - - do { - - try fileManager.removeItemAtPath(fileURL.path!.stringByAppendingString("-wal")) - } - catch _ { } + fileManager.removeSQLiteStoreAtURL(fileURL) var store: NSPersistentStore? coordinator.performBlockAndWait { diff --git a/CoreStoreDemo/CoreStoreDemo/Info.plist b/CoreStoreDemo/CoreStoreDemo/Info.plist index 4799161..614a3ac 100644 --- a/CoreStoreDemo/CoreStoreDemo/Info.plist +++ b/CoreStoreDemo/CoreStoreDemo/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.0.1 CFBundleSignature ???? CFBundleVersion diff --git a/Libraries/GCDKit b/Libraries/GCDKit index 50c517c..154d4cc 160000 --- a/Libraries/GCDKit +++ b/Libraries/GCDKit @@ -1 +1 @@ -Subproject commit 50c517ccc8cdf3794956eef90f70a18dbd4dd0c4 +Subproject commit 154d4ccc646093387ca033f386ef3840ae082f3e diff --git a/README.md b/README.md index 9d6a268..7f886bc 100644 --- a/README.md +++ b/README.md @@ -174,27 +174,37 @@ This one-liner does the following: For most cases, this configuration is usable as it is. But for more hardcore settings, refer to this extensive example: ```swift -let dataStack = DataStack(modelName: "MyModel") // loads from the "MyModel.xcdatamodeld" file +let dataStack = DataStack( + modelName: "MyModel", // loads from the "MyModel.xcdatamodeld" file + migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"] // model versions for incremental migrations +) do { // creates an in-memory store with entities from the "Config1" configuration in the .xcdatamodeld file - let persistentStore = try dataStack.addInMemoryStore(configuration: "Config1") // persistentStore is an NSPersistentStore instance + let persistentStore = try dataStack.addInMemoryStoreAndWait(configuration: "Config1") // persistentStore is an NSPersistentStore instance print("Successfully created an in-memory store: \(persistentStore)" } -catch let error as NSError { - print("Failed creating an in-memory store with error: \(error.description)" +catch { + print("Failed creating an in-memory store with error: \(error as NSError)" } do { - try dataStack.addSQLiteStoreAndWait( + try dataStack.addSQLiteStore( fileURL: sqliteFileURL, // set the target file URL for the sqlite file configuration: "Config2", // use entities from the "Config2" configuration in the .xcdatamodeld file - automigrating: true, // automatically run lightweight migrations or entity policy migrations when needed - resetStoreOnMigrationFailure: true) - print("Successfully created an sqlite store: \(persistentStore)" + resetStoreOnMigrationFailure: true, + completion: { (result) -> Void in + switch result { + case .Success(let persistentStore): + print("Successfully added sqlite store: \(persistentStore)" + case .Failure(let error): + print("Failed adding sqlite store with error: \(error)" + } + } + ) } -catch let error as NSError { - print("Failed creating an sqlite store with error: \(error.description)" +catch { + print("Failed adding sqlite store with error: \(error as NSError)" } CoreStore.defaultStack = dataStack // pass the dataStack to CoreStore for easier access later on @@ -203,7 +213,7 @@ CoreStore.defaultStack = dataStack // pass the dataStack to CoreStore for easier (If you have never heard of "Configurations", you'll find them in your *.xcdatamodeld* file) xcode configurations screenshot -In our sample above, note that you don't need to do the `CoreStore.defaultStack = dataStack` line. You can just as well hold a reference to the `DataStack` like below and call all its instance methods directly: +In our sample code above, note that you don't need to do the `CoreStore.defaultStack = dataStack` line. You can just as well hold a reference to the `DataStack` like below and call all its instance methods directly: ```swift class MyViewController: UIViewController { let dataStack = DataStack(modelName: "MyModel") @@ -241,7 +251,28 @@ class MyViewController: UIViewController { ## Migrations -(README pending) +So far we have only seen `addSQLiteStoreAndWait(...)` used to initialize our persistent store. As the method name's "AndWait" suffix suggests, this method will block, even if a migration occurs. If migrations are expected, the asynchronous variant `addSQLiteStore(... completion:)` method is recommended: +```swift +do { + let progress: NSProgress = try dataStack.addSQLiteStore( + fileName: "MyStore.sqlite", + configuration: "Config2", + completion: { (result) -> Void in + switch result { + case .Success(let persistentStore): + print("Successfully added sqlite store: \(persistentStore)" + case .Failure(let error): + print("Failed adding sqlite store with error: \(error)" + } + } + ) +} +catch { + print("Failed adding sqlite store with error: \(error as NSError)" +} +``` +The `completion` block is called +PersistentStoreResult ### Incremental migrations (README pending)