Offline persistence with the Swift SDK

This guide will show you how to use contentful-persistence.swift to synchronize Contentful data to your local datastore.

Contentful's Content Delivery API (CDA) is a read-only API for retrieving content from Contentful. All content, both JSON and binary, is fetched from the server closest to a user's location by using our global CDN.

We publish SDKs for various languages to make developing applications easier.

Pre-requisites

This tutorial assumes you have read and understood the guide that covers the Contentful data model.

Authentication

For every request, clients need to provide an API key, which is created per space and used to delimit applications and content classes.

You can create an access token using the Contentful web app or the Content Management API.

Setup

Persisting data to disk for use when offline is important for providing a pleasant user experience. The Contentful persistence library provides a SynchronizationManager type which interfaces with the /sync endpoint of Contentful's Content Delivery API to synchronize all data within a Contentful space to a local data store. It relyies on property mappings provided by you to return types of your own definition.

In order to create a SynchronizationManager instance, you will first need to define your data model by passing in your NSManagedObject subclass types, which must conform to the Persistable protocols, into your PersistenceModel.

let model = PersistenceModel(spaceType: SyncInfo.self,
                             assetType: Asset.self,
                             entryTypes: [Image.self, Gallery.self, Author.self])

You will also need to tell the SynchronizationManager where to store the data by passing in a storage class which conforms to the PersistenceStore protocol. There is a default implementation of the PersistenceStore using Core Data, but if you want to use a different local store, you can create your own.

let store = CoreDataStore(context: self.managedObjectContext)
let syncManager = ContentfulSynchronizer(client: client,
                                          // Store separate entities for every locale in the space.
                                          localizationScheme: .all, 
                                          persistenceStore: store,
                                          persistenceModel: model)

In this case, it's your responsibility to provide the right NSManagedObjectContext for your application. If you are working with Core Data in a multi-threaded environment, make sure to read Apple's Core Data Programming Guide. Depending on your setup, you might need to create different managed object contexts for writing and reading data. While you can use the CoreDataStore class for querying, you don't have to.

The mapping between your persistence model classes and Contentful data is done by conforming to the Persistable protocols mentioned above. In the example below, we define the mapping between a Contentful content type with the identifier "author" to our NSManagedObject subclass Author:

class Author: NSManagedObject, EntryPersistable {

  static let contentTypeId = "author"

  // Properties required by Contentful.
  @NSManaged var id: String
  @NSManaged var localeCode: String
  @NSManaged var createdAt: Date?
  @NSManaged var updatedAt: Date?

  @NSManaged var biography: String?
  @NSManaged var name: String
  @NSManaged var twitterHandle: String?
  @NSManaged var authorInverse: NSSet
  @NSManaged var createdEntries: NSOrderedSet
  @NSManaged var awesomeProfilePhoto: Asset?

  // Map field names on your Contentful entry to property names on your class.
  static func fieldMapping() -> [FieldName: String] {
    return [
      "name": "name",
      "biography": "biography",
      "twitterHandle": "twitterHandle",
      "createdEntries": "createdEntries",
      // We can name our class property however we like.
      "profilePhoto": "awesomeProfilePhoto" 
    ]
  }
}

Keep in mind that for AssetPersistable types, the URL of the underlying media file will be stored, but no caching of the actual documents will be performed performed.

Special notes on the data model editor in Xcode

  • You will want to make sure that code generation is turned off in Xcode's "Data Model Editor". The editor can be found in the right side-bar when viewing your .xcdatamodel file.
  • You will also want to ensure that the id and localeCode properties on each of your EntryPersistable and AssetPersistable types is set to non-optional. This can also be acheived through the data model editor.
  • Lastly, in the "Configuration" section in the .xcdatamodel file, ensure that the module name is prefixed before your class name if you are using Swift classes. In the app (and Xcode target) we are working on is called "AwesomeApp", the class name for our Author.swift file would be set to AwesomeApp.Author.

Syncing the data

If you've made it this far, you've done all the setup necessary to sync data to your CoreData store, congrats! All that's left to do is to make the appropriate calls with via the SynchronizationManager, the update your UI using using the fetch results from your local store. Note that if you don't have a localeCode predicate specified, you will get data for all the locales you have synced with. The LocalizationScheme used by the libary is specified during initialization of the SynchronizationManager.

syncManager.sync { result in
  do {
    // In this case, we'll only fetch the entries that are localized to Mexican Spanish as our app is currently being used by somebody in Mexico.
    let authors: [Author] = try self.store.fetchAll(type: Author.self,  predicate: NSPredicate(format: "localeCode == 'es-MX'"))

  } catch {
    // Handle failure to fetch from CoreData.
  }
}