Zephyr
August 16, 2026 · View on GitHub
Sync UserDefaults over iCloud
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

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.