Zephyr

August 16, 2026 · View on GitHub

Sync UserDefaults over iCloud

CI Swift Support Platforms SwiftPM Compatible


Zephyr synchronizes specific keys and/or your app's persistent UserDefaults domain over iCloud using NSUbiquitousKeyValueStore.

Version 5 merges per key with logical generations. It does not pick one winner for the whole store. It does not upload AppleLanguages or other system defaults.

Requires Swift 5.9+, iOS 17, tvOS 17, watchOS 10, or macOS 14.

Features

  • Swift Package Manager
  • Sync specific keys or the app persistent domain
  • Background monitoring (including dotted keys such as com.example.theme)
  • Per-key conflict resolution and tombstoned deletes
  • Debug logging

Installation

.package(url: "https://github.com/ArtSabintsev/Zephyr.git", from: "5.0.1")

Manual

Copy the files in Sources/ into your project.

Setup

Turn on iCloud Key-Value storage in Xcode

  • Project → Target → Signing & Capabilities → iCloud
  • Enable Key-value storage
  • Repeat for every target that syncs

How to turn on iCloud Key Value Store Syncing

Integrate Zephyr

Register any default values before the first Zephyr call, using register(defaults:). Registered defaults are not uploaded; only keys you actually write into the persistent domain are.

Zephyr.sync() returns after any pulled values have been written. A read on the next line sees the merged result.

Sync the app persistent domain

Zephyr.sync()

Sync specific keys

Zephyr.sync(keys: "MyFirstKey", "MySecondKey")
Zephyr.sync(keys: ["MyFirstKey", "MySecondKey"])

Monitor keys (dotted names are fine)

Zephyr.addKeysToBeMonitored(keys: "MyFirstKey", "com.example.theme")
Zephyr.removeKeysFromBeingMonitored(keys: "MyFirstKey")

Inbound iCloud events apply only to monitored keys. removeKeysFromBeingMonitored stops inbound writes for those keys. An empty monitored set does not fall through to the whole persistent domain.

iCloud change notification (posted on the main queue)

NotificationCenter.default.addObserver(
    forName: Zephyr.keysDidChangeOnCloudNotification,
    object: nil,
    queue: .main
) { note in
    let keys = note.userInfo?[Zephyr.changedKeysUserInfoKey] as? [String] ?? []
    // refresh UI for `keys`
}

Calling NSUbiquitousKeyValueStore.synchronize() after a batch

Zephyr.syncUbiquitousKeyValueStoreOnChange = true  // default
Zephyr.syncUbiquitousKeyValueStoreOnChange = false

Debug logging

Zephyr.debugEnabled = true
Zephyr.sync()

App group suite

if let suite = UserDefaults(suiteName: "group.com.example.app-name") {
    Zephyr.setUserDefaultsSuite(to: suite, suiteName: "group.com.example.app-name")
}

What 5.0 changed

See CHANGELOG.md. Short version: 4.x used one date for the entire store and could revert local edits on the second sync(). 5.x compares a generation per key, never treats “missing” as “deleted,” and only syncs the app persistent domain.

iCloud still enforces a 1 MB / 1024-key quota on the key-value store. Two devices writing the same key without a generation bump are resolved by the mismatch policy (sync() prefers local; an inbound iCloud event prefers remote). Apple’s store remains last-writer-wins at the transport layer.

Created and maintained by

Arthur Ariel Sabintsev