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 asortOrderto 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 anNSFetchRequestto fetch an array of entities. - Use
fetchEntity(request)with anNSFetchRequestto 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)) } }}