diff --git a/.travis.yml b/.travis.yml index 1233073..0c4ba7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode8.3 +osx_image: xcode8.3.2 sudo: false git: submodules: false diff --git a/CoreStore.podspec b/CoreStore.podspec index 2b521c1..718a8dc 100644 --- a/CoreStore.podspec +++ b/CoreStore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "CoreStore" - s.version = "4.0.0-beta3" + s.version = "4.0.0-beta4" 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/CoreStoreTests/BaseTests/BaseTestCase.swift b/CoreStoreTests/BaseTests/BaseTestCase.swift index 5f79f40..a487b31 100644 --- a/CoreStoreTests/BaseTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTests/BaseTestCase.swift @@ -40,7 +40,7 @@ class BaseTestCase: XCTestCase { func prepareStack(configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) -> T) -> T { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) do { diff --git a/CoreStoreTests/BridgingTests.m b/CoreStoreTests/BridgingTests.m index fd248b9..aa81b49 100644 --- a/CoreStoreTests/BridgingTests.m +++ b/CoreStoreTests/BridgingTests.m @@ -163,7 +163,7 @@ - (void)test_ThatDataStacks_BridgeCorrectly { CSDataStack *dataStack = [[CSDataStack alloc] - initWithModelName:@"Model" + initWithXcodeModelName:@"Model" bundle:[NSBundle bundleForClass:[self class]] versionChain:nil]; XCTAssertNotNil(dataStack); @@ -201,7 +201,7 @@ [CSCoreStore setDefaultStack:[[CSDataStack alloc] - initWithModelName:@"Model" + initWithXcodeModelName:@"Model" bundle:[NSBundle bundleForClass:[self class]] versionChain:nil]]; [CSCoreStore diff --git a/CoreStoreTests/SetupTests.swift b/CoreStoreTests/SetupTests.swift index 8a87dde..87aef49 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -67,7 +67,7 @@ class SetupTests: BaseTestDataTestCase { let stack = self.expectLogger([.logWarning]) { DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)), migrationChain: migrationChain ) @@ -84,7 +84,7 @@ class SetupTests: BaseTestDataTestCase { dynamic func test_ThatInMemoryStores_SetupCorrectly() { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) do { @@ -139,7 +139,7 @@ class SetupTests: BaseTestDataTestCase { dynamic func test_ThatSQLiteStores_SetupCorrectly() { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) do { @@ -207,7 +207,7 @@ class SetupTests: BaseTestDataTestCase { do { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) try! stack.addStorageAndWait(sqliteStore) @@ -226,7 +226,7 @@ class SetupTests: BaseTestDataTestCase { let metadata = try createStore() let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) try sqliteStore.cs_eraseStorageAndWait( @@ -259,7 +259,7 @@ class SetupTests: BaseTestDataTestCase { dynamic func test_ThatLegacySQLiteStores_SetupCorrectly() { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) do { @@ -327,7 +327,7 @@ class SetupTests: BaseTestDataTestCase { do { let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) try! stack.addStorageAndWait(sqliteStore) @@ -346,7 +346,7 @@ class SetupTests: BaseTestDataTestCase { let metadata = try createStore() let stack = DataStack( - modelName: "Model", + xcodeModelName: "Model", bundle: Bundle(for: type(of: self)) ) try sqliteStore.cs_eraseStorageAndWait( diff --git a/README.md b/README.md index 5d54ed8..220d24f 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,19 @@ Unleashing the real power of Core Data with the elegance and safety of Swift

-* **Swift 3.0.1:** iOS 8+ / macOS 10.10+ / watchOS 2.0+ / tvOS 9.0+ +* **Swift 3.1:** iOS 8+ / macOS 10.10+ / watchOS 2.0+ / tvOS 9.0+ -Upgrading from CoreStore 2.x to 3.x? Check out the [new features](#features) and make sure to read the [Migration guide](#upgrading-from-2xx-to-3xx). +Upgrading from CoreStore 3.x to 4.x? Check out the [new features](#features) and make sure to read the [Migration guide](#upgrading-from-3xx-to-4xx). ## Why use CoreStore? -I was [MagicalRecord](https://github.com/magicalpanda/MagicalRecord)'s heavy user back then, but I took the promising opportunity to create CoreStore when Swift came around. Part of the inspiration is to address the trend of developers [avoiding](http://inessential.com/2010/02/26/on_switching_away_from_core_data) [Core Data](http://bsktapp.com/blog/why-is-realm-great-and-why-are-we-not-using-it/) [for](https://www.quora.com/Why-would-you-use-Realm-over-Core-Data) [perplexing](http://sebastiandobrincu.com/blog/5-reasons-why-you-should-choose-realm-over-coredata) [reasons](https://medium.com/the-way-north/ditching-core-data-865c1bb5564c#.a5h8ou6ri). +CoreStore is the answer to the [pain](http://inessential.com/2010/02/26/on_switching_away_from_core_data) [of](http://bsktapp.com/blog/why-is-realm-great-and-why-are-we-not-using-it/) [using](https://www.quora.com/Why-would-you-use-Realm-over-Core-Data) [Core](http://sebastiandobrincu.com/blog/5-reasons-why-you-should-choose-realm-over-coredata) [Data](https://medium.com/the-way-north/ditching-core-data-865c1bb5564c#.a5h8ou6ri). CoreStore was (and is) heavily shaped by real-world needs of developing data-dependent apps. It enforces safe and convenient Core Data usage while letting you take advantage of the industry's encouraged best practices. And with Core Data and Swift continuously being improved by Apple, CoreStore will just get better and better! ### Features +- **New in 4.0: Say goodbye to xcmappingmodeld files!** The new `CoreStoreObject` is THE replacement to `NSManagedObject`. `CoreStoreObject` subclasses can declare type-safe properties all in Swift code, no need to maintain separate resource files for the models. As bonus, these special properties support custom types, and can be used to create type-safe keypaths and queries. *(See [Type-safe `CoreStoreObject`s](#type-safe-corestore-objects))* +- **New in 4.0: Easier custom migrations.** Say goodbye to .xcmappingmodel files; CoreStore can now infer entity mappings when possible, while still allowing an easy way to write custom mappings. *(See [Migrations](#migrations))* - **Heavy support for multiple persistent stores per data stack.** CoreStore lets you manage separate stores in a single `DataStack`, just the way *.xcdatamodeld* configurations are designed to. CoreStore will also manage one stack by default, but you can create and manage as many as you need. *(See [Setting up](#setting-up))* - **Progressive migrations.** No need to think how to migrate from all previous model versions to your latest model. Just tell the `DataStack` the sequence of version strings (`MigrationChain`s) and CoreStore will automatically use progressive migrations when needed. *(See [Migrations](#migrations))* - **Plug-in your own logging framework.** Although a default logger is built-in, all logging, asserting, and error reporting can be funneled to `CoreStoreLogger` protocol implementations. *(See [Logging and error reporting](#logging-and-error-reporting))* @@ -41,7 +43,6 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep - **Full Documentation.** No magic here; all public classes, functions, properties, etc. have detailed *Apple Docs*. This *README* also introduces a lot of concepts and explains a lot of CoreStore's behavior. - **Informative (and pretty) logs.** All CoreStore and Core Data-related types now have very informative and pretty print outputs! *(See [Logging and error reporting](#logging-and-error-reporting))* - **Objective-C support!** Is your project transitioning from Objective-C to Swift but still can't quite fully convert some huge classes to Swift yet? CoreStore adjusts to the ever-increasing Swift adoption. While still written in pure Swift, all CoreStore types have their corresponding Objective-C-visible "bridging classes". *(See [Objective-C support](#objective-c-support))* -- **iCloud storage (beta) support.** CoreStore allows creation of iCloud persistent stores, as well as observing of iCloud-related events through the `ICloudStoreObserver`. *(See [iCloud storage](#icloud-storages))* - **More extensive Unit Tests.** Extending CoreStore is safe without having to worry about breaking old behavior. *Have ideas that may benefit other Core Data users? [Feature Request](https://github.com/JohnEstropia/CoreStore/issues)s are welcome!* @@ -51,6 +52,7 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep - [TL;DR (a.k.a. sample codes)](#tldr-aka-sample-codes) - [Architecture](#architecture) - CoreStore Tutorials (All of these have demos in the **CoreStoreDemo** app project!) + - [Type-safe `CoreStoreObject`s](#type-safe-corestore-objects) - [Setting up](#setting-up) - [In-memory store](#in-memory-store) - [Local store](#local-store) @@ -98,7 +100,7 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep Setting-up with progressive migration support: ```swift CoreStore.defaultStack = DataStack( - modelName: "MyStore", + xcodeModelName: "MyStore", migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"] ) ``` @@ -115,18 +117,19 @@ CoreStore.addStorage( Starting transactions: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let person = transaction.create(Into()) - person.name = "John Smith" - person.age = 42 - - transaction.commit { (result) -> Void in +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let person = transaction.create(Into()) + person.name = "John Smith" + person.age = 42 + }, + completion: { (result) -> Void in switch result { - case .success(let hasChanges): print("success!") - case .failure(let error): print(error) + case .success: print("success!") + case .failure(let error): print(error) } } -} +) ``` Fetching objects (simple): @@ -197,7 +200,7 @@ This one-liner does the following: For most cases, this configuration is enough 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 + xcodeModelName: "MyModel", // loads from the "MyModel.xcdatamodeld" file migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"] // model versions for progressive migrations ) let migrationProgress = dataStack.addStorage( @@ -225,7 +228,7 @@ CoreStore.defaultStack = dataStack // pass the dataStack to CoreStore for easier 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") // keep reference to the stack + let dataStack = DataStack(xcodeModelName: "MyModel") // keep reference to the stack override func viewDidLoad() { super.viewDidLoad() do { @@ -278,7 +281,7 @@ let migrationProgress = CoreStore.addStorage( SQLiteStore( fileName: "MyStore.sqlite", configuration: "Config2", // optional. Use entities from the "Config2" configuration in the .xcdatamodeld file - mappingModelBundles: [NSBundle.mainBundle()], // optional. The bundles that contain required .xcmappingmodel files, if any + mappingModelBundles: [Bundle.main], // optional. The bundles that contain required .xcmappingmodel files localStorageOptions: .recreateStoreOnModelMismatch // optional. Provides settings that tells the DataStack how to setup the persistent store ), completion: { /* ... */ } @@ -286,20 +289,16 @@ let migrationProgress = CoreStore.addStorage( ``` Refer to the *SQLiteStore.swift* source documentation for detailed explanations for each of the default values. -CoreStore can decide the default values for these properties, so `SQLiteStore`s can be initialized with default arguments: +CoreStore can decide the default values for these properties, so `SQLiteStore`s can be initialized with no arguments: ```swift try CoreStore.addStorageAndWait(SQLiteStore()) ``` -or -```swift -let migrationProgress = CoreStore.addStorage(SQLiteStore.self, completion: { /* ... */ }) -``` The file-related properties above are actually requirements of another protocol that `SQLiteStore` implements, the `LocalStorage` protocol: ```swift public protocol LocalStorage: StorageInterface { var fileURL: NSURL { get } - var mappingModelBundles: [NSBundle] { get } + var migrationMappingProviders: [SchemaMappingProvider] { get } var localStorageOptions: LocalStorageOptions { get } func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]? func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws @@ -369,7 +368,7 @@ The `ICloudStore` only keeps weak references of the registered observers. You ma ## Migrations -We have seen `addStorageAndWait(...)` used to initialize our persistent store. As the method name's "AndWait" suffix suggests though, this method blocks so it should not do long tasks such as store migrations. In fact CoreStore will only attempt a synchronous **lightweight** migration if you explicitly provide the `.allowSynchronousLightweightMigration` option: +We have seen `addStorageAndWait(...)` used to initialize our persistent store. As the method name's "~AndWait" suffix suggests though, this method blocks so it should not do long tasks such as store migrations. In fact CoreStore will only attempt a synchronous **lightweight** migration if you explicitly provide the `.allowSynchronousLightweightMigration` option: ```swift try dataStack.addStorageAndWait( SQLiteStore( @@ -482,45 +481,95 @@ Each `MigrationType` indicates the migration type for each step in the `Migratio To ensure deterministic state for objects in the read-only `NSManagedObjectContext`, CoreStore does not expose API's for updating and saving directly from the main context (or any other context for that matter.) Instead, you spawn *transactions* from `DataStack` instances: ```swift let dataStack = self.dataStack -dataStack.beginAsynchronous { (transaction) -> Void in - // make changes - transaction.commit() -} +dataStack.perform( + asynchronous: { (transaction) -> Void in + // make changes + }, + completion: { (result) -> Void in + // ... + } +) ``` or for the default stack, directly from `CoreStore`: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - // make changes - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + // make changes + }, + completion: { (result) -> Void in + // ... + } +) ``` -The `commit()` method saves the changes to the persistent store. If `commit()` is not called when the transaction block completes, all changes within the transaction is discarded. +Transaction blocks automatically save changes once the block completes. To cancel and rollback a transaction, throw a `CoreStoreError.userCancelled` from inside the closure by calling `try transaction.cancel()`: +```swift +dataStack.perform( + asynchronous: { (transaction) -> Void in + // ... + if shouldCancel { + try transaction.cancel() + } + // ... + }, + completion: { (result) -> Void in + if case .failure(.userCancelled) = result { + // ... cancelled + } + } +) +``` +Never use `try?` or `try!` on a `transaction.cancel()` call. Always use `try`. Using `try?` will swallow the cancellation and the transaction will proceed to save as normal. Using `try!` will crash the app as `transaction.cancel()` will *always* throw an error. -The examples above use `beginAsynchronous(...)`, but there are actually 3 types of transactions at your disposal: *asynchronous*, *synchronous*, and *unsafe*. +The examples above use `perform(asynchronous:...)`, but there are actually 3 types of transactions at your disposal: *asynchronous*, *synchronous*, and *unsafe*. ### Transaction types #### Asynchronous transactions -are spawned from `beginAsynchronous(...)`. This method returns immediately and executes its closure from a background serial queue: +are spawned from `perform(asynchronous:...)`. This method returns immediately and executes its closure from a background serial queue. The return value for the closure is declared as a generic type, so any value returned from the closure can be passed to the completion result: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - // make changes - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Bool in + // make changes + return transaction.hasChanges + }, + completion: { (result) -> Void in + switch result { + case .success(let hasChanges): print("success! Has changes? \(hasChanges)") + case .failure(let error): print(error) + } + } +) ``` -Transactions created from `beginAsynchronous(...)` are instances of `AsynchronousDataTransaction`. +The success and failure can also be declared as separate handlers: +```swift +CoreStore.perform( + asynchronous: { (transaction) -> Int in + // make changes + return transaction.delete(objects) + }, + success: { (numberOfDeletedObjects: Int) -> Void in + print("success! Deleted \(numberOfDeletedObjects) objects") + }, + failure: { (error) -> Void in + print(error) + } +) +``` +Transactions created from `perform(asynchronous:...)` are instances of `AsynchronousDataTransaction`. #### Synchronous transactions -are created from `beginSynchronous(...)`. While the syntax is similar to its asynchronous counterpart, `beginSynchronous(...)` waits for its transaction block to complete before returning: +are created from `perform(synchronous:...)`. While the syntax is similar to its asynchronous counterpart, `perform(synchronous:...)` waits for its transaction block to complete before returning: ```swift -CoreStore.beginSynchronous { (transaction) -> Void in - // make changes - _ = transaction.commit() -} +let hasChanges = CoreStore.perform( + synchronous: { (transaction) -> Bool in + // make changes + return transaction.hasChanges + } +) ``` `transaction` above is a `SynchronousDataTransaction` instance. -Since `beginSynchronous(...)` technically blocks two queues (the caller's queue and the transaction's background queue), it is considered less safe as it's more prone to deadlock. Take special care that the closure does not block on any other external queues. +Since `perform(synchronous:...)` technically blocks two queues (the caller's queue and the transaction's background queue), it is considered less safe as it's more prone to deadlock. Take special care that the closure does not block on any other external queues. #### Unsafe transactions are special in that they do not enclose updates within a closure: @@ -540,7 +589,7 @@ downloadAnotherJSONWithCompletion({ (json) -> Void in ``` This allows for non-contiguous updates. Do note that this flexibility comes with a price: you are now responsible for managing concurrency for the transaction. As uncle Ben said, "with great power comes great race conditions." -As the above example also shows, only unsafe transactions are allowed to call `commit()` multiple times; doing so with synchronous and asynchronous transactions will trigger an assert. +As the above example also shows, with unsafe transactions `commit()` can be called multiple times. You've seen how to create transactions, but we have yet to see how to make *creates*, *updates*, and *deletes*. The 3 types of transactions above are all subclasses of `BaseDataTransaction`, which implements the methods shown below. @@ -571,23 +620,27 @@ Note that if you do explicitly specify the configuration name, CoreStore will on After creating an object from the transaction, you can simply update its properties as normal: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let person = transaction.create(Into()) - person.name = "John Smith" - person.age = 30 - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let person = transaction.create(Into()) + person.name = "John Smith" + person.age = 30 + }, + completion: { _ in } +) ``` To update an existing object, fetch the object's instance from the transaction: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let person = transaction.fetchOne( - From(), - Where("name", isEqualTo: "Jane Smith") - ) - person.age = person.age + 1 - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let person = transaction.fetchOne( + From(), + Where("name", isEqualTo: "Jane Smith") + ) + person.age = person.age + 1 + }, + completion: { _ in } +) ``` *(For more about fetching, see [Fetching and querying](#fetching-and-querying))* @@ -595,27 +648,31 @@ CoreStore.beginAsynchronous { (transaction) -> Void in ```swift let jane: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - // WRONG: jane.age = jane.age + 1 - // RIGHT: - let jane = transaction.edit(jane)! // using the same variable name protects us from misusing the non-transaction instance - jane.age = jane.age + 1 - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + // WRONG: jane.age = jane.age + 1 + // RIGHT: + let jane = transaction.edit(jane)! // using the same variable name protects us from misusing the non-transaction instance + jane.age = jane.age + 1 + }, + completion: { _ in } +) ``` This is also true when updating an object's relationships. Make sure that the object assigned to the relationship is also created/fetched from the transaction: ```swift let jane: MyPersonEntity = // ... let john: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - // WRONG: jane.friends = [john] - // RIGHT: - let jane = transaction.edit(jane)! - let john = transaction.edit(john)! - jane.friends = NSSet(array: [john]) - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + // WRONG: jane.friends = [john] + // RIGHT: + let jane = transaction.edit(jane)! + let john = transaction.edit(john)! + jane.friends = NSSet(array: [john]) + }, + completion: { _ in } +) ``` ### Deleting objects @@ -624,31 +681,37 @@ Deleting an object is simpler because you can tell a transaction to delete an ob ```swift let john: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - transaction.delete(john) - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + transaction.delete(john) + }, + completion: { _ in } +) ``` or several objects at once: ```swift let john: MyPersonEntity = // ... let jane: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - transaction.delete(john, jane) - // transaction.delete([john, jane]) is also allowed - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + transaction.delete(john, jane) + // transaction.delete([john, jane]) is also allowed + }, + completion: { _ in } +) ``` If you do not have references yet to the objects to be deleted, transactions have a `deleteAll(...)` method you can pass a query to: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - transaction.deleteAll( - From(), - Where("age > 30") - ) - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + transaction.deleteAll( + From(), + Where("age > 30") + ) + }, + completion: { _ in } +) ``` ### Passing objects safely @@ -657,60 +720,73 @@ Always remember that the `DataStack` and individual transactions manage differen ```swift let jane: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - let jane = transaction.edit(jane)! - jane.age = jane.age + 1 - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let jane = transaction.edit(jane)! + jane.age = jane.age + 1 + }, + completion: { _ in } +) ``` But `CoreStore`, `DataStack` and `BaseDataTransaction` have a very flexible `fetchExisting(...)` method that you can pass instances back and forth with: ```swift let jane: MyPersonEntity = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - let jane = transaction.fetchExisting(jane)! // instance for transaction - jane.age = jane.age + 1 - transaction.commit { - let jane = CoreStore.fetchExisting(jane)! // instance for DataStack +CoreStore.perform( + asynchronous: { (transaction) -> MyPersonEntity in + let jane = transaction.fetchExisting(jane)! // instance for transaction + jane.age = jane.age + 1 + return jane + }, + success: { (transactionJane) in + let jane = CoreStore.fetchExisting(transactionJane)! // instance for DataStack print(jane.age) + }, + failure: { (error) in + // ... } -} +) ``` -`fetchExisting(...)` also works with multiple `NSManagedObject`s or with `NSManagedObjectID`s: +`fetchExisting(...)` also works with multiple `NSManagedObject`s, `CoreStoreObject`s, or with `NSManagedObjectID`s: ```swift var peopleIDs: [NSManagedObjectID] = // ... -CoreStore.beginAsynchronous { (transaction) -> Void in - let jane = transaction.fetchOne( - From(), - Where("name", isEqualTo: "Jane Smith") - ) - jane.friends = NSSet(array: transaction.fetchExisting(peopleIDs)!) - // ... -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let jane = transaction.fetchOne( + From(), + Where("name", isEqualTo: "Jane Smith") + ) + jane.friends = NSSet(array: transaction.fetchExisting(peopleIDs)!) + // ... + }, + completion: { _ in } +) ``` ## Importing data -Some times, if not most of the time, the data that we save to Core Data comes from external sources such as web servers or external files. Say you have a JSON dictionary, you may be extracting values as such: +Some times, if not most of the time, the data that we save to Core Data comes from external sources such as web servers or external files. If you have for example a JSON dictionary, you may be extracting values as such: ```swift -let json: [String: AnyObject] = // ... +let json: [String: Any] = // ... person.name = json["name"] as? NSString person.age = json["age"] as? NSNumber // ... ``` If you have many attributes, you don't want to keep repeating this mapping everytime you want to import data. CoreStore lets you write the data mapping code just once, and all you have to do is call `importObject(...)` or `importUniqueObject(...)` through `BaseDataTransaction` subclasses: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let json: [String: AnyObject] = // ... - try! transaction.importObject( - Into(), - source: json - ) - transaction.commit() -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let json: [String: Any] = // ... + try! transaction.importObject( + Into(), + source: json + ) + }, + completion: { _ in } +) ``` -To support data import for an entity, implement either `ImportableObject` or `ImportableUniqueObject` on the `NSManagedObject` subclass: +To support data import for an entity, implement either `ImportableObject` or `ImportableUniqueObject` on the `NSManagedObject` or `CoreStoreObject` subclass: - `ImportableObject`: Use this protocol if the object have no inherent uniqueness and new objects should always be added when calling `importObject(...)`. - `ImportableUniqueObject`: Use this protocol to specify a unique ID for an object that will be used to distinguish whether a new object should be created or if an existing object should be updated when calling `importUniqueObject(...)`. @@ -719,7 +795,7 @@ Both protocols require implementers to specify an `ImportSource` which can be se typealias ImportSource = NSDictionary ``` ```swift -typealias ImportSource = [String: AnyObject] +typealias ImportSource = [String: Any] ``` ```swift typealias ImportSource = NSData @@ -737,18 +813,21 @@ public protocol ImportableObject: class { ``` First, set `ImportSource` to the expected type of the data source: ```swift -typealias ImportSource = [String: AnyObject] +typealias ImportSource = [String: Any] ``` -This lets us call `importObject(_:source:)` with any `[String: AnyObject]` type as the argument to `source`: +This lets us call `importObject(_:source:)` with any `[String: Any]` type as the argument to `source`: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let json: [String: AnyObject] = // ... - try! transaction.importObject( - Into(), - source: json - ) - // ... -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let json: [String: Any] = // ... + try! transaction.importObject( + Into(), + source: json + ) + // ... + }, + completion: { _ in } +) ``` The actual extraction and assignment of values should be implemented in the `didInsert(from:in:)` method of the `ImportableObject` protocol: ```swift @@ -760,18 +839,21 @@ func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) t ``` Transactions also let you import multiple objects at once using the `importObjects(_:sourceArray:)` method: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let jsonArray: [[String: AnyObject]] = // ... - try! transaction.importObjects( - Into(), - sourceArray: jsonArray - ) - // ... -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let jsonArray: [[String: Any]] = // ... + try! transaction.importObjects( + Into(), + sourceArray: jsonArray // make sure this is of type Array + ) + // ... + }, + completion: { _ in } +) ``` Doing so tells the transaction to iterate through the array of import sources and calls `shouldInsert(from:in:)` on the `ImportableObject` to determine which instances should be created. You can do validations and return `false` from `shouldInsert(from:in:)` if you want to skip importing from a source and continue on with the other sources in the array. -If on the other hand, your validation in one of the sources failed in such a manner that all other sources should also be cancelled, you can `throw` from within `didInsert(from:in:)`: +If on the other hand, your validation in one of the sources failed in such a manner that all other sources should also be rolled back and cancelled, you can `throw` from within `didInsert(from:in:)`: ```swift func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws { self.name = source["name"] as? NSString @@ -784,21 +866,25 @@ func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) t ``` Doing so can let you abandon an invalid transaction immediately: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let jsonArray: [[String: AnyObject]] = // ... - do { +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let jsonArray: [[String: Any]] = // ... + try transaction.importObjects( Into(), sourceArray: jsonArray ) - } - catch { - return // Woops, don't save - } - transaction.commit { + }, + success: { // ... + }, + failure: { (error) in + switch error { + case Errors.InvalidNameError: print("Invalid name") + // ... + } } -} +) ``` #### `ImportableUniqueObject` @@ -806,7 +892,7 @@ Typically, we don't just keep creating objects every time we import data. Usuall ```swift public protocol ImportableUniqueObject: ImportableObject { typealias ImportSource - typealias UniqueIDType: NSObject + typealias UniqueIDType: ImportableAttributeType static var uniqueIDKeyPath: String { get } var uniqueIDValue: UniqueIDType { get set } @@ -821,40 +907,46 @@ public protocol ImportableUniqueObject: ImportableObject { Notice that it has the same insert methods as `ImportableObject`, with additional methods for updates and for specifying the unique ID: ```swift class var uniqueIDKeyPath: String { - return "personID" + return #keyPath(MyPersonEntity.personID) } -var uniqueIDValue: NSNumber { +var uniqueIDValue: Int { get { return self.personID } set { self.personID = newValue } } -class func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> NSNumber? { - return source["id"] as? NSNumber +class func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> Int? { + return source["id"] as? Int } ``` For `ImportableUniqueObject`, the extraction and assignment of values should be implemented from the `update(from:in:)` method. The `didInsert(from:in:)` by default calls `update(from:in:)`, but you can separate the implementation for inserts and updates if needed. You can then create/update an object by calling a transaction's `importUniqueObject(...)` method: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let json: [String: AnyObject] = // ... - try! transaction.importUniqueObject( - Into(), - source: json - ) - // ... -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let json: [String: Any] = // ... + try! transaction.importUniqueObject( + Into(), + source: json + ) + // ... + }, + completion: { _ in } +) ``` or multiple objects at once with the `importUniqueObjects(...)` method: ```swift -CoreStore.beginAsynchronous { (transaction) -> Void in - let jsonArray: [[String: AnyObject]] = // ... - try! transaction.importUniqueObjects( - Into(), - sourceArray: jsonArray - ) - // ... -} +CoreStore.perform( + asynchronous: { (transaction) -> Void in + let jsonArray: [[String: AnyObject]] = // ... + try! transaction.importUniqueObjects( + Into(), + sourceArray: jsonArray + ) + // ... + }, + completion: { _ in } +) ``` As with `ImportableObject`, you can control wether to skip importing an object by implementing `shouldInsert(from:in:)` and `shouldUpdate(from:in:)`, or to cancel all objects by `throw`ing an error from the `uniqueID(from:in:)`, `didInsert(from:in:)` or `update(from:in:)` methods. @@ -863,11 +955,11 @@ As with `ImportableObject`, you can control wether to skip importing an object b ## Fetching and Querying Before we dive in, be aware that CoreStore distinguishes between *fetching* and *querying*: - A *fetch* executes searches from a specific *transaction* or *data stack*. This means fetches can include pending objects (i.e. before a transaction calls on `commit()`.) Use fetches when: - - results need to be `NSManagedObject` instances + - results need to be `NSManagedObject` or `CoreStoreObject` instances - unsaved objects should be included in the search (though fetches can be configured to exclude unsaved ones) - A *query* pulls data straight from the persistent store. This means faster searches when computing aggregates such as *count*, *min*, *max*, etc. Use queries when: - you need to compute aggregate functions (see below for a list of supported functions) - - results can be raw values like `NSString`s, `NSNumber`s, `Int`s, `NSDate`s, an `NSDictionary` of key-values, etc. + - results can be raw values like `NSString`s, `NSNumber`s, `Int`s, `NSDate`s, an `NSDictionary` of key-values, or any type that conform to `QueryableAttributeType`. (See *QueryableAttributeType.swift* for a list of built-in types) - only values for specified attribute keys need to be included in the results - unsaved objects should be ignored @@ -875,7 +967,6 @@ Before we dive in, be aware that CoreStore distinguishes between *fetching* and The search conditions for fetches and queries are specified using *clauses*. All fetches and queries require a `From` clause that indicates the target entity type: ```swift let people = CoreStore.fetchAll(From()) -// CoreStore.fetchAll(From()) works as well ``` `people` in the example above will be of type `[MyPersonEntity]`. The `From()` clause indicates a fetch to all persistent stores that `MyPersonEntity` belong to. @@ -995,22 +1086,7 @@ let johnsAge = CoreStore.queryValue( Where("name == %@", "John Smith") ) ``` -The example above queries the "age" property for the first object that matches the `Where` condition. `johnsAge` will be bound to type `Int?`, as indicated by the `Select` generic type. For `queryValue(...)`, the following are allowed as the return type (and therefore as the generic type for `Select`): -- `Bool` -- `Int8` -- `Int16` -- `Int32` -- `Int64` -- `Double` -- `Float` -- `String` -- `NSNumber` -- `NSString` -- `NSDecimalNumber` -- `NSDate` -- `NSData` -- `NSManagedObjectID` -- `NSString` +The example above queries the "age" property for the first object that matches the `Where` condition. `johnsAge` will be bound to type `Int?`, as indicated by the `Select` generic type. For `queryValue(...)`, types that conform to `QueryableAttributeType` are allowed as the return type (and therefore as the generic type for `Select`). For `queryAttributes(...)`, only `NSDictionary` is valid for `Select`, thus you are allowed to omit the generic type: ```swift @@ -1156,8 +1232,8 @@ These are all implemented with `CustomDebugStringConvertible.debugDescription`, ## Observing changes and notifications (unavailable on macOS) CoreStore provides type-safe wrappers for observing managed objects: -- `ObjectMonitor`: use to monitor changes to a single `NSManagedObject` instance (instead of Key-Value Observing) -- `ListMonitor`: use to monitor changes to a list of `NSManagedObject` instances (instead of `NSFetchedResultsController`) +- `ObjectMonitor`: use to monitor changes to a single `NSManagedObject` or `CoreStoreObject` instance (instead of Key-Value Observing) +- `ListMonitor`: use to monitor changes to a list of `NSManagedObject` or `CoreStoreObject` instances (instead of `NSFetchedResultsController`) ### Observe a single object @@ -1193,11 +1269,11 @@ While `ObjectMonitor` exposes `removeObserver(...)` as well, it only stores `wea To observe a list of objects, implement one of the `ListObserver` protocols and specify the `EntityType`: ```swift class MyViewController: UIViewController, ListObserver { - func listMonitorWillChange(monitor: ListMonitor) { + func listMonitorDidChange(monitor: ListMonitor) { // ... } - func listMonitorDidChange(monitor: ListMonitor) { + func listMonitorDidRefetch(monitor: ListMonitor) { // ... } } @@ -1205,22 +1281,24 @@ class MyViewController: UIViewController, ListObserver { Including `ListObserver`, there are 3 observer protocols you can implement depending on how detailed you need to handle a change notification: - `ListObserver`: lets you handle these callback methods: ```swift - func listMonitorWillChange(monitor: ListMonitor) - func listMonitorDidChange(monitor: ListMonitor) - func listMonitorWillRefetch(monitor: ListMonitor) - func listMonitorDidRefetch(monitor: ListMonitor) + func listMonitorWillChange(_ monitor: ListMonitor) + func listMonitorDidChange(_ monitor: ListMonitor) + func listMonitorWillRefetch(_ monitor: ListMonitor) + func listMonitorDidRefetch(_ monitor: ListMonitor) ``` +`listMonitorDidChange(_:)` and `listMonitorDidRefetch(_:)` implementations are both required. `listMonitorDidChange(_:)` is called whenever the `ListMonitor`'s count, order, or filtered objects change. `listMonitorDidRefetch(_:)` is called when the `ListMonitor.refetch()` was executed or if the internal persistent store was changed. + - `ListObjectObserver`: in addition to `ListObserver` methods, also lets you handle object inserts, updates, and deletes: ```swift - func listMonitor(monitor: ListMonitor, didInsertObject object: MyPersonEntity, toIndexPath indexPath: NSIndexPath) - func listMonitor(monitor: ListMonitor, didDeleteObject object: MyPersonEntity, fromIndexPath indexPath: NSIndexPath) - func listMonitor(monitor: ListMonitor, didUpdateObject object: MyPersonEntity, atIndexPath indexPath: NSIndexPath) - func listMonitor(monitor: ListMonitor, didMoveObject object: MyPersonEntity, fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) + func listMonitor(_ monitor: ListMonitor, didInsertObject object: MyPersonEntity, toIndexPath indexPath: NSIndexPath) + func listMonitor(_ monitor: ListMonitor, didDeleteObject object: MyPersonEntity, fromIndexPath indexPath: NSIndexPath) + func listMonitor(_ monitor: ListMonitor, didUpdateObject object: MyPersonEntity, atIndexPath indexPath: NSIndexPath) + func listMonitor(_ monitor: ListMonitor, didMoveObject object: MyPersonEntity, fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) ``` - `ListSectionObserver`: in addition to `ListObjectObserver` methods, also lets you handle section inserts and deletes: ```swift - func listMonitor(monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) - func listMonitor(monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) + func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) + func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) ``` We then need to create a `ListMonitor` instance and register our `ListObserver` as an observer: @@ -1270,9 +1348,8 @@ self.monitor = CoreStore.monitorSectionedList( ``` This is useful when implementing a `UITableViewDelegate`'s section header: ```swift -func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { +func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let sectionInfo = self.monitor.sectionInfoAtIndex(section) - // sectionInfo is an NSFetchedResultsSectionInfo instance return sectionInfo.name } ``` @@ -1303,22 +1380,24 @@ NSError *error
-CoreStore.beginAsynchronous { (transaction) in
-    // ...
-    transaction.commit { (result) in
+CoreStore.perform(
+    asynchronous: { (transaction) in
+        // ...
+    },
+    completion: { (result) in
         switch result {
-        case .success(let hasChanges): print(hasChanges)
+        case .success: print("Done")
         case .failure(let error): print(error)
         }
     }
-}
+)
 
 [CSCoreStore beginAsynchronous:^(CSAsynchronousDataTransaction *transaction) {
     // ...
     [transaction commitWithCompletion:^(CSSaveResult *result) {
         if (result.isSuccess) {
-            NSLog(@"hasChanges: %d", result.hasChanges);
+            NSLog(@"Done");
         }
         else if (result.isFailure) {
             NSLog(@"error: %@", result.error);
@@ -1329,26 +1408,7 @@ CoreStore.beginAsynchronous { (transaction) in
 
 
 
-All of these `CS`-prefixed bridging classes have very similar usage to the existing CoreStore APIs, and ironically *none of them are written in Objective-C*. The secret is all in *CoreStoreBridge.swift*, where we see the signature of these bridging classes, the `CoreStoreObjectiveCType` protocol:
-```swift
-public protocol CoreStoreObjectiveCType: class, AnyObject {
-    associatedtype SwiftType
-    var bridgeToSwift: SwiftType { get }
-    init(_ swiftValue: SwiftType)
-}
-```
-Notice that these bridging classes all hold a reference to their corresponding `SwiftType`.
-
-Conversely, CoreStore original types implement the `CoreStoreSwiftType` protocol:
-```swift
-public protocol CoreStoreSwiftType {
-    associatedtype ObjectiveCType
-    var bridgeToObjectiveC: ObjectiveCType { get }
-}
-```
-These two protocols let CoreStore types free to bridge instances between Objective-C and Swift.
-
-This is very different to the common approach where apps and libraries write Objective-C APIs just to support both Objective-C and Swift. The advantage with CoreStore's approach is that your Swift codebase can already use the purely-Swift API without further changes in the future, but your "hybrid" codebase can still bridge instances back and forth from Objective-C to Swift.
+All of these `CS`-prefixed bridging classes have very similar usage to the existing CoreStore APIs, and ironically *none of them are written in Objective-C*. This is very different to the common approach where apps and libraries write Objective-C APIs just to support both Objective-C and Swift. The advantage with CoreStore's approach is that your Swift codebase can already use the purely-Swift API without further changes in the future, but your "hybrid" codebase can still bridge instances back and forth from Objective-C to Swift.
 
 For example, you may have a new, modern Swift class that holds a `ListMonitor`:
 ```swift
@@ -1408,17 +1468,10 @@ NSArray *objects =
 To use these syntax sugars, include *CoreStoreBridge.h* in your Objective-C source files.
 
 
-# Roadmap
-- Built-in "singleton objects" support
-- Built-in "readonly" stores
-- CoreSpotlight auto-indexing (experimenting, still some roadblocks ahead)
-- Synching
-
-
 # Installation
 - Requires:
     - iOS 8 SDK and above
-    - Swift 3.0.1 (Xcode 8.2)
+    - Swift 3.1 (Xcode 8.3.2+)
 - Dependencies:
     - *None*
 - Other notes:
@@ -1433,7 +1486,7 @@ This installs CoreStore as a framework. Declare `import CoreStore` in your swift
 ### Install with Carthage
 In your `Cartfile`, add
 ```
-github "JohnEstropia/CoreStore" >= 3.0.0
+github "JohnEstropia/CoreStore" >= 4.0.0
 ```
 and run 
 ```
@@ -1458,50 +1511,26 @@ Add all *.swift* files to your project.
 To use the Objective-C syntax sugars, import *CoreStoreBridge.h* in your *.m* source files.
 
 # Changesets
-### Upgrading from 2.x.x to 3.x.x
+### Upgrading from 3.x.x to 4.x.x
 **Obsoleted**
-- `UnsageDataTransaction.internalContext` was removed. Accessing the internal context (or more specifically, accessing context-level methods such as fetches) are now available through the `FetchableSource` and `QueryableProtocol` protocols, which are retrievable with `NSManagedObject.fetchSource()` and `NSManagedObject.querySource()` respectively. These protocols are implemented by `DataStack` and `BaseDataTransaction`.
+- `LegacySQLiteStore` is now finally obsoleted in favor of `SQLiteStore`. For sqlite files that were created previously with `LegacySQLiteStore`, make sure to use the `SQLiteStore.legacy(...)` factory method to create an `SQLiteStore` that can load the file from the legacy file path.
+- `SQLiteStore.init(...)`'s `mappingModelBundles` argument is now obsolete. The new initializer accepts a `migrationMappingProviders` optional argument where explicit mapping sources are declared. For reference on how to do this, see [Migrations](#migrations).
 
 **Deprecated**
-Methods have been renamed to better fit the [Swift 3 naming conventions](https://swift.org/documentation/api-design-guidelines/).
-- `entityDescriptionForType(_:)` → `entityDescription(for:)`
-- `objectIDForURIRepresentation(_:)` → `objectID(for:)`
-- `ImportableObject` and `ImportableUniqueObject` protocol methods (and their variants) have been renamed. The old methods are still available, but will be removed in a future update.
-    - `shouldInsertFromImportSource(_:inTransaction:)` → `shouldInsert(from:in:)`
-    - `didInsertFromImportSource(_:inTransaction:)` → `didInsert(from:in:)`
-    - `shouldUpdateFromImportSource(_:inTransaction :)` → `shouldUpdate(from:in:)`
-    - `uniqueIDFromImportSource(_:inTransaction :)` → `uniqueID(from:in:)`
-    - `updateFromImportSource(_:inTransaction:)` → `update(from:in:)`
+- `DataStack.beginAsynchronous(...)`, `AsynchronousDataTransaction.commit(...)`, `DataStack.beginSynchronous(...)`, and , `SynchronousDataTransaction.commit(...)` are now deprecated in favor of `DataStack.perform(asynchronous:...)` and `DataStack.perform(synchronous:...)` family of methods. These new `perform(...)` methods are auto-commiting, meaning the transaction automatically calls `commit()` internally after the transction closure completes. To roll-back and cancel a transaction, call `try transaction.cancel()`. See [Saving and processing transactions](#saving-and-processing-transactions) for more details.
 
-**Miscellaneous**
-- APIs obsoleted from 2.0.0 have been removed.
-- CoreStore does not depend on [GCDKit](https://github.com/JohnEstropia/GCDKit) anymore, thanks to Swift 3's better Grand Central Dispatch API.
-- All enum cases are now lowercased
-- `CoreStoreError` now implements the new Swift `CustomNSError` protocol for better Objective-C 
-bridging.
-- Some methods may emit warnings for unused return values. `@discardableResult` annotations have been set to better reflect the responsibility of API users to use/inspect return values.
+**Other Changes
+- `ListMonitor.refetch(...)` now works by recreating its internal `NSFetchedResultsController`. Previously `refetch(...)` would only apply new `FetchClause`s on top of previous fetches. Now all `FetchClauses` are required to be passed to `refetch(...)` each time.
+- New important concept about "Dynamic Models", "Schema", and "Schema Histories".
+    - *Dynamic Models* (`DynamicObject` protocol): These are Core Data object types that any `NSManagedObject` or `CoreStoreObject`s conform to.
+    - *Schema* (`DynamicSchema` protocol): These types contain info for a single model version, as well as entities that belong to it. Currently supports `XcodeDataModelSchema` (.xcdatamodeld), `LegacyXcodeDataModelSchema`, or `CoreStoreSchema`.  
+    - *SchemaHistory* (`SchemaHistory` class): This is now the preferred way to express all models to the `DataStack`. This class contains info to all the `DynamicSchema` across multiple model versions.
 
-### Upgrading from 1.x.x to 2.x.x
-**Obsoleted**
-- `AsynchronousDataTransaction.rollback()` was removed. Undo and rollback functionality are now only allowed on `UnsafeDataTransaction`s
-- `DetachedDataTransaction` was renamed to `UnsafeDataTransaction`
-- `beginDetached()` was renamed to `beginUnsafe()`
-- `PersistentStoreResult` was removed in favor of `SetupResult`
-- `SynchronousDataTransaction.commit()` was renamed to `SynchronousDataTransaction.commitAndWait()`
-- `From` initializers that accepted `NSURL`s and `NSPersistentStore` were removed.
+- TODO: changelog
 
-**Deprecated**
-The following methods are still available, but will be removed in a future update.
-- `add*Store(...)` method variants. It is strongly recommended to convert to the new API. Refer to [Local store](#local-store)) usage then use `LegacySQLiteStore` instead of `SQLiteStore` to maintain the old default file names and directory values
-    - `addInMemoryStoreAndWait(...)` → `addStorageAndWait(InMemoryStore(...))`
-    - `addSQLiteStoreAndWait(...)` → `addStorageAndWait(LegacySQLiteStore(...))`
-    - `addInMemoryStore(...)` → `addStorage(InMemoryStore(...), ...)`
-    - `addSQLiteStore(...)` → `addStorage(LegacySQLiteStore(...), ...)`
-    - `requiredMigrationsForSQLiteStore(...)` → `requiredMigrationsForStorage(...)`
-    - `upgradeSQLiteStoreIfNeeded(...)` → `upgradeStorageIfNeeded(...)`
-    - The `resetStoreOnModelMismatch: Bool` argument for the methods above are now provided to the `LegacySQLiteStore` and `SQLiteStore` initializers as a `LocalStorageOptions` option set
-- `NSError` used to have a `coreStoreErrorCode` property that returns `CoreStoreErrorCode` enum, but all CoreStore errors are now guaranteed to be `CoreStoreError` enum type in swift, and `CSError` type on Objective-C.
-- `CoreStoreLogger.handleError(...)` was deprecated in favor of `CoreStoreLogger.log(error:...)`. `CoreStoreLogger` may also implement `CoreStoreLogger.abort(...)`, which is called just before CoreStore executes `fatalError()` due to critical runtime errors.
+### Other Releases
+
+For the full Changelog, refer to the [Releases](https://github.com/JohnEstropia/CoreStore/releases) section. 
 
 
 # Contact
@@ -1515,7 +1544,7 @@ or join our Slack team at [swift-corestore.slack.com](http://swift-corestore-sla
 
 
 # Who uses CoreStore?
-I'd love to hear about apps using CoreStore. Send me a message and I'll feature your apps here!
+I'd love to hear about apps using CoreStore. Send me a message and I'll welcome any feedback!
 
 
 # License
diff --git a/Sources/CSDataStack.swift b/Sources/CSDataStack.swift
index da8614d..1822ac6 100644
--- a/Sources/CSDataStack.swift
+++ b/Sources/CSDataStack.swift
@@ -49,41 +49,22 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
     /**
      Initializes a `CSDataStack` from the model with the specified `modelName` in the specified `bundle`.
      
-     - parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set.
+     - parameter xcodeModelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set.
      - parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
      - parameter versionChain: the version strings that indicate the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
      */
     @objc
-    public convenience init(modelName: XcodeDataModelFileName?, bundle: Bundle?, versionChain: [String]?) {
+    public convenience init(xcodeModelName: XcodeDataModelFileName?, bundle: Bundle?, versionChain: [String]?) {
         
         self.init(
             DataStack(
-                modelName: modelName ?? DataStack.applicationName,
+                xcodeModelName: xcodeModelName ?? DataStack.applicationName,
                 bundle: bundle ?? Bundle.main,
                 migrationChain: versionChain.flatMap { MigrationChain($0) } ?? nil
             )
         )
     }
     
-    /**
-     Initializes a `CSDataStack` from the model with the specified `modelName` in the specified `bundle`.
-     
-     - parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set.
-     - parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
-     - parameter versionTree: the version strings that indicate the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
-     */
-    @objc
-    public convenience init(modelName: XcodeDataModelFileName?, bundle: Bundle?, versionTree: [String: String]?) {
-        
-        self.init(
-            DataStack(
-                modelName: modelName ?? DataStack.applicationName,
-                bundle: bundle ?? Bundle.main,
-                migrationChain: versionTree.flatMap { MigrationChain($0) } ?? nil
-            )
-        )
-    }
-    
     /**
      Returns the stack's model version. The version string is the same as the name of the version-specific .xcdatamodeld file.
      */
@@ -227,6 +208,19 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
     
     // MARK: Deprecated
     
+    @available(*, deprecated, message: "Use the -[initWithXcodeModelName:bundle:versionChain:] initializer.")
+    @objc
+    public convenience init(modelName: XcodeDataModelFileName?, bundle: Bundle?, versionChain: [String]?) {
+        
+        self.init(
+            DataStack(
+                xcodeModelName: modelName ?? DataStack.applicationName,
+                bundle: bundle ?? Bundle.main,
+                migrationChain: versionChain.flatMap { MigrationChain($0) } ?? nil
+            )
+        )
+    }
+    
     @available(*, deprecated, message: "Use the -[initWithModelName:bundle:versionChain:] initializer.")
     @objc
     public convenience init(model: NSManagedObjectModel, versionChain: [String]?) {
diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift
index 74ba992..c2c4a08 100644
--- a/Sources/DataStack.swift
+++ b/Sources/DataStack.swift
@@ -37,16 +37,16 @@ public final class DataStack: Equatable {
     /**
      Convenience initializer for `DataStack` that creates a `SchemaHistory` from the model with the specified `modelName` in the specified `bundle`.
      
-     - parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set (e.g. in Unit Tests).
+     - parameter xcodeModelName: the name of the (.xcdatamodeld) model file. If not specified, the application name (CFBundleName) will be used if it exists, or "CoreData" if it the bundle name was not set (e.g. in Unit Tests).
      - parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
      - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for progressive migrations. If not specified, will default to a non-migrating data stack.
      */
-    public convenience init(modelName: XcodeDataModelFileName = DataStack.applicationName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
+    public convenience init(xcodeModelName: XcodeDataModelFileName = DataStack.applicationName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
         
         self.init(
             schemaHistory: SchemaHistory(
                 XcodeDataModelSchema.from(
-                    modelName: modelName,
+                    modelName: xcodeModelName,
                     bundle: bundle,
                     migrationChain: migrationChain
                 ),
@@ -613,6 +613,17 @@ public final class DataStack: Equatable {
     
     // MARK: Deprecated
     
+    @available(*, deprecated, renamed: "init(xcodeModelName:bundle:migrationChain:)")
+    public convenience init(modelName: XcodeDataModelFileName, bundle: Bundle = Bundle.main, migrationChain: MigrationChain = nil) {
+        
+        self.init(
+            xcodeModelName: modelName,
+            bundle: bundle,
+            migrationChain: migrationChain
+        )
+    }
+    
+    
     @available(*, deprecated, message: "Use the new DataStack.init(schemaHistory:) initializer passing a LegacyXcodeDataModelSchema instance as argument")
     public convenience init(model: NSManagedObjectModel, migrationChain: MigrationChain = nil) {
         
diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift
index 5a12e22..db733a3 100644
--- a/Sources/DynamicObject.swift
+++ b/Sources/DynamicObject.swift
@@ -48,6 +48,11 @@ public protocol DynamicObject: class {
      */
     static func cs_matches(object: NSManagedObject) -> Bool
     
+    /**
+     Used internally by CoreStore. Do not call directly.
+     */
+    func cs_id() -> NSManagedObjectID
+    
     /**
      Used internally by CoreStore. Do not call directly.
      */
@@ -86,6 +91,11 @@ extension NSManagedObject: DynamicObject {
         return object.isKind(of: self)
     }
     
+    public func cs_id() -> NSManagedObjectID {
+        
+        return self.objectID
+    }
+    
     public func cs_toRaw() -> NSManagedObject {
         
         return self
@@ -135,6 +145,11 @@ extension CoreStoreObject {
         return (self as AnyClass).isSubclass(of: type as AnyClass)
     }
     
+    public func cs_id() -> NSManagedObjectID {
+        
+        return self.rawObject!.objectID
+    }
+    
     public func cs_toRaw() -> NSManagedObject {
         
         return self.rawObject!
diff --git a/Sources/DynamicSchema+Convenience.swift b/Sources/DynamicSchema+Convenience.swift
index e7f4b30..aeb98bb 100644
--- a/Sources/DynamicSchema+Convenience.swift
+++ b/Sources/DynamicSchema+Convenience.swift
@@ -31,6 +31,12 @@ import Foundation
 
 public extension DynamicSchema {
     
+    /**
+     Prints the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations. This is useful for converting current `XcodeDataModelSchema`-based models into the new `CoreStoreSchema` framework. Additional adjustments may need to be done to the generated source code for "Transformable" attributes.
+     
+     - Important: After transitioning to the new `CoreStoreSchema` framework, it is recommended to add the new schema as a new version that the existing versions' `XcodeDataModelSchema` can migrate to. It is discouraged to load existing SQLite files created with `XcodeDataModelSchema` directly into a `CoreStoreSchema`.
+     - returns: a string that represents the source code for the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations.
+     */
     public func printCoreStoreSchema() -> String {
         
         let model = self.rawModel()
diff --git a/Sources/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift
index d432367..084e331 100644
--- a/Sources/NSManagedObjectContext+Querying.swift
+++ b/Sources/NSManagedObjectContext+Querying.swift
@@ -91,7 +91,7 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource {
     @nonobjc
     public func fetchExisting(_ objects: S) -> [T] where S.Iterator.Element == T {
         
-        return objects.flatMap({ self.fetchExisting($0.cs_toRaw().objectID) })
+        return objects.flatMap({ self.fetchExisting($0.cs_id()) })
     }
     
     @nonobjc
diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift
index 969b0a1..2436013 100644
--- a/Sources/ObjectMonitor.swift
+++ b/Sources/ObjectMonitor.swift
@@ -260,16 +260,15 @@ public final class ObjectMonitor: Equatable {
     
     private init(context: NSManagedObjectContext, object: ObjectType) {
         
-        let rawObject = object.cs_toRaw()
+        let objectID = object.cs_id()
         let fetchRequest = CoreStoreFetchRequest()
-        fetchRequest.entity = rawObject.entity
+        fetchRequest.entity = objectID.entity
         fetchRequest.fetchLimit = 0
         fetchRequest.resultType = .managedObjectResultType
         fetchRequest.sortDescriptors = []
         fetchRequest.includesPendingChanges = false
         fetchRequest.shouldRefreshRefetchedObjects = true
         
-        let objectID = rawObject.objectID
         let fetchedResultsController = CoreStoreFetchedResultsController(
             context: context,
             fetchRequest: fetchRequest.dynamicCast(),
diff --git a/Sources/Where.swift b/Sources/Where.swift
index 447b37f..2571238 100644
--- a/Sources/Where.swift
+++ b/Sources/Where.swift
@@ -148,7 +148,7 @@ public struct Where: FetchClause, QueryClause, DeleteClause, Hashable {
             self.init(NSPredicate(format: "\(keyPath) == nil"))
             
         case let object?:
-            self.init(NSPredicate(format: "\(keyPath) == %@", argumentArray: [object.cs_toRaw().objectID]))
+            self.init(NSPredicate(format: "\(keyPath) == %@", argumentArray: [object.cs_id()]))
         }
     }
     
@@ -171,7 +171,7 @@ public struct Where: FetchClause, QueryClause, DeleteClause, Hashable {
      */
     public init(_ keyPath: KeyPath, isMemberOf list: S) where S.Iterator.Element: DynamicObject {
         
-        self.init(NSPredicate(format: "\(keyPath) IN %@", list.map({ $0.cs_toRaw().objectID }) as NSArray))
+        self.init(NSPredicate(format: "\(keyPath) IN %@", list.map({ $0.cs_id() }) as NSArray))
     }
     
     /**