My Eight Years with CloudKit: From Open Source IceCream to Commercial Apps

If you are a Realm developer, IceCream is almost certainly your go-to choice when looking for a CloudKit synchronization solution. Long before the birth of native CloudKit support for Core Data (NSPersistentCloudKitContainer), Cai Yue discovered CloudKit—a hidden treasure in the Apple ecosystem—thanks to his keen technical sense. Not only does it enable cloud synchronization at almost zero cost, but its authentication model integrated with iCloud also significantly lowers the barrier to entry while resolving user privacy concerns.

In his subsequent development career, Yue continued to explore what CloudKit could bring to applications. From MusicMate, a social app centered around music, to Setlists, a concert setlist sharing app that utilizes the CloudKit Public Database for cross-app data sharing, CloudKit has become an indispensable tool in his arsenal.

I specifically invited him to write this article to share his development journey, especially his practical experience, pitfalls, and advanced tips regarding CloudKit. I hope this will help more developers in the Apple ecosystem use CloudKit well and use it right.

My first contact with CloudKit actually dates back to 2017. At that time, I had just graduated and was working as an iOS engineer at Shanbay in Nanjing. That year, the wave of mobile internet was still surging. iOS, Android, and cross-platform development technologies were developing like wildfire, and various experts were active on platforms like Weibo sharing the latest technologies.

Although our iOS team at the company only had 5 people, the technical atmosphere was excellent. Our team lead told us: “We won’t write requirements on Friday afternoons. We’ll dedicate that time to exchanging and learning new iOS technologies. I’ll handle the unfinished requirements from the PM.”

As a fresh graduate, this was like rain after a long drought; I was like a sponge, eagerly looking forward to the team recharging time every week.

During one Friday sharing session, I showed off an iOS Side Project (a quantified goal app similar to a To-Do List called “Small Goal,” which has since been removed from the store) and demonstrated how I used CloudKit to silky-smoothly sync user local data.

After listening to my sharing, the team lead encouraged me: “You could extract the part that adds iCloud sync to local data with one click and make it into a dedicated open-source library. There should be a future in that.”

So, I spent about two weekends encapsulating this function and releasing it as open source on GitHub, naming it IceCream. The slogan at the time was roughly: “Add just one line of code to your project, and your Realm database can sync on iCloud.

After publishing the project, I posted a tweet about it.

image-20251213155006280

Boy, was I in for a surprise. After posting, it suddenly spread across the internet. Many developers starred my project, and that tweet was even retweeted by Peter Steinberger. For a time, everyone was discussing IceCream. The project quickly garnered hundreds and then thousands of organic and healthy stars, and people submitted many PRs. It was from this moment that I began to be known by more developers. (I still remember opening my computer in my rental apartment late at night after work, seeing the project being retweeted and discussed, and my hands trembling as I typed. Laughs)

Gradually, IceCream was used by more and more developers in their personal or even company Apps. During that time, I was very active, spending almost all my spare time replying to emails and maintaining the project. The climax of the story happened around September 2018, when I received an email from Cupertino—IceCream had received recognition from the official Apple CloudKit team.

image-20251213155601403

This even brought me opportunities. In 2019, taking advantage of a company assignment to San Jose for WWDC, I had a very pleasant face-to-face exchange with members of the CloudKit team.

image-20251213155654615

The IceCream project has been maintained in my spare time since its inception (the update frequency has dropped in recent years, but if there is still a high demand, I will pick it up and maintain it again 😄). During this process, I gained a very detailed understanding of every aspect and corner of CloudKit, and I have stepped into almost every pitfall, big and small.

IceCream vs. CKSyncEngine

There is a fun story here. IceCream was born in 2017, and from the first version, I named its core management class SyncEngine. It wasn’t until 2023 that Apple officially launched CKSyncEngine, and many of its interface definitions are surprisingly similar to those in IceCream. To some extent, this counts as a form of official “validation” for me.

However, the design philosophies of the two are different. IceCream is a complete encapsulation for the Realm database and can be called directly; CKSyncEngine requires developers to be familiar with the local persistence layer and adapt it themselves within the API.

For example, in IceCream, the core logic is to listen for Realm changes by holding a NotificationToken, thereby automatically performing CloudKit synchronization in the background:

Swift
BackgroundWorker.shared.start {
    let realm = try! Realm()
    // Observe Realm changes
    self.notificationToken = realm.objects(T.self).observe({ [weak self] (changes) in
        guard let self = self else { return }
        switch changes {
        case .initial(_):
            break
        case .update(let collection, _, let insertions, let modifications):
            // Filter and transform data to be synced
            let recordsToStore = (insertions + modifications)
                .filter { $0 < collection.count }
                .map { collection[$0] }
                .map { $0.record }
            let recordIDsToDelete = modifications
                .filter { $0 < collection.count }
                .map { collection[$0] }
                .map { $0.recordID }
            
            guard !recordsToStore.isEmpty || !recordIDsToDelete.isEmpty else { return }
            self.pipeToEngine?(recordsToStore, recordIDsToDelete)
        case .error(_):
            break
        }
    })
}

In Apple’s CKSyncEngine, you need to track changes in the local database yourself, and then inform the engine which changes to upload during the next sync via the add(pendingDatabaseChanges:) and add(pendingRecordZoneChanges:) methods.

Simply put, if you are using Realm, IceCream is still the best choice for “one line of code to achieve sync”; if you use SQLite, Core Data, or other storage methods, or if you want higher flexibility, then CKSyncEngine is a powerful tool. (Note: CKSyncEngine requires iOS 17+, while IceCream supports down to iOS 10 😆).

To this day, 8 years after its birth, IceCream is still one of the most popular open-source projects under the CloudKit tag on GitHub.

Why Choose CloudKit?

The reasons why CloudKit is so popular among developers are simply these:

  1. Free.
  2. Does not count against your quota: Data synced in the Private Database counts against the user’s own iCloud storage quota.
  3. Simple configuration: Client-driven, especially suitable for iOS engineers with no backend development experience.
  4. Core selling point: Although Apple does not recommend doing so, providing an “iCloud Sync” feature in Apps has become a core paid feature for many applications.

Regarding “free,” Craig Federighi once said a classic line at WWDC: “CloudKit is effectively free…with limits.”

image-20251213163023815

Although that detailed Limits chart from the early days can no longer be found on the official website, in all these years, I have never heard of any developer around me needing to pay for CloudKit. Apple’s intention is that they want developers to use CloudKit, as long as they don’t abuse it.

image-20251213160116418

In addition, although developers often complain about iCloud instability, for a developer like me who is unfamiliar with server maintenance, CloudKit is a treasure. The system’s built-in Notes and iCloud Drive are based on it, which is enough to prove its reliability. In my actual experience, the iCloud service has always been very stable.

Limitations and Pitfalls of CloudKit

Fatbobman specifically asked me to talk about the limitations of CloudKit. Based on years of project experience, I think the following points are worth noting:

1. Limited Access Speed

If you request a large amount of data via CKQueryOperation (for example, more than one page/100 items), the completion time will be significantly longer. I did an experiment: requesting the 1000 most recently updated records from the Public Database took about 10 seconds to fetch them all.

For mature commercial projects, this duration is usually unacceptable. Therefore, the frontend must perform sufficient loading optimization:

  • Most of the time, you don’t need to wait for all data to load before displaying it. You can adopt an “on-demand” strategy: load the first few items for user consumption, and continue to request and cache the remaining data in the background. This is consistent with the philosophy of App Launch optimization: Lazy Load.
  • Additionally, for large files like images and videos, CloudKit offers limited configuration (no CDN acceleration, etc.) and is quite a black box. For large file synchronization, it is recommended to use iCloud Documents or other professional cloud storage services.

2. Unable to Perform Aggregation Operations like Count

This is currently a major shortcoming of CloudKit: you cannot quickly get “the total number of people matching xxx criteria.”

The current workaround is: Spin up a separate dedicated service (for example, running a script on a server) to periodically poll or listen for changes in CloudKit data, calculate the values, and store them back. The client can simply read directly or access that service when needed. If anyone has a better pure CloudKit implementation method, feel free to exchange ideas.

3. Production Environment Schema Cannot Be Rolled Back

CloudKit Console is divided into two environments: Development and Production. Note: Once the Schema is deployed to Production, fields can only be added, not removed or modified in type. This is consistent with the principles mentioned in Fatbobman’s previous articles.

What if there is a deprecated field in the cloud? My method is do not handle it 😆. Leave it there as a “redundant” field, simply set it to Optional in the client Model parsing, and stop using it in the business code:

Swift
// Deprecated field handling
let emailAddress = record.value(forKey: "emailAddress") as? String ?? ""

This places high demands on the developer’s foresight in designing data tables and their overall software engineering level.

Music Mate in Practice: How to “Exploit” CloudKit

In February 2022, I launched a music social App — Music Mate. Its core function is to “peek” at what songs others are listening to through swipe up/down gestures, and if you find someone with similar taste, you can start a chat or check their social accounts.

image-20251213160736473

Since the beta version, I have been using CloudKit Public Database as the cloud data storage. Combined with SwiftUI, the experience is excellent.

Typical Query Scenarios

CloudKit can well satisfy the specific query requirements in the “listening and making friends” business:

  1. Show friends who are listening to the same song as you This is the core feature. The simplified Query is written as follows:
Swift
let musicItemID = "1435933839" // Current song ID
let predicate = NSPredicate(format: "musicItemID == %@", musicItemID)
let query = CKQuery(recordType: "FriendAnnotation", predicate: predicate)
query.sortDescriptors = [
    NSSortDescriptor(key: "modificationDate", ascending: false)
]

let operation = CKQueryOperation(query: query)
operation.recordMatchedBlock = { [weak self] recordID, result in
    self?.resolveRecordResult(recordID: recordID, result: result)
}
operation.queryResultBlock = { [weak self] result in
    self?.resolveResult(result: result)
}
container.publicCloudDatabase.add(operation)
  1. Show friends listening to music within 10km nearby
Swift
let currentLocation = CLLocation(latitude: 31, longitude: 121.51)
// Use CloudKit's specific distanceToLocation function
let predicate = NSPredicate(format: "distanceToLocation:fromLocation:(location, %@) < %f", currentLocation, 10000)
  1. Other filters Such as showing only users who have linked Instagram/Red (Xiaohongshu), or filtering by gender, etc.

Music Mate has been online for more than three years, experiencing a peak of more than 30,000 new users pouring in globally in a single day, and the CloudKit service has never crashed once. It gives me great peace of mind.

At the same time, Music Mate users’ “favorite songs list” is synced via IceCream in the Private Database. Since the launch, I have received almost no user feedback about data loss or sync errors.

Side Note: The notification “xxx played the song you are listening to” in Music Mate is actually implemented using a third-party IM SDK via APNs, not CloudKit Subscription. Although I haven’t used Subscription deeply, CKQuerySubscription is a feature with huge potential and is worth exploring.

image-20251213161121957

Advanced CloudKit Tricks

Besides conventional data storage and synchronization, I also discovered some other uses for CloudKit.

1. Remote Feature Flags

In business, we often need Feature Flags for A/B testing or enabling specific modes. By establishing a configuration table in the CloudKit Public Database, you can quickly build a remote configuration system without deploying an additional backend.

2. Cross-App Access to Cloud Data

This is a relatively niche but powerful feature. Different Apps under the same developer account can not only access their own Container but also access each other’s Containers.

For example, I recently launched an App to help preview concert setlists called Setlists. On the setlist detail page, users can see how many people (in Music Mate) are currently listening to each song. This data is obtained by the Setlists client directly requesting Music Mate’s Public Database.

image-20251213161333236

Implementation Steps:

  1. In the Signing & Capabilities of the Setlists Xcode project, check the iCloud Container for Music Mate.

Screenshot 2025-12-11 at 17.15.49.png

  1. Specify the Container ID in the code:
Swift
// Accessing Music Mate's container in the Setlists App
private let container = CKContainer(identifier: "iCloud.reversed-domain.xxx.musicmate")
let operation = CKQueryOperation(query: query)
container.publicCloudDatabase.add(operation)

This easily achieves a cross-application data sharing ecosystem.

Summary

That covers some of my insights and experiences using CloudKit over the years. In Apple’s technology frameworks, there are many “Hidden Gems” like CloudKit that are powerful but easily overlooked, waiting for us to slowly sift through the sand to discover them.

I hope this article serves as a starting point. If you have more insights on using CloudKit, feel free to communicate and share with me through various channels.

About Author

Cai Yue is an iOS engineer turned entrepreneur, now building music apps. He’s launched Music Mate (connect through music) and Setlists (preview concert setlists), with more projects underway. He shares indie dev insights on X and Xiaohongshu. Check them out!

Subscribe to Fatbobman

Weekly Swift & SwiftUI highlights. Join developers.

Subscribe Now