Flapjack is an iOS/macOS/tvOS framework with 2 primary goals.
- Help you abstract your model-focused database persistence layer from the rest of your app
- Simplify the database layer's API into an easy-to-use, easy-to-remember, full Swift one
It lets you skip the boilerplate commonly associated with database layers like Core Data and lets you introduce structured, sane data persistence in your app sooner, letting you spend more of your time creating the app you really want. We use it at O'Reilly Media for our iOS apps, and if you like what you see, perhaps you will too.
Swift Package Manager is the preferred way to use Flapjack. Add the following as a dependency to the dependencies
array in your Package.swift
file:
.package(name: "Flapjack", url: "https://github.com/oreillymedia/flapjack.git", .upToNextMajor(from: "0.8.1"))
Then you'll specify Flapjack
as a dependency of the target in which you wish to use it. You can also import FlapjackCoreData
and FlapjackUIKit
.
.package(name: "FlapjackCoreData", url: "https://github.com/oreillymedia/flapjack.git", .upToNextMajor(from: "0.8.1"))
.package(name: "FlapjackUIKit", url: "https://github.com/oreillymedia/flapjack.git", .upToNextMajor(from: "0.8.1"))
Flapjack is also available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Flapjack', '0.8.1'
# If you're using Core Data...
pod 'Flapjack/CoreData', '0.8.1'
# If you're targeting iOS and want some helpers...
pod 'Flapjack/UIKit', '0.8.1'
And run pod install
at the command line.
Full documentation is forthcoming, but here's a good thorough run-through of what Flapjack has to offer.
In your iOS project (like perhaps in your UIApplicationDelegate
), kick things off with the following code (if you're using Core Data; support for more databases planned).
import Flapjack
// Create the DataAccess object, your main point-of-entry for persistence.
// You can also pass in `.sql(filename: "YourCoreDataStore.sql")`.
let dataAccess = CoreDataAccess(name: "YourCoreDataStore", type: .memory)
// Then tell the stack to configure itself.
dataAccess.prepareStack(asynchronously: true) { error in
if let error = error {
print(error.localizedDescription)
}
// Make sure you retain your `dataAccess` variable, and now you're all
// ready to go!
}
For your model objects to take part in the simplified API provided by Flapjack, you'll need to make sure they conform to DataObject
. For a class such as Pancake
that has the fields identifier
, flavor
, and radius
defined in a Core Data model, this would look like the following.
extension Pancake: DataObject {
// The type of your primary key, if you have one of your own.
public typealias PrimaryKeyType = String
// The name of the entity as Core Data knows it.
public static var representedName: String {
return "Pancake"
}
// The key path to your model's primary key.
public static var primaryKeyPath: String {
return #keyPath(identifier)
}
// An array of sorting criteria.
public static var defaultSorters: [SortDescriptor] {
return [
SortDescriptor(#keyPath(flavor), ascending: true, caseInsensitive: true),
SortDescriptor(#keyPath(radius), ascending: false)
]
}
}
Now you're cookin'. Interacting with the data store is even easier.
// Get every pancake.
let pancakes = dataAccess.mainContext.objects(ofType: Pancake.self)
// Get just the chocolate chip ones.
let pancakes = dataAccess.mainContext.objects(ofType: Pancake.self, attributes: ["flavor": "Chocolate Chip"])
// Create your own.
let pancake = dataAccess.mainContext.create(Pancake.self, attributes: ["flavor": "Rhubarb"])
// Save your changes.
let error = context.persist()
Granted you don't want to do expensive data operations on the main thread. Flapjack's Core Data support follows best practices for such a thing:
dataAccess.performInBackground { [weak self] context in
let pancake = context.create(Pancake.self, attributes: ["flavor": flavor, "radius": radius, "height": height])
let error = context.persist()
DispatchQueue.main.async {
guard let `self` = self else {
return
}
let foregroundPancake = self.dataAccess.mainContext.object(ofType: Pancake.self, objectID: pancake.objectID)
completion(foregroundPancake, error)
}
}
Sick of your database? There's a function for that, too.
dataAccess.deleteDatabase(rebuild: true) { error in
if let error = error {
print(error.localizedDescription)
}
// It's almost as if it never happened.
}
This wouldn't be nearly as much fun if Flapjack didn't provide a way to automatically listen for model changes. The DataSource
and SingleDataSource
protocols define a way to listen for changes on a collection of persisted objects or a single object, respectively. If you're targeting Core Data, the two implementations of those protocols (CoreDataSource
and CoreSingleDataSource
) are powered by NSFetchResultsController
and listening to .NSManagedObjectContextObjectsDidChange
, respectively.
import Flapjack
let dataSourceFactory = CoreDataSourceFactory(dataAccess: dataAccess)
let queryAttributes = ["radius": 2.0, "flavor": "Chocolate Chip"]
let dataSource: CoreDataSource<Pancake> = dataSourceFactory.vendObjectsDataSource(attributes: queryAttributes, sectionProperty: "flavor", limit: 100)
// Prepare yourself for pancakes, but only chocolate chip ones bigger than a 2" radius, and no more than 100.
// This block fires every time the data source picks up an insert/change/deletion.
dataSource.onChange = { itemChanges, sectionChanges in
// If you've added `Flapjack/UIKit` to your Podfile, you get helper extensions!
self.tableView.performBatchUpdates(itemChanges, sectionChanges: sectionChanges)
// Get a specific pancake:
print("\(String(describing: dataSource.object(at: IndexPath(item: 0, section: 0))))")
}
// Kick off a call to start listening (and immediately fire `.onChange` with all existing results).
dataSource.execute()
For a more complete example on how to use CoreDataSource
, see AutomaticViewController.swift. To see the steps you'd have to go through to access stored data without it, see ManualViewController.swift.
Support for "easier" Core Data migrations is currently evolving, but here's what you can expect right now. Flapjack has a Migrator
class that you can conform to, and it's this object you'll use to provide your DataAccess
class with a way to migrate your data store. It's a relatively sparse protocol right now, but if you look at the Core Data implementation of this object (CoreDataMigrator
), you can see how this comes together. This is a pretty close adaptation of the way we handle migrations in our iOS apps at O'Reilly Media. Here's what happens, step by step.
- By conforming to
DataAccessDelegate
, you'll be notified when the stack is ready for aMigrator
. - In response to this delegate call, you'll initialize and return a
CoreDataMigrator
by providing thestoreURL
andbundle
where the data store file and compiled model can be found, respectively. - Then the
DataAccess
object should handle the rest, which is essentially a call tomigrate()
. - Upon invocation of
migrate()
, a temporary folder is made to house any intermediary files. - Then your compiled data model is scanned for all available model versions, and then we also try and figure out which version is the current version, and then we build an iterative list of versions by which to migrate (support for supplying a custom list of versions to migrate is forthcoming).
- Then, between each version, we either process a heavyweight migration (if an explicit mapping model is found) or a lightweight migration (if an implicit mapping model can be inferred).
- Matt Blackmon (@mblackmon)
- Laura Dickey (@lj-dickey)
- Ben Kreeger (@kreeger)
- Scott Starr (@awaltzforvenus)
Flapjack is available under the MIT license. See LICENSE file for more info.