Direct cache access
Apollo iOS provides the ability to directly read and update the cache as needed using type-safe generated operation models. This provides a strongly-typed interface for accessing your cache data in pure Swift code.
The ApolloStore
has APIs for accessing the cache via both ReadTransaction
and ReadWriteTransaction
.
This article explains how you can access cache data directly. To learn how you can use the cache alongside network requests to fetch data for your GraphQL operations, read our documentation on fetching locally cached data.
Reading cache data
You can read data from the local cache directly with ApolloStore.withinReadTransaction(_:callbackQueue:completion:)
. The transaction block provides a ReadTransaction
.
A ReadTransaction
can be used to read any of your generated GraphQL queries or fragments.
1// Read from a GraphQL Query
2client.store.withinReadTransaction({ transaction in
3 let data = try transaction.read(
4 query: HeroNameQuery(episode: .jedi)
5 )
6 print(data.hero?.name)
7})
8
9// Read from a GraphQL fragment
10client.store.withinReadTransaction({ transaction -> HeroDetails in
11 let data = try transaction.readObject(
12 ofType: HeroDetails.self,
13 withKey: "Hero:123"
14 )
15
16 print(data.hero?.name)
17})
Writing cache data
You can write data to the local cache with ApolloStore.withinReadWriteTransaction(_:callbackQueue:completion:)
. The transaction block provides a ReadWriteTransaction
. In addition the the ability to write data to the cache, ReadWriteTransaction
has all of the functionality of a ReadTransaction
. To write data to the cache, you'll need to define a LocalCacheMutation
.
Defining local cache mutations
Like reading cache data, writing to the cache uses type-safe generated models. However, because the generated models for your operations and fragments are immutable, you cannot change the values on these models to write them to the cache. In order to write to the cache, you can define a LocalCacheMutation
.
A LocalCacheMutation
is just a GraphQL query or fragment definition that has been flagged as a local cache mutation using the Apollo iOS specific directive @apollo_client_ios_localCacheMutation
.
When a query or fragment with this directive is defined, the code generation engine will generate a mutable model that can be used with a ReadWriteTransaction
to write data to the cache.
Your query definitions can also define variables for these operations to mutate cache data for fields with input arguments. For more information see our operation argument documentation.
1query HeroNameLocalCacheMutation(
2 $episode: Episode!
3) @apollo_client_ios_localCacheMutation {
4 hero(episode: $episode) {
5 id
6 name
7 }
8}
1fragment MutableHeroDetails on Hero
2@apollo_client_ios_localCacheMutation {
3 id
4 name
5}
The generated models for your mutations will have mutable fields (var
instead of let
). Generating both getters and setters for fields on the mutable models means that they are larger than immutable generated models.
Separating cache mutations from network operations
By flagging a query as a
LocalCacheMutation
, the generated model for that cache mutation no longer conforms toGraphQLQuery
. This means you can no longer use that cache mutation as a query operation.Fundamentally, this is because cache mutation models are mutable, whereas network response data is immutable. Cache mutations are designed to access and mutate only the data necessary.
If our cache mutation models were mutable, mutating them outside of a
ReadWriteTransaction
wouldn't persist any changes to the cache. Additionally, mutable data models require nearly double the generated code. By maintaining immutable models, we avoid this confusion and reduce our generated code.Avoid creating mutable versions of entire query operations. Instead, define mutable fragments or queries to mutate only the fields necessary.
Writing local cache mutations to the cache
Once you have generated mutable query models, you can use them with ReadWriteTransaction.update()
.
1store.withinReadWriteTransaction({ transaction in
2 let cacheMutation = HeroNameLocalCacheMutation(episode: .CLONES)
3
4 try transaction.update(cacheMutation) { (data: inout HeroNameLocalCacheMutation.Data) in
5 data.hero.name = "General Kenobi"
6 }
7
8 let queryData = try transaction.read(
9 query: HeroNameQuery(episode: .jedi)
10 )
11 print(queryData.hero?.name) // "General Kenobi"
12})
13
Writing mutable fragments to the cache
To write data for a mutable fragment, use ReadWriteTransaction.updateObject(ofType:withKey:variables:_:)
. You will need to pass the cache key of the object you want to update along with the mutable fragment.
For more information on cache keys in the normalized cache, check out our documentation on normalized cache responses.
1store.withinReadWriteTransaction({ transaction in
2 try transaction.updateObject(
3 ofType MutableHeroDetails.self,
4 withKey: "Hero:123"
5 ) { (data: inout MutableHeroDetails) in
6 data.hero.name = "General Kenobi"
7
8 let queryData = try transaction.read(
9 query: HeroNameQuery(episode: .jedi)
10 )
11
12 print(queryData.hero?.name) // "General Kenobi"
13 }
14})
Deleting cache data
There are presently three deletion methods available:
ApolloStore.clear
Removes all data from the cache immediately.
ReadWriteTransaction.removeObject(for:)
Removes a single object for the given CacheKey
.
ReadWriteTransaction.removeObjects(matching:)
Removes all objects with a CacheKey
that matches the given pattern. Pattern matching is not case sensitive. For an in memory cache it is the equivalent of checking whether the cache key contains the pattern and SQLite
caches will perform a LIKE
query to remove the objects. This method can be very slow depending on the number of records in the cache, it is recommended that this method be called on a background queue.
The removeObject(for:)
and removeObjects(matching:)
functions will only remove things at the level of an object. They cannot remove individual properties from an object, and cannot remove outside references to an object.
Deleting an object from the cache does not perform cascading deletions. That is, if you remove an object which has a reference to another object, the reference will be removed, but that other object will not be removed and will remain in the cache. Likewise, if you delete an object, references to that object will not be deleted, they will simply fail, causing a cache miss, when you attempt to load the object again.
This means that if you are planning to remove something, be sure that you either:
A) Know for sure you no longer need it
or
B) Are fine with your cache policy potentially triggering an additional fetch if the missing value causes a read to fail
Note: As of right now, there is not a way to delete a single property's value. For instance, calling
try transaction.removeRecord(for: "2001.name")
will result in no action, as there would not be a record with the cache key"2001.name"
, sincename
is a scalar field on the"2001"
record.
Deleting objects by cache key
You will need to have a clear understanding of how you are generating cache keys to be able to use this functionality effectively. To help you figure out how to construct a cache key or pattern to delete, we recommend having a clear understanding of how cache keys are generated and configuring custom cache keys for your cache objects.
Removing objects by their cache key allows you to clear stale or unneeded cache data without deleting the entire cache.
In Apollo iOS, you can configure custom cache keys for your objects. The key for an entry in the cache that uses custom cache IDs will have the format ${ObjectType}:${CustomCacheKey)
.
In this example, we've configured our cache to attempt to use the id
field as a cache ID.
1static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
2 try? CacheKeyInfo(jsonValue: object["id"])
3}
This indicates that for every object which has an id
field, its cache ID will be the value of the id
field.
If you have previously fetched a Book
object with the ID "2001"
, the key for that entry in the cache will be "Book:2001"
. To delete this object from the cache, you can call transaction.removeObject(for: "Book:2001")
within a ReadWriteTransaction
. This will remove the cache entry with that key, along with all of its associated scalar properties and the references to other objects it stores.
We can also delete all objects of a concrete object type from the cache. To remove all Book
objects, you can use transaction.removeObjects(matching: "Book:")
.