A Core Data wrapper that leverages the power of generics to allow you to work with custom model objects.
Y—Persistence is licensed under the Apache 2.0 license.
Documentation is automatically generated from source code comments and rendered as a static website hosted via GitHub Pages at: https://yml-org.github.io/ypersistence-ios/
PersistenceManager
serves as a wrapper for Core Data's NSPersistentContainer
and also vends managed object contexts for performing core data operations.
You will need to instantiate one PersistenceManager
per NSPersistentContainer
. This should be done
on application launch. You should then use Dependency Injection to pass to classes that need it.
The standard initializer lets you specify the model name, merge policy, and bundle. It provides sensible defaults for merge policy and bundle, but you need to provide a model name.
Prior to first use, you need to call load
on the persistence manager, which is an asynchronous operation. Usually it is fast, but can take several seconds when a migration needs to occur.
import YPersistence
final class AppCoordinator {
let persistenceManager = PersistenceManager(modelName: "MyModel")
func configure(completion: @escaping () -> Void) {
// configure analytics, network, etc.
...
// configure persistence
persistenceManager.load { _ in
completion()
}
}
}
Each persistence manager has three methods for vending managed object contexts:
mainContext
returns the main context, suitable for read-only operations on the main thread only.
workerContext
returns a new private queue context, suitable for short-lived add, edit, or delete operations.
contextForThread
returns a context that is suitable for read-only operations on the current thread only. When called from the main thread, it will return mainContext
. When called from a background thread, it will create a new private queue context for that thread (if none yet exists) and cache it.
In most simple use cases, you don't even need to worry about managing contexts because for save and delete operations a local workerContext
will be created, and for fetch operations the appropriate contextForThread
will be used. For advanced use cases, we support passing in the context that you wish to use.
Important: All writing to the Core Data container should be done through short-lived worker contexts. Reading can be done from any context as long as it is done in the same thread on which the context was created.
Y—Persistence leverages the power of generics to allow you to do common operations such as fetch, save, and delete without having to create separate queries for each entity (SQL table). It also lets you convert between Core Data NSManagedObject
and generic model objects (struct or class). In order for this to work, the model objects need to conform to different protocols.
// a business object
struct Person {
let personId: String
let name: String
}
// a managed object
class PersonRecord: NSManagedObject {
@NSManaged var id: String!
@NSManaged var name: String!
}
The CoreModel
protocol is used to represent any uniquely identifiable model object. It requires a model object to have a unique identifier, but that identifier can be any appropriate type (String
, Int
, or UUID
are common types). Essentially all model objects used in Y—Persistence (whether they be JSON model objects or Core Data NSManagedObject
s) need to conform to CoreModel
.
UidType: UniqueIdentifier
the type of field that will be used as the unique identifier.uid: UidType
the unique identifier for this object
extension Person: CoreModel {
typealias UidType = String
public var uid: String { personId }
}
extension PersonRecord: CoreModel {
typealias UidType = String
public var uid: String { id ?? "" }
}
The DataRecord
protocol is used to represent any Core Data record. It extends CoreModel
, so our records need to be uniquely identifiable (so that we can fetch, delete, or save). It requires:
entityName: String
the name of the Core Data entity (SQL table)uidKey: String
the name of the attribute (SQL column) that serves as unique key. Defaults to "uid".
extension PersonRecord: DataRecord {
static var entityName: String { "PersonRecord" }
static var uidKey: String { "id" }
}
The ModelRepresentable
protocol is used to represent any Core Data record that can be associated with a model object (which can be any struct or class that conforms to CoreModel
). This will be used to help convert between the Core Data record and a business model object.
ModelType: CoreModel
the associated model object for this type of record.uid: ModelType.UidType
the record's unique identifier (which matches the associated model's unique identifier).
extension PersonRecord: ModelRepresentable {
typealias ModelType = Person
}
The RecordFromModel
protoocl is used to represent any Core Data record that can be populated from an associated model object. Common use case: save records to Core Data from model objects returned from an API call.
func fromModel(_ model: ModelType)
populates the Core Data record from a model object.
extension PersonRecord: RecordFromModel {
func fromModel(_ model: Person) {
id = model.personId
name = model.name
}
}
The RecordToModel
protocol is used to represent any Core Data record that can be used to populate an associated model object. Common use case: fetch records from Core Data as model objects that can be used in API POST request (or handed off to UI as thread-safe model objects).
func toModel() -> ModelType
converts the Core Data record to a model object.
extension PersonRecord: RecordToModel {
func toModel() -> Person {
Person(
personId: id ?? "",
name: name ?? ""
)
}
}
Conforming to some of the protocols above allows Y—Persistence to perform generic operations such as fetch, save, and delete without having to build unique queries for each different entity (SQL table) in your Core Data model.
Fetching can be done by a single uid or an array of uids and can return either a record (NSManagedObject
subclass) or a model.
func fetchPerson(uid: String) throws -> Person? {
try persistenceManager.fetchModel(entity: PersonRecord.self, uid: uid)
}
func fetchPeople(uids: [String]) throws -> [Person] {
try persistenceManager.fetchModel(entity: PersonRecord.self, uids: uids)
}
Deletes can be performed by passing uids or by passing single or multiple model objects (from which the uid is extracted).
func deletePeople(by uids: [String]) throws {
try persistenceManager.deleteRecords(entity: PersonRecord.self, uids: uids)
}
func delete(person: Person) throws {
try persistenceManager.deleteModel(entity: PersonRecord.self, model: person)
}
func delete(people: [Person]) throws {
try persistenceManager.deleteModels(entity: PersonRecord.self, models: people)
}
Saves can be performed on an array of model objects and can optionally overwrite existing records. Use shouldOverwrite: true
when replacing local records with remote models or false
when you are caching results of a paged fetch.
func save(people: [Person]) throws {
try persistenceManager.save(
entity: PersonRecord.self,
models: people,
shouldOverwrite: true
)
}
You can add Y—Persistence to an Xcode project by adding it as a package dependency.
- From the File menu, select Add Packages...
- Enter "https://github.com/yml-org/ypersistence-ios" into the package repository URL text field
- Click Add Package
brew install swiftlint
sudo gem install jazzy
Clone the repo and open Package.swift
in Xcode.
We utilize semantic versioning.
{major}.{minor}.{patch}
e.g.
1.0.5
We utilize a simplified branching strategy for our frameworks.
- main (and development) branch is
main
- both feature (and bugfix) branches branch off of
main
- feature (and bugfix) branches are merged back into
main
as they are completed and approved. main
gets tagged with an updated version # for each release
feature/{ticket-number}-{short-description}
bugfix/{ticket-number}-{short-description}
e.g.
feature/CM-44-button
bugfix/CM-236-textview-color
Prior to submitting a pull request you should:
- Compile and ensure there are no warnings and no errors.
- Run all unit tests and confirm that everything passes.
- Check unit test coverage and confirm that all new / modified code is fully covered.
- Run
swiftlint
from the command line and confirm that there are no violations. - Run
jazzy
from the command line and confirm that you have 100% documentation coverage. - Consider using
git rebase -i HEAD~{commit-count}
to squash your last {commit-count} commits together into functional chunks. - If HEAD of the parent branch (typically
main
) has been updated since you created your branch, usegit rebase main
to rebase your branch.- Never merge the parent branch into your branch.
- Always rebase your branch off of the parent branch.
When submitting a pull request:
- Use the provided pull request template and populate the Introduction, Purpose, and Scope fields at a minimum.
- If you're submitting before and after screenshots, movies, or GIF's, enter them in a two-column table so that they can be viewed side-by-side.
When merging a pull request:
- Make sure the branch is rebased (not merged) off of the latest HEAD from the parent branch. This keeps our git history easy to read and understand.
- Make sure the branch is deleted upon merge (should be automatic).
- Tag the corresponding commit with the new version (e.g.
1.0.5
) - Push the local tag to remote
You can generate your own local set of documentation directly from the source code using the following command from Terminal:
jazzy
This generates a set of documentation under /docs
. The default configuration is set in the default config file .jazzy.yaml
file.
To view additional documentation options type:
jazzy --help
A GitHub Action automatically runs each time a commit is pushed to main
that runs Jazzy to generate the documentation for our GitHub page at: https://yml-org.github.io/ypersistence-ios/