Skip to content

oreillymedia/flapjack

Repository files navigation

Flapjack

Flapjack is an iOS/macOS/tvOS framework with 2 primary goals.

  1. Help you abstract your model-focused database persistence layer from the rest of your app
  2. 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.

Getting started

Swift Package Manager

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"))

CocoaPods

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.

Usage

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.
}

Data sources

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.

Migrations

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 a Migrator.
  • In response to this delegate call, you'll initialize and return a CoreDataMigrator by providing the storeURL and bundle 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 to migrate().
  • 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).

Authors

License

Flapjack is available under the MIT license. See LICENSE file for more info.