contentful-persistence.swift - Core Data Offline Persistence for Contentful

August 26, 2026 · View on GitHub

Join Contentful Community Slack   Join Contentful Community Forum

contentful-persistence.swift - Core Data Offline Persistence for Contentful

An integration to simplify persisting data from Contentful to a local Core Data database, built on top of the official contentful.swift library. It uses the Sync API of the Content Delivery API to synchronize all content in a Contentful space to a device and keep it up to date with delta updates.

This repository is actively maintained   MIT License   Build Status

Version   Carthage compatible   Swift Package Manager compatible   iOS | macOS | watchOS | tvOS  

What is Contentful?

Contentful provides content infrastructure for digital teams to power websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship their products faster.

Table of contents

Core Features

  • Keeps a local Core Data database in sync with a Contentful space using the Sync API, fetching only what changed since the last sync.
  • Maps Contentful entries and assets onto your own NSManagedObject subclasses via the EntryPersistable and AssetPersistable protocols, with automatic or custom field mapping.
  • Resolves relationships between entries — including to-one and to-many links — as pages arrive, deferring and caching any links that can't yet be resolved so they complete once the target syncs in (even across app launches).
  • Localization support via LocalizationScheme: persist only the default locale, a single locale, or every locale your space supports.
  • Two ways to seed a database before the first network sync: bundling exported Contentful JSON, or shipping a pre-built SQLite file.
  • Database versioning with automatic wipe-and-reseed when your bundled schema version increases.
  • Rich Text fields decode into RichTextDocument, which conforms to NSCoding so it can be stored directly in a Core Data Transformable attribute.
  • Zero additional third-party runtime dependencies — the SDK only relies on Foundation, CoreData, and contentful.swift.
  • Ships with a privacy manifest for App Store submissions.

Getting started

In order to get started with contentful-persistence.swift, it's highly recommended that you're already familiar with the contentful.swift SDK, and with Apple's Core Data framework, since many issues encountered during development are Core Data specific. Read the Core Data Programming Guide if you're new to it.

Requirements

RequirementVersion
Swift5.0 or later
Xcode15.x recommended (CI builds against Xcode 15.4)
iOS12.0+
macOS10.13+
tvOS12.0+
watchOS4.0+

The SDK depends only on contentful.swift at runtime; it has no other third-party dependencies.

Installation

Swift Package Manager

Swift Package Manager is the recommended way to integrate the SDK. In Xcode, choose File > Add Package Dependencies… and enter https://github.com/contentful/contentful-persistence.swift, or add the dependency to your Package.swift manifest:

.package(url: "https://github.com/contentful/contentful-persistence.swift", .upToNextMajor(from: "0.18.2"))

Then add the product to the targets that need it:

.target(
    name: "MyApp",
    dependencies: [
        .product(name: "ContentfulPersistence", package: "contentful-persistence.swift")
    ]
)

CocoaPods

platform :ios, '12.0'
use_frameworks!
pod 'ContentfulPersistenceSwift', '~> 0.18.2'

To learn more about operators for dependency versioning within a Podfile, see the CocoaPods doc on the Podfile.

Carthage

Add the following to your Cartfile:

github "contentful/contentful-persistence.swift" ~> 0.18.2

Then build the XCFrameworks:

carthage update --use-xcframeworks

Your first sync

The SynchronizationManager manages the state of your Core Data database and keeps it in sync with the data from your Contentful space:

import Contentful
import ContentfulPersistence

// Tell the library which of your `NSManagedObject` subclasses conforming to `EntryPersistable`
// should be used when mapping API responses to Core Data entities.
let entryTypes: [EntryPersistable.Type] = [Author.self, Category.self, Post.self]

// Initialize the data store and its schema.
let store = CoreDataStore(context: managedObjectContext)
let persistenceModel = PersistenceModel(spaceType: SyncInfo.self, assetType: Asset.self, entryTypes: entryTypes)

// Initialize the Contentful.Client. Passing it to the manager below wires up the
// `persistenceIntegration` so that sync responses are reported automatically.
let client = Client(spaceId: "<YOUR_SPACE_ID>", accessToken: "<YOUR_DELIVERY_ACCESS_TOKEN>")

// Create the manager.
let syncManager = SynchronizationManager(
    client: client,
    localizationScheme: .all, // Save data for all locales your space supports.
    persistenceStore: store,
    persistenceModel: persistenceModel
)

// Sync with the API. The callback fires once every page of the sync has been persisted.
syncManager.sync { result in
    switch result {
    case .success:
        do {
            let posts: [Post] = try store.fetchAll(type: Post.self, predicate: NSPredicate(value: true))
            print(posts)
        } catch {
            // Handle error thrown by Core Data fetches.
        }
    case .failure(let error):
        print(error)
    }
}

To continue syncing from where you left off, simply call syncManager.sync(then:) again later — the manager persists and reuses the sync token from your SyncSpacePersistable (SyncInfo above) automatically.

Using the SDK

Define your Core Data model

To integrate your model classes with contentful-persistence.swift, conform to AssetPersistable for Contentful Assets, or EntryPersistable for your own content types. Both protocols extend ContentSysPersistable, which requires a non-optional id property, plus optional localeCode, createdAt, and updatedAt properties.

Next, create the corresponding entity in your project's xcdatamodel file.

NOTE: Optionality in Core Data entities differs from Swift optionality. For a Core Data entity, optionality means that a property may be absent when saving to the database. To configure a property's optionality, open the "Data Model Inspector" in Xcode's "Utilities" right sidebar and toggle the "Optional" checkbox.

The mapping of Contentful fields to your entity's properties is derived automatically from matching names, but you can customize it by implementing static func fieldMapping() -> [FieldName: String] on your EntryPersistable type. This should not include metadata from the sys object (e.g. id, createdAt).

import Foundation
import CoreData
import ContentfulPersistence
import Contentful

// The following @objc attribute is only necessary if your xcdatamodel Default configuration doesn't have your module
// name prepended to the Swift class. To enable removing the @objc attribute, change the Class for your entity to `ModuleName.Post`.
@objc(Post)
class Post: NSManagedObject, EntryPersistable {

    // The identifier of the corresponding content type in Contentful.
    static let contentTypeId = "post"

    // Properties derived from the `sys` object of Contentful resources.
    @NSManaged var id: String
    @NSManaged var localeCode: String?
    @NSManaged var createdAt: Date?
    @NSManaged var updatedAt: Date?

    // Custom fields on the content type.
    @NSManaged var body: String?
    @NSManaged var comments: NSNumber?
    // NOTE: Unlike date fields in the `sys` object, this library can't store `Date` for custom fields.
    // Use `String` and map to `Date` after fetching from Core Data.
    @NSManaged var customDateField: String?
    @NSManaged var date: Date?
    @NSManaged var slug: String?
    @NSManaged var tags: Data?
    @NSManaged var title: String?
    @NSManaged var authors: NSOrderedSet?
    @NSManaged var category: NSOrderedSet?
    @NSManaged var theFeaturedImage: Asset?

    // Define the mapping from the fields on your Contentful.Entry to your model class.
    // In the example below, only the `title`, `date`, `author`, and `featuredImage` fields are populated.
    static func fieldMapping() -> [FieldName: String] {
        return [
            "title": "title",
            "featuredImage": "theFeaturedImage",
            "author": "authors",
            "date": "date"
        ]
    }
}

SpaceType and AssetType

PersistenceModel requires both a spaceType and an assetType. These correspond to Core Data entities used for storing sync-token metadata (SyncSpacePersistable) and Contentful Assets (AssetPersistable), respectively.

To function correctly, these types must:

  • Be NSManagedObject subclasses.
  • Conform to SyncSpacePersistable and AssetPersistable, respectively.
  • Be defined as entities in your Core Data xcdatamodel file.
class SyncInfo: NSManagedObject, SyncSpacePersistable {
    @NSManaged var syncToken: String?
    @NSManaged var dbVersion: NSNumber?
}

class Asset: NSManagedObject, AssetPersistable {
    @NSManaged var id: String
    @NSManaged var localeCode: String?
    @NSManaged var title: String?
    @NSManaged var assetDescription: String?
    @NSManaged var urlString: String?
    @NSManaged var createdAt: Date?
    @NSManaged var updatedAt: Date?

    @NSManaged var size: NSNumber?
    @NSManaged var width: NSNumber?
    @NSManaged var height: NSNumber?
    @NSManaged var fileType: String?
    @NSManaged var fileName: String?
}

And then in the xcdatamodeld file:

Relationships

Let's say we have the following content model in our Contentful space:

Product
- name: String
- relatedProducts: [Product]

It represents a product with a name and a list of related products. This translates into a Swift model as follows:

class Product: NSManagedObject {
    // Contentful metadata.
    @NSManaged var id: String
    @NSManaged var localeCode: String?
    @NSManaged var createdAt: Date?
    @NSManaged var updatedAt: Date?

    // Fields defined on the content type.
    @NSManaged var name: String?
    @NSManaged var relatedProducts: NSOrderedSet?
}

extension Product: EntryPersistable {
    static var contentTypeId = "product"

    static func fieldMapping() -> [FieldName: String] {
        return [
            "name": "name",
            "relatedProducts": "relatedProducts"
        ]
    }
}

The corresponding Core Data entity looks like this — note the type and the arrangement set to Ordered:

After fetching products from the database, related products can be accessed like this:

for product in products {
    guard let relatedProductsSet = product.relatedProducts,
          let relatedProducts = relatedProductsSet.array as? [Product] else {
        continue
    }

    for relatedProduct in relatedProducts {
        print("Related product:", relatedProduct.id, relatedProduct.name as Any)
    }
}

Relationships that can't be resolved yet — because the target entry hasn't synced down in an earlier page, or belongs to a future sync — are cached to disk and resolved automatically once the target becomes available, including across app launches.

Rich text

Rich text fields decode into a RichTextDocument. Since RichTextDocument is an NSObject conforming to NSCoding, it can be stored directly in a Transformable Core Data attribute:

import CoreData
import Contentful
import ContentfulPersistence

@objc(Article)
class Article: NSManagedObject, EntryPersistable {
    static let contentTypeId = "article"

    @NSManaged var id: String
    @NSManaged var localeCode: String?
    @NSManaged var createdAt: Date?
    @NSManaged var updatedAt: Date?
    @NSManaged var body: RichTextDocument?

    static func fieldMapping() -> [FieldName: String] {
        return [
            "body": "body"
        ]
    }
}

Set the attribute's type to Transformable in the Core Data model editor. To render the stored document into native views, use rich-text-renderer.swift.

Localization

Configure SynchronizationManager with a LocalizationScheme to control which locales are persisted:

  • .default — save entities only for the default locale of your space.
  • .one(localeCode) — save entities for a single, specific locale.
  • .all — save entities for every locale your space supports. Remember to include localeCode when building predicates against your Persistable model classes.
let syncManager = SynchronizationManager(
    client: client,
    localizationScheme: .one("de-DE"),
    persistenceStore: store,
    persistenceModel: persistenceModel
)

If you need to switch from a lightweight initial sync (e.g. .default) to persisting every locale afterwards, use the two-phase sync helper, which resets the sync token between phases so all locales are captured on the follow-up pass:

try syncManager.sync(
    syncSpacePersistable: SyncInfo.self,
    initialLocalizationScheme: .default,
    onInitialCompletion: { result in
        // Called once the default-locale sync finishes.
    },
    onFinalCompletion: { result in
        // Called once the follow-up sync for all locales finishes.
    }
)

Advanced configuration

Preseeding from a bundled database

You can ship a pre-built SQLite database in your app bundle to avoid an initial network sync entirely. The database is copied into place on two conditions: no database currently exists in the target container folder, or the existing one has a dbVersion lower than the one you're providing.

  1. Bundle your preseeded database (for example Test.sqlite) in your app's main bundle.

  2. Use the SynchronizationManager initializer that accepts a PreseedConfiguration:

    let sqliteContainerFolderPath = <path to the folder where Core Data stores its SQLite files>
    
    let preseedConfig = PreseedConfiguration(
        resourceName: "Test",                          // Preseeded db name in bundle.
        resourceExtension: "sqlite",                    // Preseeded db extension in bundle.
        sqliteContainerPath: sqliteContainerFolderPath,  // Folder Core Data creates to store the sqlite file.
        dbVersion: 2                                     // New version (must be greater than the existing one, if any).
    )
    
    let syncManager = try SynchronizationManager(
        client: client,
        localizationScheme: .all,
        persistenceStore: store,
        persistenceModel: persistenceModel,
        preseedConfig: preseedConfig
    )
    
  3. On initialization, the SDK checks whether the SQLite file already exists at the target path. If it doesn't — or if it exists but its dbVersion is lower than preseedConfig.dbVersion — the bundled file is copied into place and the new version is recorded.

If you're using a custom PersistenceStore implementation, pass your own preseedStrategy parameter (conforming to PreseedStrategy) to control exactly how the swap happens; it defaults to FilePreseedManager, which uses FileManager.

Preseeding from bundled JSON

Alternatively, seed a database from a directory of exported Contentful JSON files — generated with the ContentfulBundleSync command line interface — rather than a SQLite file:

try syncManager.seedDBFromJSONFiles(in: "ContentfulExport", in: Bundle.main)

Bundled media referenced by your assets can be retrieved with:

let data = SynchronizationManager.bundledData(for: asset, inDirectoryNamed: "ContentfulExport", in: Bundle.main)

Database migrations

Both preseeding mechanisms rely on the same dbVersion stored on your SyncSpacePersistable type. Independently of preseeding, calling sync(dbVersion:then:) with a higher version number than what's currently stored wipes the entire persistence store (and any cached relationships) before syncing again — useful when you ship a breaking Core Data model change:

syncManager.sync(dbVersion: SynchronizationManager.DBVersions.default.rawValue) { result in
    // ...
}

Custom persistence stores

CoreDataStore is the default PersistenceStore implementation, but you can provide your own by conforming to the PersistenceStore protocol. If you also want your custom store to support SQLite bundle preseeding, implement onStorePreseedingWillBegin(at:) and onStorePreseedingCompleted(at:), which are called immediately before and after the bundled file is swapped in.

Privacy manifest

The SDK ships PrivacyInfo.xcprivacy, declaring that it collects no data, performs no tracking, and uses file-timestamp, user-defaults, and system-boot-time APIs only for the reasons Apple permits. When installed via Swift Package Manager or CocoaPods, the manifest is bundled automatically and folds into your app's privacy report.

Documentation & References

For further information about the underlying REST API, check out the Content Delivery API Reference Documentation, or browse the API reference documentation for this library, which can also be loaded into Xcode as a Docset.

This library is a companion to contentful.swift; consult its README for details on Client, EntryDecodable, queries, and the rest of the Content Delivery API surface.

Every released change is recorded in the CHANGELOG.md.

Reach out to us

Have questions about how to use this library?

  • Reach out to our community forum: Contentful Community Forum
  • Jump into our community slack channel: Contentful Community Slack

You found a bug or want to propose a feature?

  • File an issue here on GitHub: File an issue. Make sure to remove any credential from your code before sharing it.

You need to share confidential information or have other questions?

  • File a support ticket at our Contentful Customer Support: File support ticket

Get involved

PRs Welcome

We appreciate any help on our repositories. For more details about how to contribute, see the contributing guide for our Swift SDKs.

Development setup

Development happens in Xcode on macOS, since iOS, macOS, tvOS, and watchOS all have to stay supported. Homebrew is a prerequisite.

make setup_env                      # Install or update the required brew packages.
bundle install                      # Install the Ruby gems used for linting, docs, and coverage.
carthage update --use-xcframeworks  # Resolve the test-only dependencies.
make open                           # Open ContentfulPersistence.xcworkspace.

Common tasks:

CommandPurpose
make testRun the test suite on macOS.
bundle exec fastlane test_iosRun the test suite on iOS (also test_macos, test_tvos).
bundle exec fastlane buildVerify the package builds with swift build.
make lintRun SwiftLint and the CocoaPods podspec linter.
make coverageGenerate a code-coverage report with Slather.
make carthageBuild the framework with Carthage across all platforms.

Pull requests are validated on CircleCI against Xcode 15.4.

License

This repository is published under the MIT license.

Code of Conduct

We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers.

Read our full Code of Conduct.