Zephyr syncs selected UserDefaults over iCloud, key by key
Effortlessly synchronize UserDefaults over iCloud.
At a glance
- What is it?
- Zephyr is a small Swift package that bridges an app's UserDefaults to the iCloud key-value store, and its interesting part is the conflict model. Version 5 compares a generation per key rather than one timestamp for the whole store, so a local edit is not silently reverted by the second sync, and a key that is missing is never treated as deleted.
- Who is it for?
- Zephyr suits an iOS or macOS app with a small amount of preference state that should follow the user between devices, and it suits a team that has been bitten by the whole-store last-writer-wins behaviour of the underlying API.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 68 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A bridge between UserDefaults and the iCloud key-value store
Zephyr synchronizes specific keys, or an app's persistent UserDefaults domain, over iCloud using NSUbiquitousKeyValueStore, and it is a package rather than a service, which is the main reason it is interesting. The underlying Apple API is deliberately limited, it is a small key-value store intended for preferences, and it is last-writer-wins across devices. The problem Zephyr addresses is that the obvious implementation of it in an app, syncing the whole persistent domain on every change, produces a specific and confusing failure, where a local edit made on one device is reverted by the next sync on another. The package therefore exposes a narrow interface: you ask it to sync, either the whole persistent domain or a named list of keys, and it handles the reading and the writing. The requirement list is short and modern, Swift 5.9 or later and iOS 17, tvOS 17, watchOS 10 or macOS 14, and the project is MIT licensed and maintained by Arthur Ariel Sabintsev.
Version 5 is a rewrite of the conflict model
The changelog note explains what changed and it is the most useful paragraph in the repository. Version 4 used one date for the entire store and could revert local edits on the second sync call. Version 5 compares a generation per key, never treats a missing value as a deleted one, and only syncs the app persistent domain rather than picking one winner for the whole store. The phrase logical generations is the key, since a generation is a per-key counter that advances when the key is written, and comparing them is what tells the package which side actually changed rather than which side happened to be written last. The deletion rule matters just as much, because treating a key that is absent as deleted is how a sync implementation erases a setting that a device simply has not fetched yet.
The mismatch policy, and the quota you cannot change
The readme is also clear about what the package does not fix. iCloud still enforces a one megabyte and 1024 key quota on the key-value store, and two devices writing the same key without a generation bump are resolved by the mismatch policy, where a call to `sync()` prefers the local value and an inbound iCloud event prefers the remote one. Underneath all of that, Apple's store remains last-writer-wins at the transport layer. So the package improves the common case, where a value is written on one device and the other has not touched it, and leaves the genuinely concurrent case to the application. The quota is the other hard boundary, and it is the reason the per-key API is worth having, since a syncing app that used the whole persistent domain would spend the quota on values the user never expected to travel.
Registering defaults is an ordering problem
There is one instruction in the setup section that is easy to skip and expensive to get wrong. Any default values must be registered before the first call to Zephyr, using the standard `register(defaults:)` call, and registered defaults are not uploaded, only the keys you actually write into the persistent domain are. The consequence follows from the order of operations, since a value that was never written is a value that does not exist in the store, and a key that does not exist in the store is indistinguishable from a key that was deleted unless the generation scheme says otherwise. The second half of the contract is about visibility. `Zephyr.sync()` returns after any pulled values have been written, so a read on the next line sees the merged result, which is a small but important guarantee, because the alternative is a completion handler that fires at a moment the caller cannot reason about.
Monitoring is opt-in, and an empty set is not a wildcard
Background monitoring is a listed feature, and the API for it is two calls, one to add keys to be monitored and one to remove them. Dotted key names are supported, so a value like `com.example.theme` can be monitored as it is. The rule that matters is what happens to the rest of the store. Inbound iCloud events apply only to the monitored keys, removing a key from the monitored set stops inbound writes for it, and an empty monitored set does not fall through to the whole persistent domain. That last clause is a safety property rather than a detail, since the intuitive behaviour for an empty set would be to monitor everything, and doing that would quietly reintroduce the whole-store behaviour the package exists to avoid.
Change notifications, the synchronize toggle and app groups
When a key changes in the cloud, the package posts a notification on the main queue, and the example registers an observer for `Zephyr.keysDidChangeOnCloudNotification` and reads the changed keys out of the user info under `Zephyr.changedKeysUserInfoKey`, so a UI can refresh only what moved. There is a separate toggle, `Zephyr.syncUbiquitousKeyValueStoreOnChange`, which is true by default and controls whether the underlying `synchronize()` is called on the store after a batch, and a debug flag, `Zephyr.debugEnabled`, that logs what it is doing. App groups are supported as well, so a suite created with `UserDefaults(suiteName:)` can be handed to Zephyr with `setUserDefaultsSuite(to:suite, suiteName:)`, which is how an extension and its host app share the same synced state. The README also points at the changelog for the full history of what changed in version 5.
Installing it, and the Xcode setting that is easy to miss
The package is available through Swift Package Manager, and the declaration is the one you would expect:
.package(url: "https://github.com/ArtSabintsev/Zephyr.git", from: "5.0.1")There is a manual route as well, copying the files in `Sources/` into the project, which is the way to use it without adding a package dependency. The Xcode configuration is the step that catches people out. You go to the project, then the target, then Signing and Capabilities, then iCloud, and you enable key-value storage, and you repeat that for every target that syncs. Missing it on one target means that target silently never syncs. The repository holds the package manifest, the `Sources/` directory, the `Tests/` directory, the `Assets/` folder with the Xcode settings screenshot, a swiftlint configuration, and a `.swiftpm/` directory, and the release history shows the modernising of the package for SwiftPM and iOS 17 in version 4.0.0 in July 2026, followed by 5.0.0 and 5.0.1 on the same day in August 2026.
Editorial conclusion
Zephyr suits an iOS or macOS app with a small amount of preference state that should follow the user between devices, and it suits a team that has been bitten by the whole-store last-writer-wins behaviour of the underlying API. It is a poor fit if you want to sync a lot of state, since the iCloud key-value store enforces a one megabyte and 1024 key quota that the package cannot lift, and it is a poor fit if you need conflict-free merging, because the transport underneath is still last-writer-wins. Before you adopt it, check three things: which platform versions you support, since it asks for Swift 5.9 and iOS 17, tvOS 17, watchOS 10 or macOS 14, that every target that syncs has the iCloud key-value storage capability enabled in Xcode, and that you register your default values before the first call, because registered defaults are never uploaded and only the keys you actually write are.
Frequently asked questions
What does Zephyr do?
It synchronizes specific keys, or an app's persistent UserDefaults domain, over iCloud using NSUbiquitousKeyValueStore. It is a Swift package, MIT licensed, requiring Swift 5.9 and iOS 17, tvOS 17, watchOS 10 or macOS 14.
What changed in Zephyr 5.0?
Version 4 used one date for the entire store and could revert local edits on the second sync call. Version 5 compares a generation per key, never treats a missing value as a deleted one, and only syncs the app persistent domain, so it does not pick one winner for the whole store.
How do I sync a single key with Zephyr?
Call `Zephyr.sync(keys:)` with the key names, either as variadic strings or as an array, and call `Zephyr.sync()` with no argument to sync the whole app persistent domain. The call returns after any pulled values have been written, so a read on the next line sees the merged result.
Why should I register my default values before using Zephyr?
Because registered defaults are not uploaded, only the keys you actually write into the persistent domain are. The documentation asks you to call `register(defaults:)` before the first Zephyr call.
What are the limits of Zephyr?
The iCloud key-value store still enforces a one megabyte and 1024 key quota that the package cannot lift, and it remains last-writer-wins at the transport layer, so two devices writing the same key without a generation bump are resolved by a mismatch policy where sync prefers the local value and an inbound event prefers the remote one. Releases 5.0.0 and 5.0.1 both shipped on 2026-08-16.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/artsabintsev-zephyr)Was this useful?