react-native-icloud-kit
v0.1.6
Published
CloudKit and NSUbiquitousKeyValueStore for React Native. Save/query/batch CKRecords, key-value sync, and iCloud entitlements via Expo config plugin. iOS only. Built with Expo Modules API.
Maintainers
Readme
react-native-icloud-kit
CloudKit and NSUbiquitousKeyValueStore for React Native. iOS only, built with Expo Modules API.
- CloudKit (
iCloud): Save, query, batch save, and delete records in the user's private CloudKit database. Automatic pagination, chunked batch uploads with retry, and typed error handling. - Key-Value Store (
iCloudKVS): Read and write small string values viaNSUbiquitousKeyValueStore-- automatically synced across all of the user's devices. - Expo Config Plugin: Automatically configures iCloud entitlements, CloudKit services, and KVS identifiers at build time. No manual Xcode setup.
Installation
npm install react-native-icloud-kit
# or
yarn add react-native-icloud-kit
# or
bun add react-native-icloud-kitThen install the native pods:
npx pod-installPrerequisites
- Expo >= 51.0.0 (uses Expo Modules API)
- React Native >= 0.74.0
- iOS only -- all methods return safe no-ops or throw on Android
- An Apple Developer account with an iCloud container configured
Setup
1. Configure the Expo plugin
Add the plugin to your app.json (or app.config.js) with your iCloud container identifier:
{
"expo": {
"plugins": [
[
"react-native-icloud-kit/plugin/withICloud",
{
"containerIdentifier": "iCloud.com.yourcompany.yourapp"
}
]
]
}
}This automatically:
- Adds
com.apple.developer.icloud-container-identifiersto your entitlements - Enables
CloudKitincom.apple.developer.icloud-services - Sets up the KVS ubiquity identifier (
com.apple.developer.ubiquity-kvstore-identifier) - Writes the container ID to
Info.plistso the Swift module can read it at runtime
2. Create the iCloud container in Apple Developer Portal
- Go to Certificates, Identifiers & Profiles
- Under Identifiers, select iCloud Containers
- Click + and create a container matching your
containerIdentifier(e.g.,iCloud.com.yourcompany.yourapp) - Go to your App ID and enable the iCloud capability, then associate it with the container
3. Rebuild
npx expo prebuild --clean
npx expo run:iosNote: The container identifier does NOT need to match your bundle ID. For example, your app can be
com.yourcompany.yourappwhile your container isiCloud.com.yourcompany.differentname.
API Reference
The library exports two objects: iCloud (CloudKit) and iCloudKVS (Key-Value Store).
import { iCloud, iCloudKVS } from 'react-native-icloud-kit';Types
type FieldValue = string | number | null;
type Fields = Record<string, FieldValue>;
interface CloudKitRecord {
recordId: string;
fields: Fields;
}
interface BatchRecord {
fields: Fields;
recordId?: string; // auto-generated UUID if omitted
}iCloud.isAvailable()
Check if the user is signed into iCloud.
const available = await iCloud.isAvailable();
// true if signed in, false otherwise
// Always returns false on AndroidiCloud.getUserRecordID()
Get the current user's CloudKit record ID. This is a container-scoped identifier — the same iCloud account gets a different record ID in each app's CloudKit container. Useful for diagnostics, user attribution, or generating a stable per-app identity token.
try {
const recordID = await iCloud.getUserRecordID();
console.log('User record:', recordID);
// e.g., "_abc123def456..."
} catch (error) {
// Throws if iCloud is not available
}iCloud.save(recordType, fields, recordId?)
Save a single record to the user's private CloudKit database. Creates a new record, or overwrites an existing one if a record with the same recordId already exists.
| Parameter | Type | Description |
|---|---|---|
| recordType | string | The CloudKit record type (e.g., "GameSession") |
| fields | Fields | Key-value pairs for the record |
| recordId | string? | Optional deterministic ID. Auto-generated UUID if omitted |
Returns the saved record's ID.
const id = await iCloud.save('GameSession', {
playerName: 'Alice',
score: 42,
datePlayed: Date.now(),
});
console.log('Saved record:', id);Deterministic IDs allow idempotent saves -- if you save with the same recordId twice, the second save overwrites the first instead of creating a duplicate:
const deterministicId = `session-${session.date}-${session.score}`;
await iCloud.save('GameSession', fields, deterministicId);
// Safe to call again -- same ID means same record is updatediCloud.query(recordType, predicate?, limit?)
Query records from the private CloudKit database. Supports filtering with NSPredicate syntax and automatic cursor-based pagination.
| Parameter | Type | Description |
|---|---|---|
| recordType | string | The CloudKit record type to query |
| predicate | string? | Optional NSPredicate format string. Defaults to all records |
| limit | number? | Max records to return. Defaults to all (paginated in batches of 200) |
Returns an array of CloudKitRecord objects.
// Fetch all records of a type
const all = await iCloud.query('GameSession');
// Filter with NSPredicate
const hard = await iCloud.query('GameSession', 'nValue >= 3');
// Limit results
const recent = await iCloud.query('GameSession', undefined, 10);Important: For queries to work, the fields you filter on must be marked as QUERYABLE in CloudKit Dashboard. At minimum, mark
recordNameas QUERYABLE for each record type.
iCloud.batchSave(recordType, records)
Save multiple records in a single operation. Automatically handles CloudKit's per-request limits by chunking into batches of 400 and retrying with smaller batches if the server returns limitExceeded.
| Parameter | Type | Description |
|---|---|---|
| recordType | string | The CloudKit record type |
| records | BatchRecord[] | Array of records with fields and optional recordId |
Returns the count of successfully saved records.
const records = sessions.map(s => ({
fields: { score: s.score, date: s.date },
recordId: `session-${s.id}`, // optional deterministic ID
}));
const savedCount = await iCloud.batchSave('GameSession', records);
console.log(`Saved ${savedCount} of ${records.length} records`);Retry behavior:
- Starts with chunks of 400 records
- If CloudKit returns
limitExceeded, halves the chunk size and retries - Individual records that fail are retried up to 2 times before being skipped
- Returns the total count of successfully saved records
iCloud.delete(recordType, recordId)
Delete a single record by its ID.
| Parameter | Type | Description |
|---|---|---|
| recordType | string | The CloudKit record type |
| recordId | string | The record ID to delete |
Returns true on success.
await iCloud.delete('GameSession', 'session-123');iCloud.deleteAll()
Delete ALL records from the CloudKit private database by deleting the custom record zone. This is the most efficient way to wipe all data -- a single server round-trip regardless of record count. The zone is automatically recreated on the next save, query, or batchSave call.
Returns true on success.
await iCloud.deleteAll();
// All records in the private database are now gone.
// The zone will be recreated automatically on next use.Warning: This is irreversible. All records of all types within the zone are permanently deleted.
iCloudKVS.set(key, value)
Write a string value to NSUbiquitousKeyValueStore. The value is automatically synced across all of the user's devices via iCloud.
| Parameter | Type | Description |
|---|---|---|
| key | string | The key to store under |
| value | string | The string value to store. Use JSON.stringify() for objects |
// Simple string
await iCloudKVS.set('username', 'Alice');
// Complex object as JSON
const config = { theme: 'dark', level: 5 };
await iCloudKVS.set('app_config', JSON.stringify(config));Limits: NSUbiquitousKeyValueStore allows up to 1 MB total storage and 1024 keys. Individual values should be kept small.
iCloudKVS.get(key)
Read a string value from NSUbiquitousKeyValueStore.
| Parameter | Type | Description |
|---|---|---|
| key | string | The key to read |
Returns the stored string, or null if the key doesn't exist. Returns null on Android.
const value = await iCloudKVS.get('app_config');
if (value) {
const config = JSON.parse(value);
console.log('Theme:', config.theme);
}iCloudKVS.remove(key)
Remove a key from NSUbiquitousKeyValueStore. The removal is synced across all of the user's devices.
| Parameter | Type | Description |
|---|---|---|
| key | string | The key to remove |
await iCloudKVS.remove('app_config');Error Handling
CloudKit errors are mapped to typed exceptions:
| Error | Cause |
|---|---|
| ICloudNotAvailableException | User is not signed into iCloud |
| ICloudQuotaExceededException | iCloud storage is full |
| ICloudNetworkException | Network unavailable or connection failed |
| ICloudRecordNotFoundException | Record ID does not exist |
| ICloudRateLimitedException | Too many requests; includes retry-after interval |
| ICloudException | Any other CloudKit error |
try {
await iCloud.save('MyRecord', { key: 'value' });
} catch (error) {
// error.message contains the specific reason
console.error('CloudKit error:', error.message);
}Architecture Notes
- All CloudKit operations use the private database with a custom record zone (
RNICloudKitZone). The zone is created automatically on the first operation and cached viaUserDefaultsto avoid redundant network calls. - Operations run with
.userInitiatedQoS (Apple's default for CloudKit is low priority). - The
savefunction usessavePolicy: .allKeys, which means all fields are written on every save. This makes deterministic IDs safe for overwrites -- the entire record is replaced, not merged. - Queries paginate in batches of 200 (CloudKit's recommended page size) using cursor-based pagination.
- Android:
iCloud.isAvailable()returnsfalse,iCloudKVS.get()returnsnull. All other methods throw with "iCloud is only available on iOS".
License
MIT
