2 min read
Persistence
Last Updated - Platform 25.0 - SDK 20.0We use Apple's CoreData for persistence. Being part of the OS, CoreData simplifies maintenance and migrations, and avoids bloating app size and launch times.
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)
orinsert(items)
to add new items. - Use
update(item)
orupdate(items)
to update existing items. - Use
upsert(item)
orupsert(items)
to update or add items. - Use
delete(item)
ordelete(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 asortOrder
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 anNSFetchRequest
to fetch an array of entities. - Use
fetchEntity(request)
with anNSFetchRequest
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)) } }}