Skip to main content
You can react to changes in your local store by setting up a store observer. You can read more about store observers in Accessing Data. Store observers are useful when you want to monitor changes from your local Ditto store and react to them immediately. For instance, when your end user updates their profile, asynchronously display changes in realtime.

Setting Up Store Observers

Using the registerObserver method, set up an observer within the store namespace enclosed with a query that specifies the collection to watch for changes, as well as your logic to handle the incoming changes.

Store Observer with Query Arguments

To associate arguments with your query add them as a parameter.

let observer = ditto.store.registerObserver(
  query: "SELECT * FROM cars WHERE color = :color",
  arguments: [ "color": "blue" ]){ result in /* handle change */ };

Canceling a Store Observer

To cancel a store observer, call cancel on the observer object. Once canceled, the store observer will stop processing in the background and will no longer call the provided callback.

observer.cancel()

Accessing Store Observers

To access store observers from the local Ditto store:

ditto.store.observers

Diffing Results

This feature is available in v4.11 and above.
There are some use cases where it may be required to calculate the difference between previous and current query results. For example, when you want to update a UI component or external HTTP system with the latest data, you may want to know which items have been added, removed, or changed since the last time you received the query result. The Ditto SDK provides a DittoDiffer class that allows you to opt-in to diffing only when necessary, thereby avoiding unnecessary performance and memory costs. However, diffing is computationally expensive. It requires storing the previous query results in memory, leading to increased RAM usage, which can be particularly problematic for applications handling large datasets or frequent updates. Instead of computing diffs synchronously with every store observer update, it is recommended to debounce diffing, processing changes out-of-band at a cadence that best suits your use case.
Critical Memory Management: QueryResults and QueryResultItems should be treated like database cursors. Always extract the data you need immediately and then close/dematerialize them. Never store QueryResultItems directly between observer emissions as this will cause memory bloat and potential crashes.
A DittoDiffer is given query result items and compares them to the previous set of items it has received:
let differ = DittoDiffer()
var previousDocumentIds: [String] = [] // Store only extracted IDs

let observer = ditto.store.registerObserver(
  query: "SELECT * FROM cars") { queryResult in
    let diff = differ.diff(queryResult.items)

    // Extract current document IDs and dematerialize items
    let currentDocumentIds = queryResult.items.map { item in
        let id = item.value["_id"] as? String ?? "unknown"
        item.dematerialize() // Release memory after extracting data
        return id
    }

    // Handle deletions using stored IDs from previous emission
    for index in diff.deletions {
        let deletedId = previousDocumentIds[index]
        print("Deleted car with ID: \(deletedId)")
    }

    // Handle insertions using current IDs
    for index in diff.insertions {
        let insertedId = currentDocumentIds[index]
        print("Inserted car with ID: \(insertedId)")
    }

    // Handle updates using current IDs
    for index in diff.updates {
        let updatedId = currentDocumentIds[index]
        print("Updated car with ID: \(updatedId)")
    }

    // Store only the document IDs for next callback - no live references!
    previousDocumentIds = currentDocumentIds
}
The diff() method returns a DittoDiff containing:
  • insertions: Indexes of new items in the new array
  • deletions: Indexes of items that were removed from the old array
  • updates: Indexes of items in the new array that were present in the old array and whose value has changed
  • moves: Pairs of indexes showing items that changed position

Example: Apple UIKit

Use this diff to update a UIKit class, such as UITableView, UICollectionView :
Swift
let collectionView = /* assuming a UICollectionView exists */
let differ = DittoDiffer()
ditto.store.registerObserver("...") { queryResult in

    /* ... Update data source content based on query result ... */

    let diff = differ.diff(queryResult.items)
    tableView.performBatchUpdates({
        let insertions = diff.insertions.map { IndexPath(item: $0, section: 0) }
        let deletions = diff.deletions.map { IndexPath(item: $0, section: 0) }
        let reloads = diff.updates.map { IndexPath(item: $0, section: 0) }

        collectionView.insertItems(at: insertions)
        collectionView.deleteItems(at: deletions)
        collectionView.reloadItems(at: reloads)

        collectionView.moves.map { move in
            let (from, to) = move
            collectionView.move(at: from, to: to)
        }
    }, completion: nil)
}

Key considerations

  • Keep references to arrays yourself: The differ doesn’t provide access to the old and new items themselves, so they need to be retained by the user if needed.
  • No comparison of document metadata: The differ performs a deep comparison of the value of each query result item but doesn’t take into account any metadata. Applying a series of changes to a document will not cause it to show up in updated unless those changes result in a different value from the initial state to the final state.
  • No async diffing: Computing a diff on a set of many query result items or very large query result items might block the device depending on its hardware.

Migrating from Live Query Events

If you’re using the legacy live query API that automatically provides diff information through event parameters, you can migrate to the newer store observer pattern with the DittoDiffer class. The differ provides equivalent functionality but requires manual management of previous results. For a complete migration guide, see Replacing Live Query Events with Store Observers.