Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,19 @@
"sdk/javascript/reactions"
]
},
{
"group": "Offline Support",
"pages": [
"sdk/javascript/offline-overview",
"sdk/javascript/offline-enable",
"sdk/javascript/offline-cached-data",
"sdk/javascript/offline-send",
"sdk/javascript/offline-sync",
"sdk/javascript/offline-media-caching",
"sdk/javascript/offline-encryption",
"sdk/javascript/offline-reference"
]
},
"sdk/javascript/calling-overview",
{
"group": "Users",
Expand Down Expand Up @@ -3488,6 +3501,19 @@
"sdk/react-native/reactions"
]
},
{
"group": "Offline Support",
"pages": [
"sdk/react-native/offline-overview",
"sdk/react-native/offline-enable",
"sdk/react-native/offline-cached-data",
"sdk/react-native/offline-send",
"sdk/react-native/offline-sync",
"sdk/react-native/offline-media-caching",
"sdk/react-native/offline-encryption",
"sdk/react-native/offline-reference"
]
},
"sdk/react-native/calling-overview",
{
"group": "Users",
Expand Down Expand Up @@ -3789,6 +3815,19 @@
"sdk/ios/pin-save-message"
]
},
{
"group": "Offline Support",
"pages": [
"sdk/ios/offline-overview",
"sdk/ios/offline-enable",
"sdk/ios/offline-cached-data",
"sdk/ios/offline-send",
"sdk/ios/offline-sync",
"sdk/ios/offline-media-caching",
"sdk/ios/offline-encryption",
"sdk/ios/offline-reference"
]
},
"sdk/ios/calling-overview",
{
"group": "Users",
Expand Down
200 changes: 200 additions & 0 deletions sdk/ios/offline-cached-data.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: "Show Cached Data"
sidebarTitle: "Show Cached Data"
description: "Render cached messages, conversations, and lists instantly and reconcile them against the server with the CometChat iOS SDK."
---

{/* TL;DR for Agents and Quick Reference */}
<Accordion title="AI Integration Quick Reference">

```swift
// Every read fires up to two phases: cached first (instant), then server.
let request = MessagesRequest.MessageRequestBuilder()
.set(uid: "UID")
.set(limit: 30)
.build()

request.fetchPrevious(onCachedMessages: { messages in
// Phase 1: local cache. Fires first, even when empty. Render immediately.
}, onServerMessages: { messages in
// Phase 2: server. Replaces the same slice — reconcile, don't append. Never fires offline.
}, onError: { error in
// Offline ends here, after the cached page, with ERROR_INTERNET_UNAVAILABLE.
})

// Single items (additive two-phase overloads)
CometChat.getConversation(conversationWith: "UID", conversationType: .user, listener: conversationDetailsListener)
CometChat.getMessageDetails(messageId, onCachedMessage: { _ in }, onServerMessage: { _ in }, onError: { _ in })
```

</Accordion>

With offline support enabled, every list and lookup the SDK already offers answers from the on-device cache first and then from the server. Your UI paints instantly from the cache, then reconciles when the server replies. This page describes that contract, the reads it applies to, and how to merge the two phases.

## The two-phase contract

Each read fires **up to two** callbacks, always in this order:

<Steps>
<Step title="Cached phase">
The SDK delivers a page from the local cache **first**. It fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away.
</Step>
<Step title="Server phase">
After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it.
</Step>
</Steps>

Two rules follow from this:

- **Offline, the read ends with an error.** After the cached page, a read whose server phase cannot be produced because the device is offline settles with `onError` carrying `CometChatConstants.Errors.ERROR_INTERNET_UNAVAILABLE`. The call always settles — it never hangs.
- **The server phase never fires offline.** It fires only on a genuine server response. Never treat a cached page as "end of history"; an empty cached page only means the end of the *cached* window.

All callbacks are delivered on the main thread.

<Note>
With offline support **off**, these overloads behave like the classic call: the cached phase never fires, and the server result arrives in `onServer…` instead of `onSuccess`. Nothing changes until you enable offline (see [Enable Offline Support](/sdk/ios/offline-enable)). Requires SDK **4.3.0** or later.
</Note>

<Note>
A second fetch on the **same request object** while its previous fetch is still in flight settles immediately with `ERROR_REQUEST_IN_PROGRESS` and fires no cached phase. Wait for the first fetch to finish, or create a new request.
</Note>

## Read messages

`MessagesRequest` supports both directions. `fetchPrevious` pages toward older messages; `fetchNext` pages toward newer ones. Both have a two-phase overload.

```swift
let request = MessagesRequest.MessageRequestBuilder()
.set(uid: "UID")
.set(limit: 30)
.build()

request.fetchPrevious(onCachedMessages: { messages in
// Render the cached page immediately.
}, onServerMessages: { messages in
// Reconcile this slice against the cached page, keyed by message id.
}, onError: { error in
print("Fetch failed: \(error?.errorCode ?? "")")
})
```

The classic `fetchPrevious(onSuccess:onError:)` and `fetchNext(onSuccess:onError:)` are unchanged and never deliver a cached result.

## Read conversations

`ConversationRequest` pages in one direction with `fetchNext`.

```swift
let request = ConversationRequest.ConversationRequestBuilder(limit: 30).build()

request.fetchNext(onCachedConversations: { conversations in
// Render the cached list immediately.
}, onServerConversations: { conversations in
// Reconcile, keyed by conversation id.
}, onError: { error in
print("Fetch failed: \(error?.errorCode ?? "")")
})
```

<Tip>
Conversations deleted on the server are removed from the cache only by this two-phase overload. The classic `fetchNext(onSuccess:onError:)` updates the cache but does not remove them.
</Tip>

## Read the other lists

Every remaining list read gains the same two-phase overload, with `onCached…` / `onServer…` closures of the matching type plus `onError`.

| Request | Fetch method(s) | Cached / server closure labels |
|---|---|---|
| `MessagesRequest` | `fetchPrevious`, `fetchNext` | `onCachedMessages` / `onServerMessages` |
| `ConversationRequest` | `fetchNext` | `onCachedConversations` / `onServerConversations` |
| `ThreadsRequest` | `fetchNext` | `onCachedThreads` / `onServerThreads` |
| `UsersRequest` | `fetchNext` | `onCachedUsers` / `onServerUsers` |
| `BlockedUserRequest` | `fetchNext` | `onCachedUsers` / `onServerUsers` |
| `GroupsRequest` | `fetchNext` | `onCachedGroups` / `onServerGroups` |
| `GroupMembersRequest` | `fetchNext` | `onCachedGroupMembers` / `onServerGroupMembers` |
| `BannedGroupMembersRequest` | `fetchNext` | `onCachedBannedGroupMembers` / `onServerBannedGroupMembers` |
| `ReactionsRequest` | `fetchPrevious`, `fetchNext` | `onCachedReactions` / `onServerReactions` |
| `NotificationFeedRequest` | `fetchNext` | `onCachedNotificationFeedItems` / `onServerNotificationFeedItems` |

The call shape is identical to the conversations example above — swap the request and the closure labels.

```swift
let request = UsersRequest.UsersRequestBuilder()
.set(limit: 30)
.build()

request.fetchNext(onCachedUsers: { users in
// render
}, onServerUsers: { users in
// reconcile, keyed by uid
}, onError: { error in
print("Fetch failed: \(error?.errorCode ?? "")")
})
```

<Note>
For `ReactionsRequest`, `fetchPrevious` and `fetchNext` are recorded and replayed independently — each direction keeps its own cached pages. Users, groups, members, banned members, reactions, and the notification feed are served from a per-request snapshot rather than a shared store: the cached phase replays what the server returned online for the **same builder settings**, so a list you never fetched online has an empty cached page. Per-builder details are in the [Configuration & API Reference](/sdk/ios/offline-reference).
</Note>

## Read a single item

`getConversation` and `getMessageDetails` each add a two-phase overload. Unlike the list reads, the cached callback fires **only when the item is cached** — never with an empty or nil object. A cache miss goes straight to the server phase online, or straight to `onError` offline.

`getConversation` takes a `ConversationDetailsListener`; `getMessageDetails` takes closures.

```swift
final class ConversationLoader: NSObject, ConversationDetailsListener {

func onCachedConversation(_ conversation: Conversation) {
// Fires only on a cache hit.
}

func onServerConversation(_ conversation: Conversation?) {
// Server copy, written through before delivery.
}

func onError(_ error: CometChatException?) {
print("Lookup failed: \(error?.errorCode ?? "")")
}
}

let loader = ConversationLoader()
CometChat.getConversation(conversationWith: "UID", conversationType: .user, listener: loader)

CometChat.getMessageDetails(messageId, onCachedMessage: { message in
// cache hit only
}, onServerMessage: { message in
// server copy
}, onError: { error in
print("Lookup failed: \(error?.errorCode ?? "")")
})
```

<Note>
The SDK holds the `ConversationDetailsListener` until the read ends, so you do not need to keep your own reference for a single lookup. The older `getConversation(conversationWith:conversationType:onCachedConversation:onServerConversation:onError:)` closure overload still works but is deprecated in favour of the listener form.
</Note>

## Merge the two phases

The server phase **replaces the same slice** — it is a reconcile, not a second batch. Render the cached page straight away, then on the server phase diff it into your displayed list keyed by the item's stable id (message id, conversation id, uid, guid). Do not append the server page to the cached one.

<Tip>
Key your list by id so the two phases merge cleanly: update items that changed, add new ones, and drop cached items the server slice omits. To page further back, issue the next `fetchPrevious` / `fetchNext` rather than treating the server phase as more pages.
</Tip>

## When the cached page is empty

The cache answers a read **only if every filter on it is locally computable**. A single server-only predicate makes the entire cached phase return an empty list — with no error and no warning — while the server phase still answers in full. The guiding rule is *better empty than wrong*.

The most common server-only predicate is `set(searchKeyword:)`, which blanks the cached phase on every request. Other examples on `MessagesRequest` are `hasLinks`, `updatesOnly(onlyUpdates:)` and `setInteractionGoalCompletedOnly(_:)`. On the entity lists, a keyword search or a presence filter (`UsersRequest.set(status:)`, `GroupMembersRequest.set(status:)`) is never snapshotted, so it has no cached phase either.

<Warning>
An offline-critical screen must use only cache-supported filters — one innocuous builder flag can silently disable the cached phase for a whole screen. The full per-builder matrix of what the cache answers lives in the [Configuration & API Reference](/sdk/ios/offline-reference).
</Warning>

## Known limitations

- A `MessagesRequest` with no user or group set is not fully served from the cache while offline, and its filters may not be applied to cached results.
- With offline on, `NotificationFeedRequest.fetchNext` past the last page returns an empty list without asking the server; create a new request to pick up items that arrive later.
- `ReactionsRequest` past its last page asks the server again and can return reactions it already returned — remove duplicates by id.
113 changes: 113 additions & 0 deletions sdk/ios/offline-enable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: "Enable Offline Support"
sidebarTitle: "Enable Offline Support"
description: "Turn on local caching and the durable send outbox, and verify offline support is active with the CometChat iOS SDK."
---

{/* TL;DR for Agents and Quick Reference */}
<Accordion title="AI Integration Quick Reference">

```swift
// 1. Build an OfflineConfig (off by default — call setEnabled(true)).
let offlineConfig = OfflineConfig.Builder()
.setEnabled(true)
.build()
// Swift shorthand: let offlineConfig = OfflineConfig(enabled: true)

// 2. Attach it to AppSettings.
let appSettings = AppSettings.AppSettingsBuilder()
.setRegion(region: "REGION")
.setOfflineConfig(offlineConfig)
.build()

// 3. Pass the settings to init.
CometChat.init(appId: "APP_ID", appSettings: appSettings, onSuccess: { _ in }, onError: { _ in })

// 4. Verify it is active for the logged-in user.
let active = CometChat.isOfflineEnabled() // Bool
```

</Accordion>

Offline support is **off by default**. You turn it on once, at initialization, by attaching an `OfflineConfig` to your `AppSettings`. Until you do, the SDK behaves exactly as before. This page covers the minimal setup; for every tunable option see the [Configuration & API Reference](/sdk/ios/offline-reference).

<Steps>

<Step title="Requirements">
Offline support requires CometChat iOS SDK **4.3.0** or later. Update the dependency:

{/* TODO(version): 4.3.0 is unreleased in the SDK changelog. Confirm the published pod/SPM version and whether a pre-release qualifier is needed. */}
<Tabs>
<Tab title="CocoaPods">
```ruby
pod 'CometChatSDK', '4.3.0'
```
</Tab>
<Tab title="Swift Package Manager">
Set the `https://github.com/cometchat/chat-sdk-ios.git` dependency rule to `Up to Next Major Version` with version `4.3.0`.
</Tab>
</Tabs>

If you are new to the SDK, complete the [standard setup](/sdk/ios/setup) first; the steps below only add offline configuration to your existing `init`.
</Step>

<Step title="Build an OfflineConfig">
Create an `OfflineConfig` and enable it. Every other field has a sensible default, so set only what you need. Swift can use the initializer with default arguments; the `Builder` works from both Swift and Objective-C.

Check warning on line 55 in sdk/ios/offline-enable.mdx

View check run for this annotation

Mintlify / Mintlify Validation (cometchat-22654f5b) - vale-spellcheck

sdk/ios/offline-enable.mdx#L55

Did you really mean 'initializer'?

<Tabs>
<Tab title="Initializer">
```swift
let offlineConfig = OfflineConfig(enabled: true)

Check warning on line 60 in sdk/ios/offline-enable.mdx

View check run for this annotation

Mintlify / Mintlify Validation (cometchat-22654f5b) - vale-spellcheck

sdk/ios/offline-enable.mdx#L60

Did you really mean 'offlineConfig'?
```
</Tab>
<Tab title="Builder">
```swift
let offlineConfig = OfflineConfig.Builder()

Check warning on line 65 in sdk/ios/offline-enable.mdx

View check run for this annotation

Mintlify / Mintlify Validation (cometchat-22654f5b) - vale-spellcheck

sdk/ios/offline-enable.mdx#L65

Did you really mean 'offlineConfig'?
.setEnabled(true)
.build()
```
</Tab>
</Tabs>
</Step>

<Step title="Attach it to AppSettings">
Pass the config to `AppSettingsBuilder.setOfflineConfig(_:)`. Without it, `AppSettings` keeps `OfflineConfig.default`, which has offline switched off.

```swift
let appSettings = AppSettings.AppSettingsBuilder()
.setRegion(region: "REGION")
.setOfflineConfig(offlineConfig)
.build()
```
</Step>

<Step title="Initialize the SDK">
Pass the settings to `CometChat.init` as you do today. No other call changes.

```swift
CometChat.init(appId: "APP_ID", appSettings: appSettings, onSuccess: { isSuccess in
print("Initialization completed successfully")
}, onError: { error in
print("Initialization failed: \(error.errorDescription)")
})
```
</Step>

<Step title="Verify it is active">
Call `CometChat.isOfflineEnabled()`. It returns `true` only while offline support is actually available to the logged-in user: your `OfflineConfig` turns it on, CometChat has not switched offline off for your app, and the per-user cache is open. So it is `false` before anyone logs in and after logout, even when you enabled it in `AppSettings`. Read it at the moment of each decision and keep no copy of the result.

```swift
let active = CometChat.isOfflineEnabled()
print("Offline support active: \(active)")
```

<Note>
It is also `false` while the cache cannot run encrypted on this device and you chose `.onlineOnly` (see [Encrypt the Cache](/sdk/ios/offline-encryption)). It is already `true` by the time any `onLoginSuccess` observer runs.
</Note>
</Step>

</Steps>

## Next steps

Offline support is now active for the logged-in user. Continue with [Show Cached Data](/sdk/ios/offline-cached-data) to adopt the two-phase read overloads, and [Send While Offline](/sdk/ios/offline-send) for the durable outbox. Tune caps, media, and encryption from the [Configuration & API Reference](/sdk/ios/offline-reference).
Loading
Loading