BNR Core Data Stack
The BNR Core Data Stack is a small framework, written in Swift, that makes it easy to quickly set up a multi-threading ready Core Data stack.
For more details on the design methodology see: Introducing the Big Nerd Ranch Core Data Stack
For complete source documentation see: Documentation
Deprecations
With the introduction of Apple's NSPersistentContainer, in iOS 10/macOS 10.12, BNR has chosen to deprecate the CoreDataStack class. See [container example](./Container Example) for tips on using an NSPersistentContainer
Apple has also added a type method entity()
to NSManagedObject
allowing us to deprecate CoreDataModelable and migrate many of those same convenience functions to an extension of NSManagedObject
.
While Apple also introduced some type safety to their NSFetchedResultsController
, we believe our FetchedResultsController provides a better API by:
- including type-safety in our section info
- providing a type-safe delegate
- guarding against invalid index paths in Inserts, Deletes, Moves, and Updates
Similarly our EnittyMonitor still serves a niche not covered by built in CoreData objects. See Entity Monitor
Minimum Requirements
Runtime:
- macOS 10.10
- tvOS 9.0
- iOS 8.0
Build Time:
- Xcode 8.0
- Swift 3.0
Installation
Carthage
Add the following to your Cartfile:
github "BigNerdRanch/CoreDataStack"
Then run carthage update
.
Follow the current instructions in Carthage's README for up to date installation instructions.
CocoaPods
Add the following to your Podfile:
pod 'BNRCoreDataStack'
You will also need to make sure you're opting into using frameworks:
use_frameworks!
Then run pod install
.
Usage
Type Safe Monitors
Fetched Results Controller
FetchedResultsController<T>
is a type safe wrapper around NSFetchedResultsController
using Swift generics.
Example
See BooksTableViewController.swift for an example.
Entity Monitor
EntityMonitor<T>
is a class for monitoring inserts, deletes, and updates of a specific NSManagedObject
subclass within an NSManagedObjectContext
.
Example
See EntityMonitorTests.swift for an example.
NSManagedObject Extensions
Adds convenience methods on
NSManagedObject` subclasses. These methods make fetching, inserting, deleting, and change management easier.
Example
let allBooks = try Book.allInContext(moc)
let anyBook = try Book.findFirstInContext(moc)
try Book.removeAllInContext(moc)
Constructing Your Stack
Import Framework
via: Carthage
import CoreDataStack
or via CocoaPods
import BNRCoreDataStack
Standard SQLite Backed
CoreDataStack.constructSQLiteStack(withModelName: "TestModel") { result in
switch result {
case .success(let stack):
self.myCoreDataStack = stack
print("Success")
case .failure(let error):
print(error)
}
}
In-Memory Only
do {
myCoreDataStack = try CoreDataStack.constructInMemoryStack(withModelName: "TestModel")
} catch {
print(error)
}
Working with Managed Object Contexts
Private Persisting/Coordinator Connected Context
This is the root level context with a PrivateQueueConcurrencyType
for asynchronous saving to the NSPersistentStore
. Fetching, Inserting, Deleting or Updating managed objects should occur on a child of this context rather than directly.
myCoreDataStack.privateQueueContext
Main Queue / UI Layer Context
This is our MainQueueConcurrencyType
context with its parent being the private persisting context. This context should be used for any main queue or UI related tasks. Examples include setting up an NSFetchedResultsController
, performing quick fetches, making UI related updates like a bookmark or favoriting an object. Performing a save() call on this context will automatically trigger a save on its parent via NSNotification
.
myCoreDataStack.mainQueueContext
Creating a Worker Context
Calling newChildContext()
will vend us a PrivateQueueConcurrencyType
child context of the main queue context. Useful for any longer running task, such as inserting or updating data from a web service. Calling save() on this managed object context will automatically trigger a save on its parent context via NSNotification
.
let workerContext = myCoreDataStack.newChildContext()
workerContext.performBlock() {
// fetch data from web-service
// update local data
workerContext.saveContext()
}
Large Import Operation Context
In most cases, offloading your longer running work to a background worker context will be sufficient in alleviating performance woes. If you find yourself inserting or updating thousands of objects then perhaps opting for a stand alone managed object context with a discrete persistent store like so would be the best option:
myCoreDataStack.newBatchOperationContext() { result in
switch result {
case .success(let batchContext):
// my big import operation
case .failure(let error):
print(error)
}
}
Resetting The Stack
At times it can be necessary to completely reset your Core Data store and remove the file from disk, for example when a user logs out of your application. An instance of CoreDataStack
can be reset by using the function
resetStore(resetCallback: CoreDataStackStoreResetCallback)
.
myCoreDataStack.resetStore() { result in
switch result {
case .success:
// proceed with fresh Core Data Stack
case .failure(let error):
print(error)
}
}
Contributing
Please see our guide to contributing to the CoreDataStack
Debugging Tips
To validate that you are honoring all of the threading rules it's common to add the following to a project scheme under Run > Arguments > Arguments Passed On Launch
.
-com.apple.CoreData.ConcurrencyDebug 1
This will throw an exception if you happen to break a threading rule. For more on setting up Launch Arguments check out this article by NSHipster.
iCloud and iTunes Backup Considerations
By default the CoreData store URL can be included in the backup of a device to both iCloud and local disk backups via iTunes. For sensitive information such as health records or other personally identifiable information, you should supply a URL
to the constructor that has been flagged as excluded from backup.
Example:
// Setup your URL
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
var storeFileURL = URL(string: "MyModel.sqlite", relativeTo: documentsDirectory)!
do {
var resources = URLResourceValues()
resources.isExcludedFromBackup = true
try storeFileURL.setResourceValues(resources)
} catch {
//handle error ...
}
// Create your stack
CoreDataStack.constructSQLiteStack(withModelName: "MyModel", withStoreURL: storeFileURL) { result in
switch result {
case .success(let stack):
// Use your new stack
case .failure(let error):
//handle error ...
}
}