On top of unstable CloudKit sync, we've got also extremely unstable sharing/collaboration functionality.
I mean, it's quite impressive how easy it is to enable sharing, but this feature should not be released at this point. It feels like using software in the alpha version.
Let's take NSPersistentCloudKitContainer.share(_:to:) (https://developer.apple.com/documentation/coredata/nspersistentcloudkitcontainer/3746834-share), the documentation says:
Sharing fails if any of the following conditions apply:
Any objects in managedObjects, or those the traversal finds, belong to > an existing share record.
However, it's wrong... if you pass any object from the existing share, it will return the same share... It never fails in this case.
Things are getting even weirder if you experiment a little bit with shares. So let's assume you share a couple of objects:
persistentContainer.share([A, B, C, D], to: nil)
Now you stop sharing those via UICloudSharingController and you want to share again but just C. So you call:
persistentContainer.share([C], to: nil)
and surprise, surprise, you get a new share URL, but you share all previously shared objects (A, B, C, D), not as you would have expected only C...
On top of that, you keep getting some weird errors like:
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _requestAbortedNotInitialized:](2190): <NSCloudKitMirroringDelegate: 0x3029b84b0> - Never successfully initialized and cannot execute request '<NSCloudKitMirroringExportRequest: 0x30359f1b0>123123123123' due to error: <CKError 0x3018699b0: "Partial Failure" (2/1011); "Failed to modify some record zones"; uuid = 12312312312312; container ID = "iCloud.some.id"; partial errors: {
com.apple.coredata.cloudkit.share.123123123123123:__defaultOwner__ = <CKError 0x30186a3d0: "Invalid Arguments" (12/2006); server message = "Only shared zones can be accessed in the shared DB"; op = 12312312312312; uuid = 123123123123>
}>
Even though the only thing I use is
persistentContainer.share
and
UICloudSharingController(share: share, container: cloudKitContainer)
And the cherry on the top, from time to time if you play a little with sharing multiple objects at once, it happens that it randomly wipes out some records from that share...
I don't even know where to start reporting issues and if its worth it, because every ticket will be dismissed anyway because "we need thousands of your logs, we require you to do all the job for us".
CloudKit
RSS for tagStore structured app and user data in iCloud containers that can be shared by all users of your app using CloudKit.
Posts under CloudKit tag
200 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
My app is using SwiftData with CloudKit integration. Everything at the moment is working fine. I have a struct that saves Data as an optional. This is Data related to an Image. It saves and loads as expected. When I disconnect my phone from wifi or my phone network, the image still loads. I'm assuming that means the Data is being stored locally on the phone as well. Is there a way to display what's stored locally to the user inside the application?
Edit: I've realized that CloudKit is saying the data is too large, but the images are still being saved. Does that mean they're only locally being saved?
Is there any version of UICloudSharingController available on macOS or do we need to handle it manually?
How to manually configure a share and send an invitation from macOS?
What is the recommended way of intercepting and processing errors?
As far as I know, there are 4 main areas:
iCloud account status - this can be checked and intercepted via notifications
exceptions from fetch/execute/save - it can be a simple do..catch, but what exceptions can we expect here, what should be handled, and how?
there could be some asynchronous issues with synchronization. How should we intercept them and how should they be handled?
issues with iCloud storage - quota exceeded, etc. How to intercept & handle those?
I'm trying to achieve production-ready implementation, but there are many pitfalls and hidden issues that are not well documented. Could you provide some advice on how to handle properly all these situations?
Hello,
I'm using NSPersistentCloudKitContainer. If the user disables iCloud sync for my app in the system settings and opens the app, all records are immediately wiped out, even if there are unsynced changes (like records added offline).
Disabling iCloud sync doesn't even show any warning, so the user may lose all data (if it's not already synced to Cloud).
Is it possible to intercept that the store will be wiped out when the app is launching? I would copy all records to the local storage then to avoid losing data by the user.
Apparently the @Query property wrapper from SwiftData does not update when data is loaded from CloudKit. The data can be programmatically be accessed but nothing appears in the view.
Steps to reproduce:
Create new Xcode project using the SwiftData storage option.
Provide a default value for the Item Model. That is required so data can be automatically synced by SwiftData.
Enable iCloud -> CloudKit capabilities and choose a container
Enable Background Modes -> Remote notification capability
Add a toolbar button that prints the number of items from @Query like this:
ToolbarItem {
Button {
print(items.count)
} label: {
Label("Count", systemImage: "1.circle")
}
}
Install app on a physical device that can sync to iCloud.
Add some items using the + in the toolbar
Delete app and wait for around a minute
Reinstall app with a debugger attached
Press the 1.circle button in the toolbar. It will print either 0 or the number of items you previously added. When it does not print 0 the data should be visible but it is not. Once you quit and relaunch the app or press the + button again, all of the items appear.
Has anyone else experienced this before? Anything I can do so the data appears / the view reloads once the items are available? I need to update my view once the data has been loaded from iCloud.
I already filed a bug report with id FB14619787.
CloudKit sync is very unstable. Sometimes it just stops syncing for no reason, other times it works almost instantly.
The core issue with synchronization is that CoreData relies mostly on two things:
silent push notifications, which are by design unreliable and can be throttled
user interactions, I noticed that the local database is updated most likely periodically and also based on some app events like entering the foreground.
Unfortunately, there is no SDK function that allows us to force sync with CloudKit, which basically prevents us from providing some features to recover if a user encounters problems.
After thousands of tests, I finally discovered what was wrong and how to make the synchronization stable. Basically, I noticed that at some point CoreData decides that it won't synchronize data unless you deactivate and activate the application, which is crazy. It's getting even worse if we talk about extensions like the keyboard extension on iOS. The same happens on all platforms.
Therefore, knowing that I implemented a trick that happened to work perfectly. The workaround requires to periodically sending an event pretending that the app is going foreground.
macOS:
var cancellable = Set<AnyCancellable>()
// ...
Timer.publish(every: 20.0, on: RunLoop.main, in: .common)
.autoconnect()
.sink { _ in
NotificationCenter.default.post(.init(name: NSApplication.willBecomeActiveNotification))
}
.store(in: &cancellable)
iOS:
var cancellable = Set<AnyCancellable>()
// ...
Timer.publish(every: 20.0, on: RunLoop.main, in: .common)
.autoconnect()
.sink { _ in
NotificationCenter.default.post(.init(name: UIApplication.didBecomeActiveNotification))
}
.store(in: &cancellable)
After that, everything works perfectly. Pitty that the solution mostly meant for enterprise is so unstable and there is not even a single SDK function to recover from that (force sync).
Any plans to fix CoreData+CloudKit? I also created a ticket: #FB14531806.
I am able to fetch CloudKit records from my MacOS command line tool/daemon.
However, I would like CloudKit to notify my daemon whenever CKRecords were altered so I would not have to poll periodically.
In CloudKit console I see that my app successfully created CloudKit subscription, but the part that confuses me is where in my app do I define callback function that gets called whenever CloudKit attempted to notify my app of CloudKit changes?
My first question - do I need to define callback in my implementation of UNUserNotificationCenterDelegate? NSApplicationDelegate? Something else?
My second question, would CKSyncEngine work from command line application?
For a photo based social media network (think instagram), which cloud platform would be best from the point of not costing even with scale?
See I was attracted to cloudkit because as the user count increases, so does the free allowed storage. Compared to say Supabase which gives literally 1GB of file storage on the free plan, which would get used up very quickly as more and more users upload photos.
Very interested to hear thoughts on choosing the right platform. (Keep in mind only interested in developing an iOS app and not worried about cross platform compatibility).
Hello everyone,
I'm currently working on an iOS app using SwiftUI and Core Data, integrating CloudKit for data synchronization. I've set up my Core Data stack with NSPersistentCloudKitContainer, and everything appears to be working correctly locally. However, I'm not seeing any records appearing in the CloudKit Dashboard. This issue started occurring after transferring the Apple Developer account ownership and changing the CloudKit container.
Here's a summary of my setup and what I've tried so far:
Setup
PersistenceController.swift
import SwiftUI
import Foundation
import CoreData
import CloudKit
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "Model")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No Descriptions found")
}
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.company.Project")
container.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
func saveContext() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
static let preview: PersistenceController = {
let controller = PersistenceController()
// Remove existing preview data
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Letter.fetchRequest()
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let userFetchRequest: NSFetchRequest<NSFetchRequestResult> = User.fetchRequest()
let userBatchDeleteRequest = NSBatchDeleteRequest(fetchRequest: userFetchRequest)
let senderFetchRequest: NSFetchRequest<NSFetchRequestResult> = Sender.fetchRequest()
let senderBatchDeleteRequest = NSBatchDeleteRequest(fetchRequest: senderFetchRequest)
do {
try controller.container.viewContext.execute(batchDeleteRequest)
try controller.container.viewContext.execute(userBatchDeleteRequest)
try controller.container.viewContext.execute(senderBatchDeleteRequest)
try controller.container.viewContext.save()
} catch {
fatalError("Failed to delete preview data: \(error)")
}
}
Entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.company.Project</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>
What I've Tried
Verified that the iCloud capability is enabled in the Xcode project settings.
Checked the containerIdentifier in the NSPersistentCloudKitContainer setup.
Ensured the app is signed in with the correct iCloud account.
Observed that local Core Data operations work correctly and save without errors.
Waited to ensure any potential synchronization delays are accounted for.
Observations
I see debug messages in the Xcode console indicating that records are being serialized and saved to CloudKit, but they do not appear in the CloudKit Dashboard.
I have verified that I'm looking at the correct Private Database and _defaultZone.
When I delete the app and reinstall it, I can see that the data remains, indicating that the data is being stored somewhere in iCloud but not visible in the CloudKit Dashboard.
After resetting the schema in the CloudKit Console and running the app, the schema (including record types) syncs immediately, but the records are still not visible.
Console Output
CoreData: debug: CoreData+CloudKit: -[PFCloudKitSerializer newCKRecordsFromObject:fullyMaterializeRecords:includeRelationships:error:](576): Serializer has finished creating record: <CKRecord: 0x15b13e600; recordType=CD_Letter, recordID=F879D7B8-0338-418D-A330-6B9DF7947C6A:(com.apple.coredata.cloudkit.zone:__defaultOwner__), values={
"CD_content" = "Dear User. Happy Valentine's Day! Today. l want to remind you of how much you mean to me. Your presence in my life fills my heart with joy and love. Thank you for being my confidant, my friend, and my love. Every moment with you is precious, and I look forward to creating many more beautiful memories together. With all my love, Thomas";
"CD_createdAt" = "2024-07-30 03:17:13 +0000";
"CD_date" = "2024-07-30 03:15:58 +0000";
"CD_emotion" = Lovely;
"CD_entityName" = Letter;
"CD_icon" = "❤️🔥";
"CD_id" = "81569A99-E74C-43AF-B346-220A75EA336E";
"CD_imageData" = "{ length=282816, sha256=a76343766534e061472e243deec7f0b428c85e8a6b94e6cd761443c45f5be41c }";
"CD_primaryAlpha" = "0.4313725490196079";
"CD_primaryBlue" = 0;
"CD_primaryGreen" = "0.09014883061658401";
"CD_primaryRed" = "0.4156862745098039";
"CD_secondaryAlpha" = 1;
"CD_secondaryBlue" = "0.8823529411764706";
"CD_secondaryGreen" = "0.8784313725490196";
"CD_secondaryRed" = "0.9490196078431372";
"CD_sender" = "EF893D7E-E9E9-453B-B76E-6A5D77E14AA3";
"CD_tertiaryAlpha" = 1;
"CD_tertiaryBlue" = "0.8823529411764706";
"CD_tertiaryGreen" = "0.8784313725490196";
"CD_tertiaryRed" = "0.9490196078431372";
"CD_title" = "I love you";
}>
Despite this, no records are found in the CloudKit Dashboard.
Request for Assistance
Are there any additional steps I might have missed to ensure that records sync correctly with CloudKit?
Could there be any known issues or additional configurations required for syncing Core Data with CloudKit?
Any advice on further troubleshooting steps or areas to investigate would be greatly appreciated.
Thank you for your time and assistance!
I have a SwiftUI + SwiftData (with iCloud) app. The setup code is standard, with schema migrations (mostly lightweight, one custom). Everything works correctly except in one scenario.
When I run a newer version of the app (with an updated schema adding one field + migration) on a device with the previous data container (the usual app version update through TestFlight), I encounter an issue on the first launch. The app crashes on the first "cold" start but runs successfully on subsequent launches.
The error I receive on the first run is:
addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134060)
NSLocalizedFailureReason : Unable to find a configuration named 'default' in the specified managed object model.
What might be a problem and how to resolve it?
In Xcode I have created UI-less application. I tried to add following code:
import CloudKit
let container = CKContainer.default()
And it is failing with:
In order to use CloudKit, your process must have a com.apple.developer.icloud-services entitlement. The value of this entitlement must be an array that includes the string "CloudKit" or "CloudKit-Anonymous".
If I go to project and select my Command Line Tool target I don't see CloudKit capability that I usually see in UI based applications.
So, is it impossible to use CloudKit from Command Line tools?
I'm getting an error: Unknown Relationship Type - nil when using SwiftData and CloudKit. I've searched for this on Google, but seems like one has experienced it before. This is on iOS 18.
I have a Transaction model:
@Model
final class Transaction {
var timestamp: Date = Date()
var note: String = ""
var cost: Cost? = nil
var receipt: Receipt? = nil
//Relationships
var merchant: Merchant? = nil
var category: TransactionCategory? = nil
var tags: [Tag]? = nil
init(timestamp: Date) {
self.timestamp = timestamp
}
}
This has a relationship with TransactionCategory.
@Model
final class TransactionCategory {
var id = UUID()
var name: String = ""
//Relationship
var transactions: [Transaction]? = []
init() { }
init(name: String) {
self.name = name
}
}
I've tried using @Relationship here in TransactionCategory, but it didn't make a difference.
There is Picker that allows you to select a Category. In this case I created a random one, which was inserted into the context and saved. This is successful by the way. When you press a done button, a Transaction is created and the Category modified like this: newTransaction.category = category. It fails as this point with that error.
I also tried to create the Transaction, insert into the context, and then update the Category but it then failed at the context insertion, prior to updating the Category.
As you can see, I have another model called Merchant. When you press the done button, if you've typed in a Merchant name, it will create it, insert it into the context and then update the transaction as such:
newTransaction.merchant = getMerchant()
private func getMerchant() -> Merchant? {
//Create merchant if applicable
if merchant.name.isEmpty == false {
if let first = merchants.first(where: {$0.name == merchant.name.trim()}) {
// Set to one that already exist
return first
} else {
// Insert into context and insert into transction
context.insert(merchant)
return merchant
}
}
return nil
}
This code works fine and Merchant has the same relationship with Transaction as Category does.
Does anyone have any idea what could be causing this problem?
Hi!
I'm using CoreData + CloudKit. It works well both on macOS and iOS, however, I can't make it work with extensions (share, action, keyboard).
I get Invalid bundle ID for container error:
<CKSchedulerActivity: 0x3029f4d20; identifier=com.apple.coredata.cloudkit.activity.export.A65D5B7A-18AA-400A-B25F-F042E46646F6, priority=2, container=iCloud.com.org.app.dev:Sandbox, relatedApplications=(
"com.org.App.dev.App-Keyboard"
), xpcActivityCriteriaOverrides={
ActivityGroupName = "com.apple.coredata.cloudkit.App Keyboard.A65D5B7A-18AA-400A-B25F-F042E46646F6";
Delay = 0;
Priority = Utility;
}>
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:](2812): <NSCloudKitMirroringDelegate: 0x303fd82d0>: Error recovery failed because the following fatal errors were found: {
"<CKRecordZoneID: 0x300ef9bc0; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>" = "<CKError 0x300efa5e0: \"Permission Failure\" (10/2007); server message = \"Invalid bundle ID for container\"; op = xxxxxxx; uuid = zzzzz-xxxxx; container ID = \"iCloud.com.org.app.dev\">";
}
I checked everything 10x: profiles, bundle ids, entitlements, etc. I even removed all local provisioning profiles and recreated them, I also tried setting different CloudKit container, but nothing helps. I tested it on a real device.
My setup:
main app bundle id: com.org.App.dev
keyboard bundle id: com.org.App.dev.App-Keyboard
action extension bundle id: com.org.App.dev.Action-Extension
CloudKit container id: iCloud.com.org.app.dev
I keep the CoreData database in the app group container, but I also tried locally and it doesn't really matter.
This is how I setup my CoreData:
self.persistentContainer = NSPersistentCloudKitContainer(name: "AppCoreModel")
persistentContainer.persistentStoreDescriptions = [createCloudStoreDescription()]
persistentContainer.loadPersistentStores { [self] _, error in
if let error {
logError("Could not load Core Data store \(error)")
} else {
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
logDebug(persistentContainer.persistentStoreDescriptions.first?.url?.absoluteString ?? "")
logDebug("Core Data store loaded")
}
}
private func createCloudStoreDescription() -> NSPersistentStoreDescription {
let cloudStoreOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.org.app.dev"
)
cloudStoreOptions.databaseScope = .private
let documentsUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppConstants.appGroupId)!
let cloudStoreDescription = NSPersistentStoreDescription(
url: documentsUrl.appendingPathComponent("cloud-database.sqlite")
)
cloudStoreDescription.type = NSSQLiteStoreType
cloudStoreDescription.cloudKitContainerOptions = cloudStoreOptions
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
return cloudStoreDescription
}
Any help would be highly appreciated. It seems like iOS bug, because everything seems to be configured properly. I even checked app identifiers if containers are properly assigned.
Similar issue when using CloudKit directly (unresolved):
https://developer.apple.com/forums/thread/665280
For a long time, I've been using just a shared file via iCloud to ensure synchronization between devices. It works very well, everything is synchronized within seconds. However, I started encountering issues with race conditions, especially if there are more than 2 devices updating data. It's easy then to override the database accidentally.
That's why I decided to try with the tool that is designed for that: iCloud Database. However, I quickly realized that the performance was extremely low.
As I understand, the recommended approach to listen to changes in the database is to create a subscription, and I did that. However, during my tests, I discovered that it can take even 10 or more minutes to get that notification. It's unacceptable, some user could be waiting for new changes on their device.
I could of course spam with fetch every 10 seconds (using changeToken), but I'm not sure if it's a good idea. I could also provide manual fetch like pull to refresh or something like that, but automatic synchronization would be much better for the user.
I have recently moved some of my data save operations from the view context to background contexts. Since the switch, some (but not all) of my users are reporting that the background save operations crash 100% of the time. After researching, I have narrowed it down to the fact that these save operations involve establishing a relationship to an entity with a derived attribute. An example is shown below:
try await CoreDataStack.shared.performBackgroundTask { context in
var transaction = Transaction(context: context)
transaction.amount = NSDecimalNumber(decimal: 0)
transaction.id = UUID()
let account = Account.account(withName: "Default", in: context)
transaction.account = account
try context.save()
} // <= Crashes!
In the above example, each Transaction has a to-one relationship to an Account, and the latter has a to-many relationship the the former.
Account has a derived attribute called balance that is calculated using the expression sum:(transactionItems.amount).
The would then crash when the performBackgroundTask block exits (after the save() operation returns):
Thread 4 Crashed:
0 libobjc.A.dylib 0x00000001850ae00c objc_release_x8 + 8
1 CoreData 0x00000001900d8cfc -[_CDSnapshot dealloc] + 72 (_CDSnapshot.m:691)
2 CoreData 0x00000001900d8b68 _NSQLRow_dealloc_standard + 48 (NSSQLRow.m:156)
3 CoreFoundation 0x0000000187d72228 __CFBasicHashRemoveValue + 192 (CFBasicHash.c:1332)
4 CoreFoundation 0x0000000187d7212c CFBasicHashRemoveValue + 452 (CFBasicHash.c:1418)
5 CoreFoundation 0x0000000187d71638 CFDictionaryRemoveValue + 196 (CFDictionary.c:477)
6 CoreData 0x00000001900f6fa8 -[NSPersistentStoreCache decrementRefCountForObjectID:] + 96 (NSPersistentStoreCache.m:120)
7 CoreData 0x00000001900f6eec -[NSSQLCore managedObjectContextDidUnregisterObjectsWithIDs:generation:] + 172 (NSSQLCore.m:4329)
8 CoreData 0x00000001900f30f4 0x1900d6000 + 119028
9 CoreData 0x00000001900f4d74 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 204 (NSPersistentStoreCoordinator.m:404)
10 libdispatch.dylib 0x000000018fe9b0d8 _dispatch_client_callout + 20 (object.m:576)
11 libdispatch.dylib 0x000000018fea26e0 _dispatch_lane_serial_drain + 744 (queue.c:3934)
12 libdispatch.dylib 0x000000018fea31e8 _dispatch_lane_invoke + 380 (queue.c:4025)
13 libdispatch.dylib 0x000000018feae258 _dispatch_root_queue_drain_deferred_wlh + 288 (queue.c:7185)
14 libdispatch.dylib 0x000000018feadaa4 _dispatch_workloop_worker_thread + 532 (queue.c:6779)
15 libsystem_pthread.dylib 0x000000020f3c0c7c _pthread_wqthread + 288 (pthread.c:2696)
16 libsystem_pthread.dylib 0x000000020f3bd488 start_wqthread + 8
performBackgroundTask is defined as:
func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async rethrows -> T {
try await container.performBackgroundTask { context in
context.transactionAuthor = appTransactionAuthorName
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return try block(context)
}
}
Things to note:
If I do not establish the relationship to Account from Transaction, the crash doesn't happen; if I uncheck derived on the balance attribute of Account, the crash also does not happen.
For those users who are experiencing the crash, the crash rate is 100%; for those who aren't experiencing the crash, the crash never happens. So it's either 100% or 0%.
I have double checked all concurrency usages and all operations are performed within NSPersistentCloudKitContainer's performBackgroundTaskmethod, so it's probably not a threading issue. I have also tried ASan, Core Data's concurrency debug, as well as Zombie objects to no avail.
The crash seems to be related to a user's existing data. While I couldn't reproduce the crash on my own database, once I replaced the underlying SQLite file with one of my crashing user's, the crash is 100% reproducible.
I am at my wit's end here. Is this an internal Core Data bug, or am I doing something incorrectly? Any help is greatly appreciated!
Hello.
I'm going to create an app that supports purchase records.
SwiftData was used to implement storage, backup, and restoration functions at once.
One question is whether paging is necessary.
When you receive data from a server, you usually implement paging to send and receive data efficiently.
In the case of Realm, I know that it uses Lazy data, so I know that there is no need for paging.
What about SwiftData? Can the number of data affect performance??
I’ve setup the Cloudkit persistent container with private and shared database (see code below). I’ve enabled NSPersistentHistoryTrackingKey to true also for .shared database. I’ve noticed in the example from Apple that the History Tracking is only enabled in .private but not for .shared.
Questions:
For a CloudKit setup to sync
(a) between owners’ own devices (only private database), and
(b) between multiple iCloud Users through .private and .shared databases,
Do I need to enable history tracking for .shared database if I want to check the remote changes in the .shared database (or is the history tracking of the .private database of the owner also accessible in the .shared database)?
========================
let APP_BUNDLE_IDENTIFIER = Bundle.main.bundleIdentifier!
let APP_GROUP_IDENTIFIER = "group." + APP_BUNDLE_IDENTIFIER
private func setupPersistentContainer(_ container: NSPersistentCloudKitContainer? = nil, isStartup: Bool = true) -> NSPersistentCloudKitContainer {
let container = container ?? getCloudKitContainer(name: CORE_DATA_DATA_MODEL_NAME)
let defaultDirectoryURL: URL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_IDENTIFIER) ?? NSPersistentCloudKitContainer.defaultDirectoryURL()
let privateDataStoreURL = defaultDirectoryURL.appendingPathComponent("PrivateDataStore.store")
let sharedDataStoreURL = defaultDirectoryURL.appendingPathComponent("SharedDS.store")
// MARK: Private Store configuration
let privateDataStoreDescription = NSPersistentStoreDescription(url: privateDataStoreURL)
privateDataStoreDescription.configuration = "PrivateDataStore"
// Enable lightweight migration
privateDataStoreDescription.shouldInferMappingModelAutomatically = true
privateDataStoreDescription.shouldMigrateStoreAutomatically = true
// Turn History Tracking
privateDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let logOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: CLOUDKIT_LOG_CONTAINER_ID)
logOptions.databaseScope = .private
privateDataStoreDescription.cloudKitContainerOptions = logOptions
// turn on remote change notifications
privateDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [privateDataStoreDescription]
// MARK: Share Store configuration
let sharedDataStoreDescription = NSPersistentStoreDescription(url: sharedDataStoreURL)
sharedDataStoreDescription.configuration = "SharedDS"
// MARK: Enable lightweight migration
sharedDataStoreDescription.shouldInferMappingModelAutomatically = true
sharedDataStoreDescription.shouldMigrateStoreAutomatically = true
sharedDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let sharedOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: CLOUDKIT_LOG_CONTAINER_ID)
sharedOptions.databaseScope = .shared
sharedDataStoreDescription.cloudKitContainerOptions = sharedOptions
// turn on remote change notifications
sharedDataStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions.append(sharedDataStoreDescription)
self.stores = [StoreType : NSPersistentStore]()
container.loadPersistentStores(completionHandler: { [self] (storeDescription, error) in
if let error = error as NSError? {
print(error)
}
if let cloudKitContainerOptions = storeDescription.cloudKitContainerOptions {
if cloudKitContainerOptions.databaseScope == .private {
self.stores[.privateStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? privateDataStoreURL)
} else if cloudKitContainerOptions.databaseScope == .shared {
self.stores[.sharedStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? sharedDataStoreURL)
}
} else {
self.stores[.privateStore] = container.persistentStoreCoordinator.persistentStore(for: storeDescription.url ?? privateDataStoreURL)
}
})
/// Automatically merge changes in background context into View Context
/// Since we always use background context to save and viewContext to read only. The store values should always trump
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Create separate context for read and write
container.viewContext.name = VIEW_CONTEXT_NAME
container.viewContext.transactionAuthor = self.contextAuthor
self.setQueryGeneration(context: container.viewContext, from: .current)
return container
}
I encountered issues with some branch objects being assigned to multiple zones error (on iOS 17.5.1 and above). I get the errors when calling persistentContainer.shareshare(:to:completion:) and persistentContainer.persistUpdatedShare(:in:completion:).
"The operation couldn't be completed. Request '89D3F62D-548D-4816-9F1B-594390BD8F70' was aborted because the mirroring delegate never successfully initialized due to error: Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Object graph corruption detected. Objects related to 'Oxa2255fdc1fa980c5 x-coredata://CB800FA2-6054-4D91-8EBC-E9E31890344F/CDChildObject/p588' are assigned to multiple zones: {l <CKRecordZonelD: 0x3026a1170; zoneName=com.apple.coredata.cloud-kit.share.5D30F204-5970-489F-
BC2E-F863F1808A93, ownerName=defaultOwner>, <CKRecordZonelD: 0x302687b40; zoneName=com.apple.coredata.cloud-kit.zone, ownerName=_defaultOwner>"
In my setup, I moved all my root objects into one custom zone (there is only one custom zone in my private database). In one of my root object, there are 6 'one-to-one' and 2 'one-to-many' relationships. The branch objects can contains other relationships.
Create root object flow:
func saveToPersistent(_ object: ViewModelObject) {
serialQueue.async {
let context = backgroundContext()
context.performAndWait {
// Create new baby with its one-to-one child objects.
let cdNewBaby = self.newCDBaby(object, context)
if let share = self.getShareZone(.privateStore).first {
self.moveToShareZone(pObjects, share: share, store: .privateStore)
}
CoreDataManager.single.saveContext(context)
self.updateZoneNSaveContext([cdNewBaby], context: context)
} // context.perform
} // serialQueue.async
}
func backgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.transactionAuthor = contextAuthor
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
func getShareZone(_ storeType: StoreType, zoneName: String? = nil) -> [CKShare] {
var shares: [CKShare] = []
do {
shares = try persistentContainer.fetchShares(in: stores[storeType])
} catch {
print(error)
return shares
}
if let zoneName = zoneName {
shares = shares({ $0.recordID.zoneID.zoneName == zoneName })
}
return shares
}
func moveToShareZone(_ sharedObjects: [NSManagedObject], share: CKShare, store: StoreType) {
self.persistentContainer.share(sharedObjects, to: share) { managedObjects, share, container, error in
if let error = error {
print(error)
} else if let share = share, let store = self.stores[store] {
self.persistentContainer.persistUpdatedShare(share, in: store) { (share, error) in
if let error = error {
print(error)
}
}
}
}
} // moveToShareZone
Create one-to-many relationship branch object flow:
serialQueue.async {
let context = self.backgroundContext()
context.performAndWait {
// MARK: Retrieve the Root record
let pObjects = CDRootRecord.fetchRecord(rootRecord.uuidString, store: store, zoneName: zoneName, context: context)
if let pRootRecord = pObjects.first {
self.newCDLogContent(pRootRecord.self, viewModelObject: viewModelObject, context: context)
// MARK: Save Log
CoreDataManager.single.saveContext(context)
}
} // context.performAndWait
} // serialQueue
Questions:
(1) Should I save a root object first before share to custom zone; or share to custom zone first before save? (I implemented save before share to zone in the past and found some issues on iOS16 where the object is not saved; and end of sharing object before save which works)
(2) As I understand, if a branch record is saved under a root record, it should automatically go into the root record. Or do I have to also share the branch record to the custom zone?
I have a large file saved in iCloud drive. I need just a portion of that file.
func ubiquitousData(file: URL, offset: UInt64, size: UInt64) async -> Data {
// downloads just the portion of the ubiquitous `file`.
}
FileManager already has an api that downloads the full file. Is there a way to download just a portion of the file?