2 min read

Persistence

Last Updated - Platform 25.0 - SDK 20.0

We use Apple's CoreData for persistence. Being part of the OS, CoreData simplifies maintenance and migrations, and avoids bloating app size and launch times.

core data overview

Objects

The Platform and SDK locally persist the following objects using CoreData:

  • Recently Viewed Product Identifiers
  • Search History
  • Viewed App Story Identifiers
  • Wishlist Item Identifiers

Stores

The PoqSDK offers the CoreDataStore wrapper around NSPersistentContainer and an NSManagedObjectContext to make it even easier to use Core Data.

A CoreDataStore can be subclassed or used in composition, and should be initialised using the name and bundle of the relevant momd file.

final class SomethingStore {
lazy var store = CoreDataStore(name: "Model", bundle: .main)
}

The CoreDataStore provides the store(for:) function to return a CoreDataObjectStore to interact with the NSPersistentStore for a specific object type, and with an optional storage limit. See Object Stores for more information.

final class SomethingStore {
lazy var items = store.store(for: CoreDataSomethingMapping(), limit: 50)
func fetchAll() throws -> [Something] {
try items.fetchAll()
}
}

This works well with the execute function for completion handler syntax which wraps the throwing return into a Result.

final class SomethingStore {
func fetchAll(completion: @escaping (Result<[Something], Error>) -> Void) {
store.execute(completion) { try items.fetchAll() }
}
}

Object Stores

The CoreDataObjectStore class wraps an NSManagedObjectContext to provide access for a specific object. It hides the Core Data data layer model behind a mapping object to return the domain model.

You can initialise a CoreDataObjectStore directly, but we recommend using the store(for:) function of a CoreDataStore. To initialise an object store you need to create a CoreDataMapping to map between an NSManagedObject and its domain model.

The CoreDataObjectStore offers the following common functions for interacting with a database. If these functions modify the managed context, the context is saved automatically.

  • Use count() to return the number of items in the database.
  • Use fetchAll() to fetch all items.
  • Use insert(item) or insert(items) to add new items.
  • Use update(item) or update(items) to update existing items.
  • Use upsert(item) or upsert(items) to update or add items.
  • Use delete(item) or delete(items) to delete items.
  • Use deleteAll() to delete all items.

For more advanced fetches you can use fetch(request) passing a custom NSFetchRequest. Some of the existing SDK mappings have functions for creating useful fetch requests, see the following.

try items.fetch(items.mapping.fetchRequest(for: SomeIdentifier()))

Object Mappings

Object stores need a CoreDataObjectMapping to describe how to map and fetch the NSManagedObject equivalent for a domain model.

To create a mapping implement the protocol and the following mapping functions.

  • Implement map(from: object, into: entity) to map into the entity to store.
  • Implement map(from: entity) to map to the domain model.

Implement the following functions to describe how to access the store. Sometimes it is useful to add custom fetchRequest() functions that can be used via a store's mapping property.

  • Implement fetchRequest() to return all the entities. Set a sortOrder to order them.
  • Implement fetchRequest(for: object) to return the equivalent stored entity.

For entities with more complex relationships you should use initialiser injection to delegate mapping, continue to Entity Relationships for more information.

Entity Relationships

The CoreDataObjectStore allows you to fetch, or map to, the NSManagedObject entities directly. This can be useful, or may be required, when working with entities with relationships.

  • Use fetchEntities(request) with an NSFetchRequest to fetch an array of entities.
  • Use fetchEntity(request) with an NSFetchRequest to fetch the first entity returned.
  • Use entity(for: object) to fetch or create and entity in the context without saving.

Let's say we have an entity CoreDataList that depends on CoreDataItem.

For inserting or updating we can set up the CoreDataListMapping to delegate the mapping of it's items. For fetch requests you could use another closure for reverse mapping, or handle it directly.

final class CoreDataListMapping {
var mapItem: (Item) -> CoreDataItem?
init(mapItem: @escaping (Item) -> CoreDataItem?) {
self.mapItem = mapItem
}
func map(from object: Object, into entity: Entity) {
entity.items = object.items.compactMap(mapItem)
}
func map(from entity: Entity) -> Object {
List(
id: entity.id,
items: entity.items.map { Item(id: $0.id) }
)
}
}

With the above we can provide the entity(for:) to the mapping object from the owning CoreDataStore.

final class ListStore: CoreDataStore {
lazy var items = store(for: CoreDataItemMapping())
lazy var lists = store(for: CoreDataListMapping(mapItem: { [items] in items.entity(for: $0) }))
func fetchAll(completion: @escaping (Result<[List], Error>) -> Void) {
execute(completion) { try lists.fetchAll() }
}
func fetch(id: String, completion: @escaping (Result<List, Error>) -> Void) {
execute(completion) { try lists.fetch(lists.mapping.fetchRequest(for: id)) }
}
}