{"id":961,"url":"https://github.com/deeje/CloudCore","last_synced_at":"2025-07-30T19:33:11.521Z","repository":{"id":36517474,"uuid":"115927238","full_name":"deeje/CloudCore","owner":"deeje","description":"Robust CoreData-CloudKit synchronization: offline editing, relationships, private, shared and public databases, field-level deltas, encrypted values, cacheable assets, and more.","archived":false,"fork":true,"pushed_at":"2025-06-16T18:20:39.000Z","size":595,"stargazers_count":161,"open_issues_count":13,"forks_count":17,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-06-29T06:02:13.766Z","etag":null,"topics":["cloudkit","coredata","ios","macos","synchronization","tvos","watchos"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"Sorix/CloudCore","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deeje.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-01-01T15:19:03.000Z","updated_at":"2025-05-09T06:11:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/deeje/CloudCore","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/deeje/CloudCore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeje%2FCloudCore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeje%2FCloudCore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeje%2FCloudCore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeje%2FCloudCore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deeje","download_url":"https://codeload.github.com/deeje/CloudCore/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeje%2FCloudCore/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267785862,"owners_count":24144123,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-29T02:00:12.549Z","response_time":2574,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudkit","coredata","ios","macos","synchronization","tvos","watchos"],"created_at":"2024-01-05T20:15:35.659Z","updated_at":"2025-07-30T19:33:11.170Z","avatar_url":"https://github.com/deeje.png","language":"Swift","readme":"# CloudCore\n\n![Platform](https://img.shields.io/cocoapods/p/CloudCore.svg?style=flat)\n![Status](https://img.shields.io/badge/status-production-green.svg)\n![Swift](https://img.shields.io/badge/swift-5.0-orange.svg)\n\n**CloudCore** is an advanced sync engine for CloudKit and Core Data.\n\n#### Features\n* Leveraging **NSPersistentHistory**, local changes are pushed to CloudKit when online.  Never lose a change again.\n* Pull manually or on CloudKit **remote notifications**.\n* **Differential sync**, only changed object and values are uploaded and downloaded.\n* Core Data relationships are preserved\n* **private database** and **shared database** push and pull is supported.\n* **public database** push is supported\n* Parent-Child relationships can be defined for CloudKit Sharing\n* Respects Core Data options (cascade deletions, external storage).\n* Support for 'Allows Cloud Encryption' for attributes in Core Data with automatic encoding to and from encryptedValues[] in CloudKit.\n* Knows and manages CloudKit errors like `userDeletedZone`, `zoneNotFound`, `changeTokenExpired`, `isMore`.\n* Available on iOS and iPadOS (watchOS and tvOS haven't been tested)\n* Sharing can be extended to your NSManagedObject classes, and native SharingUI is implemented\n* Maskable Attributes allows you to control which attributes are ignored during upload and/or download.\n* Cacheable Assets are uploaded automatically and downloaded on-demand, using long-lived operations separate from sync operations.\n\n#### CloudCore vs NSPersistentCloudKitContainer?\n\nNSPersistentCloudKitContainer provides native support for Core Data \u003c-\u003e CloudKit synchronization.  Here are some thoughts on the differences between these two approaches, as of May 2022.\n\n###### NSPersistentCloudKitContainer\n* Simple to enable\n* Support for Private, Shared, and Public databases\n* Synchronizes All Records\n* No CloudKit Metadata (e.g. recordName, systemFields, owner)\n* Record-level Synchronization (entire objects are pushed)\n* Offline Synchronization is opaque, but doesn't appear to require NSPersistentHistoryTracking\n* All Core Data names are preceeded with \"CD_\" in CloudKit\n* Core Data Relationships are mapped thru CDMR records in CloudKit\n* Sharing is supported via zones\n* No(?) long-lived operations support for large file upload/download\n\n###### CloudCore\n* Support requires specific configuration in the Core Data Model\n* Support for Private, Shared, and Public databases\n* Selective Synchronization (e.g. can delete local objects without deleting remote records)\n* Explicit CloudKit Metadata\n* Field-level Synchronization (only changed attributes are pushed)\n* Offline Synchronziation via NSPersistentHistoryTracking\n* Core Data names are mapped exactly in CloudKit\n* Core Data Relationships are mapped to CloudKit CKReferences\n* Maskable Attributes provides fine-grain control over local-only data and manually managed remote data\n* Sharing is supported via root records\n* Supports upload/download of large data files via long-lived operations, with proper schema configuration\n\nApple very clearly states that NSPersistentCloudKitContainer is a foundation for future support of more advanced features. I'm still waiting to learn which first-party apps use it. #YMMV\n\n## How it works?\nCloudCore is built using a \"black box\" architecture, so it works fairly invisibly for your application.  You just need to add several lines to your `AppDelegate` to enable it, as well as identify various aspects of your Core Data Model schema. Synchronization and error resolving is managed automatically.\n\n1. CloudCore stores *change tokens* from CloudKit, so only changed data is downloaded.\n2. When CloudCore is enabled (`CloudCore.enable`) it pulls changed data from CloudKit and subscribes to CloudKit push notifications about new changes.\n3. When `CloudCore.pull` is called manually or by push notification, CloudCore pulls and saves changed data to Core Data.\n4. When data is written to your persistent container (parent context is saved) CloudCore finds locally changed data and pushes to CloudKit.\n5. By leveraging NSPersistentHistory, changes can be queued when offline and pushed when online.\n\n## Installation\n\n### CocoaPods\n**CloudCore** is available through [CocoaPods](http://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'CloudCore'\n```\n\n## How to help?\nWhat would you like to see improved?\n\n## Quick start\n1. Enable CloudKit capability for you application:\n\n![CloudKit capability](https://cloud.githubusercontent.com/assets/5610904/25092841/28305bc0-2398-11e7-9fbf-f94c619c264f.png)\n\n2. For each entity type you want to sync, add this key: value pair to the UserInfo record of the entity:\n* `CloudCoreScopes`: `private`\n\n3. Also add 4 attributes to each entity:\n  * `privateRecordData` attribute with `Binary` type\n  * `publicRecordData` attribute with `Binary` type\n  * `recordName` attribute with `String` type\n  * `ownerName` attribute with `String` type\n\n4. And enable 'Preserve After Deletion' for the following attributes\n  * `privateRecordData` \n  * `publicRecordData`\n\n5. Make changes in your **AppDelegate.swift** file:\n\n```swift\nfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -\u003e Bool {\n  // Register for push notifications about changes\n  application.registerForRemoteNotifications()\n\n  // Enable CloudCore syncing\n  CloudCore.enable(persistentContainer: persistentContainer)\n\n  return true\n}\n\n// Notification from CloudKit about changes in remote database\nfunc application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -\u003e Void) {\n  // Check if it CloudKit's and CloudCore notification\n  if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) {\n    // Fetch changed data from iCloud\n    CloudCore.pull(using: userInfo, to: persistentContainer, error: nil, completion: { (fetchResult) in\n      completionHandler(fetchResult.uiBackgroundFetchResult)\n    })\n  }\n}\n```\n\n6. **Enable NSPersistentHistoryTracking** when you initialize your Core Data stack\n\n```swift\nlazy var persistentContainer: NSPersistentContainer = {\n  let container = NSPersistentContainer(name: \"YourApp\")\n\n  let storeDescription = container.persistentStoreDescriptions.first\n  storeDescription?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)\n\n  container.loadPersistentStores { storeDescription, error in\n    if let error = error as NSError? {\n      // Replace this implementation with code to handle the error appropriately.                \n    }\n  }\n  return container\n}()\n```\n\n7. To identify changes from your app that should be pushed, **save** from the convenience function performBackgroundPushTask\n\n```swift\npersistentContainer.performBackgroundPushTask { moc in\n  // make changes to objects, properties, and relationships you want pushed via CloudCore\n  try? context.save()\n}\n```\n\n8. Make first run of your application in a development environment, fill an example data in Core Data and wait until sync completes. CloudKit will create needed schemas automatically.\n\n## Service attributes\nCloudCore stores CloudKit information inside your managed objects, so you need to add attributes to your Core Data model for that. If required attributes are not found in an entity, that entity won't be synced.\n\nRequired attributes for each synced entity:\n1. *Private Record Data* attribute with `Binary` type\n2. *Public Record Data* attribute with `Binary` type\n3. *Record Name* attribute with `String` type\n4. *Owner Name* attribute with `String` type\n\nYou may specify attributes' names in one of two 2 ways (you may combine that ways in different entities).\n\n### Default names\nThe most simple way is to name attributes with default names because you don't need to map them in UserInfo.\n\n### Mapping via UserInfo\nYou can map your own attributes to the required service attributes.  For each attribute you want to map, add an item to the attribute's UserInfo, using the key `CloudCoreType` and following values:\n* *Record Name* value is `recordName`.\n* *Owner Name* value is `ownerName`.\n\n![Model editor User Info](https://cloud.githubusercontent.com/assets/5610904/24004400/52e0ff94-0a77-11e7-9dd9-e1e24a86add5.png)\n\nWhen your *entities have relationships*, CloudCore will look for the following key:value pair in the UserInfo of your entities:\n\n`CloudCoreParent`: name of the to-one relationship property in your entity\n\n### 💡 Tips\n* I recommend to set the *Record Name* attribute as `Indexed`, to speed up updates in big databases.\n* *P… Record Data* attributes are used to store archived version of `CKRecord` with system fields only (like timestamps, tokens), so don't worry about size, no real data will be stored here.\n\n## Scope: Public and/or Private\nYou can designate which databases each entity will synchronized with.  For each entity you want to synchronize, add an item to the entity's UserInfo, using the key `CloudCoreScope` and following values:\n* `public` = pushed to public database\n* `private` = synchronized with private (or shared) database\n* 'public,private' = both\n\n### Why Both?\nMaintaining two copies of a record means we get all the benefits of a private (and sharable) record, while also automatically maintaining a fully updated public copy.\n\n## Maskable Attributes\nYou can designate attributes in your managed objects to be masked during upload and/or download.  For each attribute you want to mask, add an item to the attribute's UserInfo, using the key `CloudCoreMasks` and following values:\n* `upload` = ignored during modify operations\n* `download` = ignored during fetch operations\n* `upload,download` = both\n\n## Cacheable Assets\nBy default, CloudCore will transform assets in your CloudKit records into binary data attributes in your Core Data objects.\n\nBut when you're working with very large files, such as photos, audio, or video, this default mode isn't optimal.\n\n* Uploading large files can take a long time, and sync will fail if not completed timely.\n* To optimize a user's device storage, you may want to downloading large files on-demand.\n\nCacheable Assets addresses these requirements by leveraging Maskable Attributes to ignore asset fields during sync, and then enabling push and pull of asset fields using long-lived operations.\n\nIn order to manage cache state, assets must be stored in their own special entity type in your existing schema, which comform to the CloudCoreCacheable protocol.  This protocol defines a number of attributes required to manage cache state:\n\n```swift\npublic protocol CloudCoreCacheable: CloudCoreType {        \n        // fully masked\n    var cacheStateRaw: String? { get set }\n    var operationID: String? { get set }\n    var uploadProgress: Double { get set }\n    var downloadProgress: Double { get set }\n    var lastErrorMessage: String? { get set }\n        // sync'ed\n    var remoteStatusRaw: String? { get set }\n    var suffix: String? { get set }\n}\n```\n\nThe heart of CloudCoreCacheable is implemented using the following properties:\n\n```swift\npublic extension CloudCoreCacheable {\n    \n    var cacheState: CacheState    \n    var remoteStatus: RemoteStatus\n    var url: URL\n    \n}\n```\n\nOnce you've configured your Core Data schema to support cacheable assets, you can create and download them as needed.\n\nWhen you create a new cacheable managed object, you must store its data at the file URL before saving it.  The default value of cacheState is \"local\" and the default value of remoteStatus is \"pending\". Once CloudCore pushes the new cacheable record, it sets the cacheState to \"upload\", which triggers a long-lived modify operation.  On completion, the cacheable managed object will have its cacheState set to \"cached\" and its remoteStatus set to \"available\".\n\nWhen cacheable records are pulled from CloudKit, the asset field is ignored (because it is masked), and the cacheState will be \"remote\".  When the remoteStatus is \"available\", you can trigger a long-lived fetch operation by setting the cacheState to \"download\" and saving the object.  Once completed, the cacheable object will have its cacheState set to \"cached\", and the data will be locally available at the file URL.\n\nNote that cacheState represents a state machine.\n```\n(**new**) =\u003e local -\u003e (push) -\u003e upload -\u003e uploading -\u003e cached\n(pull) =\u003e remote -\u003e **download** -\u003e downloading -\u003e cached\n```\n\n### Important\nSee the Example app for specific details.  Note, specifically, that I **need to override awakeFromInsert and prepareForDeletion** for my cacheable managed object type Datafile.  If anyone has ideas on how to push this critical implementation detail into CloudCore itself, let me know! \n\n## CloudKit Sharing\nCloudCore has built-in support for CloudKit Sharing.  There are several additional steps you must take to enable it in your application.\n\n1. Add the CKSharingSupported key, with value true, to your info.plist\n\n2. Implement the appropriate delegate(… userDidAcceptCloudKitShare), something like…\n\n```swift\nfunc windowScene(_ windowScene: UIWindowScene, \n\t\t\t\t userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {\n  let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])\n  acceptShareOperation.qualityOfService = .userInitiated\n  acceptShareOperation.perShareCompletionBlock = { meta, share, error in\n    CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { }\n  }\n  acceptShareOperation.acceptSharesCompletionBlock = { error in\n    // N/A\n  }\n  CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation)\n}\n```\n\nOR\n\n```swift\nfunc application(_ application: UIApplication,\n                 userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {\n  let acceptShareOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])\n  acceptShareOperation.qualityOfService = .userInitiated\n  acceptShareOperation.perShareCompletionBlock = { meta, share, error in\n    CloudCore.pull(rootRecordID: meta.rootRecordID, container: self.persistentContainer, error: nil) { }\n  }\n  acceptShareOperation.acceptSharesCompletionBlock = { error in\n    // N/A\n  }\n  CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation)\n}\n```\n\nNote that when a user accepts a share, the app does not receive a remote notification of changes from iCloud, and so it must specifically pull the shared record in.\n\n3. Use a CloudCoreSharingController to configure a UICloudSharingController for presentation\n\n4. When a user wants to delete an object, your app must distinguish between the owner and a sharer, and either delete the object or the share.\n\n## Example application\nYou can find example application at [Example](/Example/) directory, which has been updated to demonstrate sharing, maskable attributes, and cacheable assets.\n\n**How to run it:**\n1. Set Bundle Identifier.\n2. Check that embedded binaries has a correct path (you can remove and add again CloudCore.framework).\n3. If you're using simulator, login at iCloud on it.\n\n**How to use it:**\n* **+** button adds new object to local storage (that will be automatically synced to Cloud)\n* **Share* button presents the CloudKit Sharing UI\n* **refresh** button calls `pull` to fetch data from Cloud. That is only useful for simulators because Simulator unable to receive push notifications\n* Use [CloudKit dashboard](https://icloud.developer.apple.com/dashboard/) to make changes and see it at application, and make change in application and see ones in dashboard. Don't forget to refresh dashboard's page because it doesn't update data on-the-fly.\n\n## Example app using Cacheable Assets\n[MediaBook](https://github.com/deeje/MediaBook) is a production-level iOS app being developed, which demonstrates how to handle cacheable assets in collection views.\n\n## Tests\nCloudKit objects can't be mocked up, that's why there are 2 different types of tests:\n\n* `Tests/Unit` here I placed tests that can be performed without CloudKit connection. That tests are executed when you submit a Pull Request.\n* `Tests/CloudKit` here located \"manual\" tests, they are most important tests that can be run only in configured environment because they work with CloudKit and your Apple ID.\n\n  Nothing will be wrong with your account, tests use only private `CKDatabase` for application.\n\n  **Please run these tests before opening pull requests.**\n\nTo run them you need to:\n\n  1. Change `TestableApp` bundle id.\n  2. Run in simulator or real device `TestableApp` target.\n  3. Configure iCloud on that device: Settings.app → iCloud → Login.\n  4. Run `CloudKitTests`, they are attached to `TestableApp`, so CloudKit connection will work.\n\n## Roadmap\n\n- [ ] Add methods to clear local cache and remote database\n- [ ] Add error resolving for `limitExceeded` error (split saves by relationships).\n\n## Authors\n\ndeeje cooley, [deeje.com](http://www.deeje.com/)\n- refactored into Pull/Push termonology\n- added offline sync via NSPersistentHistory\n- added CloudKit Sharing support\n- added Maskable Attributes\n- added Cacheable Assets\n\nVasily Ulianov, [va...@me.com](http://www.google.com/recaptcha/mailhide/d?k=01eFEpy-HM-qd0Vf6QGABTjw==\u0026c=JrKKY2bjm0Bp58w7zTvPiQ==)\nOpen for hire / relocation.\n- implemented version 1 and 2, with dynamic mapping between CoreData and CloudKit\n\nOleg Müller\n- added full support for CoreData relationships\n","funding_links":[],"categories":["Core Data","Swift"],"sub_categories":["Linter"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeeje%2FCloudCore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeeje%2FCloudCore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeeje%2FCloudCore/lists"}