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?
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
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?
I recently updated our CloudKit collaboration invite codebase to use the new UIActivityController and NSItemProvider invitation as described in Apple's documentation. We previously used UICloudSharingController's init(preparationHandler:), which is since deprecated.
We have all of the previous functionality in place: we successfully create a CKShare, send the invite out, engage the share, and collaborate. However, we cannot get the Messages CKShare preview to use our custom image and title (henceforth referred to as “collaboration metadata”). Previously, while using UICloudSharingController's init(preparationHandler:) to commence the share invite, the collaboration metadata successfully displayed in the Messages conversation. Now, we have a generic icon of our app and “Shared with App-Name" title, leading to a loss of contextual integrity for the invite flow.
My question: How do we make the collaboration metadata appear in the Messages conversation?
Here is our code for creating the UIActivityController, NSItemProvider, CKShare, and other related entities. It encapsulates the entire CloudKit CKShare invite setup. You will note that we do configure the CKShare with metadata, and we do set the LPLinkMetadata on the UIActivityItemsConfiguration. GitHub Gist.
The metadata does successfully appear in the UIActivityController and the CKShare's image and title are available to the person receiving the share once they engage it and open it in our app – but the Messages preview item retains the generic message content. Also please note that this issue does occur in the production environment.
As a final note, examining UICloudSharingController's definition leads me to believe that supplying a UIActivityItemSource is the key to getting correct Messages collaboration metadata in place. My efforts at using an item adhering to UIActivityItemSource in the UIActivityViewController used to send the share did not yield the rich previews and displayed metadata I am aiming for.
I have a Multiplatform app for iOS and macOS targets.
I am using CloudKit with CoreData and have successfully established a private and public database.
The app has successfully synced private and public data for months between macOS (dev machine), an iPhone 13 Pro and an iPad Pro 12.9inch 2nd gen. The public data also syncs perfectly to simulator instances running under other iCloud accounts.
Recently I added a new entity in the public DB and here is where I seemed to have made a mistake.
I entered data into the new public database via my developer UI built into the macOS app running on my MBP before I indexed the necessary fields.
Side note - I find it necessary to index the following for each Entity to ensure iCloud sync works as expected on all devices...
modifiedTimestamp - Queryable
modifiedTimestamp - Sortable
recordName - Queryable
Realising my mistake, I indexed the above CKRecord fields for the new Entity.
Since then, the macOS target has remained in some way "frozen" (for want of a better term). I can add new public or private records in the macOS app but they do not propagate to the public or private stores in iCloud.
I have attempted many fixed, some summarised below:
clean build folder from Xcode;
remove all files from the folder /Users//Library/Containers/, place in recycle bin, empty recycle bin, then build and run;
build and run on iPhone and iPad targets to ensure all apps are current dev version, then repeat above processes.
I've read through the console logging when I build and run the macOS app many many times to see whether I can find any hint. The closest thing I can find is...
BOOL _NSPersistentUIDeleteItemAtFileURL(NSURL *const __strong) Failed to stat item: file:///Users/<me>/Library/Containers/com.me.AppName/Data/Library/Saved%20Application%20State/com.me.AppName.savedState/restorecount.plist
but my research on this and other forums suggests this is not relevant.
Through this, the app still functions as expected on iOS devices and both private and public database additions and modifications propagate to iCloud stores and to other devices.
I expect that removing the macOS app entirely from my dev machine would trigger a complete sync with all existing data. Imagine I bought a new macOS device and chose to install my app where before I had run this only on my iOS devices. My current problem suggests that I could not do this, but I know that this is not the intended behaviour.
This scenario makes me think there is a setting file for my macOS app that I'm not aware of and that this impeding the sync of all existing app data back to the fresh install of the macOS app? But that is a wild guess.
Running public releases (no betas)
Xcode 15.4 (15F31d)
macOS Sonoma 14.5
physical iOS devices running iOS 17.5.1
Any words of wisdom on how I might go about trying to solve this problem please?
I have implemented iCloud syncing in my app.
I am noticing a lot of "CKErrorInternalError" codes in my production logs. The description is "Failed user key sync" or "Failed to sync user keys". I have no idea what they mean or what is causing it.
Hello,
I'm experiencing an issue with iOS 18 Beta 3 and SwiftData.
I have a model with some attributes marked as externalStorage because they are pretty large.
In iOS 17 I could display a list of all my Models and it would not prefetch the externalStorage attributes.
Now in iOS 18 it seems the attributes are fetched even though I don't use them in my list.
It's an issue because the memory use of my app goes from 100mo on iOS 17 to more than 1gb on iOS 18 because of this.
My app is configured to sync with iCloud.
Anyone else experiencing the issue?
Thanks
Gil
I'm building an app which has both iOS and macOS versions along with extensions on both platforms. I want the main app to be able to share data the extension using the group container and I want the app on both platforms sync data over CloudKit.
CloudKit synchronization works like a dream. The data sharing between the app and extension on iOS works also exactly as intended. However on macOS every time the app is launched I get “MyApp” would like to access data from other apps. alert dialog.
I tried initializing the ModelConfiguration both with an explicit and automatic app group container identifiers. Same results.
Hi, I'm using CloudKit to create an app that backs up and records your data to iCloud.
Here's what I'm unsure about:
I understand that the 'CloudKit Dashboard' has 'Security Roles'. I thought these were meant to set permissions for accessing and modifying users' data, but I found there was no change even when I removed all 'Permissions' from 'Default Roles'. Can you clarify?
I'd like to know what _world, _icloud, and _creator in Default Roles mean respectively.
I would like to know what changes the creation, read, and write permissions make.
Is it better to just use the default settings?
Here's what I understand so far:
Default Roles:
_world: I don't know
_icloud: An account that is not my device but is linked to my iCloud
_creator: My Device
Permissions:
create: Create data
read: Read data
write: Update and delete data.
I'm not sure if I understand this correctly. Please explain.
I'm currently syncing core data with the CloudKit private and public databases, as you can see in the code below, I'm saving the private database in the default configuration in Core Data and the public in a configuration called Public everything works fine when NSPersistentCloudKitContainer syncs, what I'm having an issue with is trying to save to the public data store PublicStore, for instance when I try to save with func createIconImage(imageName: String) it saves the image to the "default" store, not the PublicStore(Public configuration).
What could I do to make the createIconImage() function save to the PublicStore sqlite database?
class CoreDataManager: ObservableObject{
static let instance = CoreDataManager()
private let queue = DispatchQueue(label: "CoreDataManagerQueue")
@AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
init(inMemory: Bool = false){
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
}
func updateCloudKitContainer() {
queue.sync {
container = setupContainer()
}
}
private func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
private func getStoreURL(for storeName: String) -> URL {
return getDocumentsDirectory().appendingPathComponent("\(storeName).sqlite")
}
func setupContainer()->NSPersistentContainer{
let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
let cloudKitContainerIdentifier = "iCloud.com.example.MyAppName"
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if iCloudSync{
if description.cloudKitContainerOptions == nil {
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
}
}else{
print("Turning iCloud Sync OFF... ")
description.cloudKitContainerOptions = nil
}
// Setup public database
let publicDescription = NSPersistentStoreDescription(url: getStoreURL(for: "PublicStore"))
publicDescription.configuration = "Public" // this is the configuration name
if publicDescription.cloudKitContainerOptions == nil {
let publicOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
publicOptions.databaseScope = .public
publicDescription.cloudKitContainerOptions = publicOptions
}
container.persistentStoreDescriptions.append(publicDescription)
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
//print("Saved successfully!")
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
class PublicViewModel: ObservableObject {
let manager: CoreDataManager
@Published var publicIcons: [PublicServiceIconImage] = []
init(coreDataManager: CoreDataManager = .instance) {
self.manager = coreDataManager
}
func createIconImage(imageName: String) {
let newImage = PublicServiceIconImage(context: manager.context)
newImage.imageName = imageName
newImage.id = UUID()
save()
}
func save() {
self.manager.save()
}
}
Is this possible? Here's what I'm trying:
I'm making an app that reads from a CloudKit database. That's working fine.
I made a second "admin" type app to update the database. But, of course, I don't intend to release the admin app to the public.
So it was all working fine while testing in the development environment, but now that my public app is in TestFlight, and I have updated the necessary stuff that should allow me to write to production, but every attempt successfully writes to development, not production.
I'm wondering if I submitted my admin app to TestFlight if it would work then. But that doesn't seem like a long term solution, since I think I would have to re-upload every 90 days... just doesn't seem ideal or correct.
Do I HAVE to write the admin functionality in to the public app and hide it?
What are better ways I could write to production other than manually through the console?
Thanks everyone!
Hi
A user of my app has contacted me about a crash they’re getting. They’ve sent me some logs (see attached) but I’m having difficulty working out what the cause is as it looks like it’s a watchdog timeout that’s occurring inside OS code, not my application. I also can’t reproduce the crash locally.
App background info:
The app logs peoples skydives, each log can contain a lot of data (rotation rates, acceleration, location, speeds, etc) and is stored in a separate file. These files can be stored in an iCloud container so the logs can be viewed from different devices. I use CoreData to maintain a database of key metadata so I can list the jumps in the UI even if the file for a jump isn’t on the device. Occasionally I have to delete this database and rebuild it by loading each jump log file and getting a fresh copy of the metadata. EG this can happen if a new version of the app requires an additional metadata field in the database.
Crash info:
The crash looks like it’s happening rebuilding the database, so the app will be trying to download and open each jump log and add the records to the database. I’ve noticed the following odd things about the crash log, which might be a good place to start:
There’s a huge number of threads in the “”UIDocument File Access” dispatch queue that are blocked
It looks like there's exactly 512 threads blocked in this queue. Which makes me think its hitting a limit.
Any idea why they are blocked?
I don’t know why there are so many, The database rebuild is done from an operation queue with a max concurrency of 10. So I would expect at most 10 jump logs to be being opened at one time
There seams to be two common stack trace patterns. Eg compare thread 1 and thread 5 in crash log 1.
In both crash logs the main thread is blocked, but in different bits of OS code in the two crash logs. It looks like this is the cause of the watchdog failure, but I’m not sure what the common cause could be.
Any ideas / help would be really appreciated.
Thanks
Tom
NOTE: I had to cut down the crash logs so they were small enough to upload.
Crash log 1 small.txt
Crash log 2 small.txt
Hello,
I have an iOS app and a companion watchOS app. Users record a workout on Apple Watch, the data for which is then transferred using both Watch Connectivity and Core Data + CloudKit (NSPersistentCloudKitContainer) to their iPhone, where it is processed and displayed.
As users are recording the workout on their Apple Watch, when they finish and the transfer begins, their iPhone is often not reachable to immediately send the data using Watch Connectivity and they have no network connection (cellular or Wi-Fi).
With Watch Connectivity I use transferFile from WCSession, which queues the file for transfer. With Core Data + Cloudkit I save the data and the export is queued.
An undetermined amount of time may pass until the user returns to their iPhone or connects to Wi-Fi and most of the time neither of the transfer methods actually transfers the data until the user opens the watchOS app into the foreground, at which point the transfer happens immediately for both methods.
I've tried a number of things already, without success, such as:
Using sendMessage from WCSession to send an immediate message to the watchOS app when the iOS app returns to the foreground to try and wake the watchOS app up so it can complete the data transfer.
On the watchOS app, after attempting to transfer the data, using downloadTask from URLSession to queue a background task to download something, in the hope that it would wake the watchOS app when network connectivity was restored and enable it to complete the data transfer.
On the watchOS app, instead of saving the data using NSPersistentCloudKitContainer, using CKRecord and CKDatabase directly to save the data using userInitiated as the quality of service, in the hope that it would be exported once network connectivity was restored.
Is there a way to trigger the watchOS app to transfer the data using Watch Connectivity or Core Data + CloudKit in the background when reachabillity or network connectivity is restored, even if the app may have been suspended by watchOS?
Many Thanks,
Alex
Hi,
I have some small amount of users who are receiving a lot of "throttling" error messages from CloudKit when they try to upload / download data from CloudKit. I can see this from the user reports as well as the CloudKit dashboard. It's erratic, unpredictable, and is causing all sorts of bad experience in my app. I would like to understand this more:
what causes a particular user to 'throttle' vs others, and what can they do to avoid it?
as an e.g if we are uploading 4000 records, having split them up into a 100 CKModifyRecordsOperations with 400 records each ... would that result in some operations getting 'throttled' in the middle of the upload? Would all the operations receive the 'throttled' error message, or only some operations?
if I replay all the operations after the recommended timeout, could they also get a 'throttle' response?
how do I reproduce something like this in the development environment? With my testing and development so far, I haven't run into such an issue myself.
Would love to hear some insight and suggestions about how to handle this.
Hello I'm a new developer and am learning the ropes. I have an app that I'm testing and seem to have run into a bug. The data is syncing from one device to another, however it takes closing the app on the Mac or force closing the app on iOS/iPadOS to get the app to reflect the new data.
Is there specific code I code share to help solve this issue or any suggestions that someone may have? Thank you ahead of time for your assistance.
import SwiftData
@main
struct ApplicantProcessorApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Applicant.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
struct ContentView: View {
var body: some View {
FilteredApplicantListView()
}
}
#Preview {
ContentView()
.modelContainer(SampleData.shared.modelContainer)
}
struct FilteredApplicantListView: View {
@State private var searchText = ""
var body: some View {
NavigationSplitView {
ApplicantListView(applicantFilter: searchText)
.searchable(text: $searchText, prompt: "Enter Name, Email, or Phone Number")
.autocorrectionDisabled(true)
} detail: { }
}
}
import SwiftData
struct ApplicantListView: View {
@Environment(\.modelContext) private var modelContext
@Query private var applicants: [Applicant]
@State private var newApplicant: Applicant?
init(applicantFilter: String = "") {
// Filters
}
var body: some View {
Group {
if !applicants.isEmpty {
List {
ForEach(applicants) { applicant in
NavigationLink {
ApplicantView(applicant: applicant)
} label: {
HStack {
VStack {
HStack {
Text(applicant.name)
Spacer()
}
HStack {
Text(applicant.phoneNumber)
.font(.caption)
Spacer()
}
HStack {
Text(applicant.email)
.font(.caption)
Spacer()
}
HStack {
Text("Expires: \(formattedDate(applicant.expirationDate))")
.font(.caption)
Spacer()
}
}
if applicant.applicationStatus == ApplicationStatus.approved {
Image(systemName: "checkmark.circle")
.foregroundStyle(.green)
.font(.title)
} else if applicant.applicationStatus == ApplicationStatus.declined {
Image(systemName: "xmark.circle")
.foregroundStyle(.red)
.font(.title)
} else if applicant.applicationStatus == ApplicationStatus.inProgress {
Image(systemName: "hourglass.circle")
.foregroundStyle(.yellow)
.font(.title)
} else if applicant.applicationStatus == ApplicationStatus.waitingForApplicant {
Image(systemName: "person.circle")
.foregroundStyle(.yellow)
.font(.title)
} else {
Image(systemName: "yieldsign")
.foregroundStyle(.yellow)
.font(.title)
}
}
}
}
.onDelete(perform: deleteItems)
}
} else {
ContentUnavailableView {
Label("No Applicants", systemImage: "pencil.fill")
}
}
}
.navigationTitle("Applicants")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addApplicant) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(item: $newApplicant) { applicant in
NavigationStack {
ApplicantView(applicant: applicant, isNew: true)
}
}
}
private func addApplicant() {
withAnimation {
let newItem = Applicant()
modelContext.insert(newItem)
newApplicant = newItem
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(applicants[index])
}
}
}
func formattedDate(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .none
return dateFormatter.string(from: date)
}
}
import SwiftData
@Model
final class Applicant {
var name = ""
var email = ""
var phoneNumber = ""
var applicationDate = Date.now
var expirationDate: Date {
return Calendar.current.date(byAdding: .day, value: 90, to: applicationDate)!
}
How Do I View My CloudKit Invoice?
I'm currently syncing Core Data with the CloudKit public database using NSPersistentCloudKitContainer. The app starts with an empty Core Data store locally and at the app launch it downloads the data from CloudKit public database to the Core Data store, but this can only be accomplished if the user is logged in, if the user is not logged, no data gets downloaded and I get the error below.
Can the NSPersistentCloudKitContainer mirror the data from the CloudKit public database to the local Core Data even if the user is not logged in? Can someone please confirm this is possible?
The reason for my question is because I was under the impression that the users didn't need to be logged to read data from the public database in CloudKit but I'm not sure this applies to NSPersistentCloudKitContainer when mirroring data. I know I can fetch data directly with CloudKit APIs without the user beign logged but I need to understand if NSPersistentCloudKitContainer should in theory work without the user being logged.
I hope someone from Apple sees this question since I have spent too much time researching without any luck.
Error
Error fetching user record ID: <CKError 0x600000cb1b00: "Not Authenticated" (9); "No iCloud account is configured">
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1192): <NSCloudKitMirroringDelegate: 0x600003b00460>: Failed to set up CloudKit integration for store: <NSSQLCore: 0x10700f0d0> (URL: file:///Users/UserName/...
With iOS 18, TipKit got explicit support for syncing tip state via iCloud.
However, before that, TipKit already did iCloud syncing implicitly, as far as I know.
How does the new explicit syncing relate to the previous mechanism? Do we have to enable iCloud syncing manually now to retain the functionality in iOS 18? Is there a way to sync with the state that was already stored by TipKit in iCloud on iOS 17?
I see the following notification in my apple developer account dashboard:
Your Apple Developer Program membership wasn’t renewed successfully.
You can still renew your membership within the next 27 days and your apps will remain available on the App Store during this time. Open the Apple Developer app on your iPhone, iPad, or Mac. Sign in to your account, tap/click Renew, and follow the prompts
when I open the developer app, sign in, there is nowhere to renew it.
I have an app on the appstore with quite a few users wich is depending on my app and I worry they will not get access to it if am not able to renew my membership
I appreciate any help I can get
To reproduce:
In Xcode, create a new project with SwiftData storage
Add a new item in the preview — everything works fine so far
Enable CloudKit sync for the target (add iCloud capability, check CloudKit, add a container)
Go back to the preview and add a new item — Xcode will now freeze
As soon as you modify the SwiftData storage, the preview freezes and the Xcode app becomes extremely slow until you either refresh the preview or restart Xcode.
Can someone please give me an overview of how sync works between Core Data and the public CloudKit database when using the NSPersistentCloudKitContainer and please point out my misunderstandings based on what I describe below?
In the following code, I'm successfully connecting to the public database in CloudKit using the NSPersistentCloudKitContainer. Below is how I have Core Data and CloudKit set up for your reference. In CloudKit I have a set of PublicIconImage that I created manually via the CloudKit Console. I intend to be able to download all images from the public database at the app launch to the local device and manage them via Core Data to minimize server requests, which works but only if the user is logged in.
This is the behavior I see:
When the app launches, all the CloudKit images get mirrored to Core Data and displayed on the screen but only if the user is logged in with the Apple ID, otherwise nothing gets mirrored.
What I was expecting:
I was under the impression that when connecting to the public database in CloudKit you didn't need to be logged in to read data. Now, if the user is logged in on the first launch, all data is successfully mirrored to Core Data, but then if the user logs off, all data previously mirrored gets removed from Core Data, and I was under the impression that since Core Data had the data already locally, it would keep the data already downloaded regardless if it can connect to CloudKit or not.
What am I doing wrong?
Core Data Model:
Entity: PublicIconImage
Attributes: id (UUID), imageName (String), image (Binary Data).
CloudKit Schema in Public Database:
Record: CD_PublicIconImage
Fields: CD_id (String), CD_imageName (String), CD_image (Bytes).
Core Data Manager
class CoreDataManager: ObservableObject{
// Singleton
static let instance = CoreDataManager()
private let queue = DispatchQueue(label: "CoreDataManagerQueue")
private var iCloudSync = true
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
func updateCloudKitContainer() {
queue.sync {
container = setupContainer()
}
}
func setupContainer()->NSPersistentContainer{
let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
let cloudKitContainerIdentifier = "iCloud.com.example.PublicDatabaseTest"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
description.cloudKitContainerOptions?.databaseScope = .public // Specify Public Database
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
View Model Class
class PublicIconImageViewModel: ObservableObject {
let manager: CoreDataManager
@Published var publicIcons: [PublicIconImage] = []
init(coreDataManager: CoreDataManager = .instance) {
self.manager = coreDataManager
loadPublicIcons()
}
func loadPublicIcons() {
let request = NSFetchRequest<PublicIconImage>(entityName: "PublicIconImage")
let sort = NSSortDescriptor(keyPath: \PublicIconImage.imageName, ascending: true)
request.sortDescriptors = [sort]
do {
publicIcons = try manager.context.fetch(request)
} catch let error {
print("Error fetching PublicIconImages. \(error.localizedDescription)")
}
}
}
SwiftUI View
struct ContentView: View {
@EnvironmentObject private var publicIconViewModel: PublicIconImageViewModel
var body: some View {
VStack {
List {
ForEach(publicIconViewModel.publicIcons) { icon in
HStack{
Text(icon.imageName ?? "unknown name")
Spacer()
if let iconImageData = icon.image, let uiImage = UIImage(data: iconImageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 35, height: 35)
}
}
}
}
.onAppear {
// give some time to get the images downlaoded
DispatchQueue.main.asyncAfter(deadline: .now() + 5){
publicIconViewModel.loadPublicIcons()
}
}
}
.padding()
}
}