iCloud & Data

RSS for tag

Learn how to integrate your app with iCloud and data frameworks for effective data storage

CloudKit Documentation

Post

Replies

Boosts

Views

Activity

Persist app state across reinstalls/multiple devices
So I’m trying to setup an existing app to work with iCloud syncing. The syncing part seems to be working for the most part. When the app is first installed it sets up some data locally. Throughout the life of the app additional data is added through updates. There is a user default setup that stores the current data version, then compares it with the new version. If it’s newer, then loads the additional data. The issue I’ve got is a user can delete the app and reinstall, or install on another device which has that data version as 0, prompting another import even though the data in the cloud is current version, resulting in duplicate data once the sync is done. How can I persist that version data? I’ve seen NSUbiquitousKeyValueStore which seems to be a cloud based version of user defaults, but it says not to rely on it if it’s critical to app functions
0
0
84
1w
Does CloudKit guarantee CKRecord.Reference is always valid?
I'm considering using CloudKit in my app (it doesn't use Core Data) and have read as many materials as I can find. I haven't fully grasped it yet and have a basic question on CKRecord.Reference. Does CloudKit guarantee CKRecord.Reference value is always valid? By valid I mean the target CkRecord pointed by the CKRecord.Reference exists in the database. Let's consider an example. Suppose there are two tables: Account and Transaction: Account Table: AccountNumber Currency Rate ------------- -------- ---- a1 USD 0.03 Transaction Table: TransactionNumber AccountNumber Amount ----------------- ------------- ------ t1 a1 20 Now suppose user does the following: User first deletes account a1 and its associated transactions t1 on device A. The device saves the change to cloud. Then user adds a new transaction t2 to account a1 on device B, before the device receives the change made in step 1 from cloud. Since a1 hasn't been deleted on device B, the operation should succeed locally. The device tries to save the change to cloud too. My questions: Q1) Will device B be able to save the change in step 2 to cloud? I hope it would fail, because otherwise it would lead to inconsistent data. But I find the following in CKModifyRecordsOperation doc (emphasis mine), which implies CloudKit allows invalid reference: During a save operation, CloudKit requires that the target record of the parent reference, if set, exists in the database or is part of the same operation; all other reference fields are exempt from this requirement. (BTW, I think the fact that, when using CloudKit, Core Data requires all relations must be optional also indicates that CloudKit can't guarantee relation is always valid, though I think that is mainly an issue on client side caused by data transfer size. The above example, however, is different in that it's an issue on cloud side - the data on cloud is inconsistent). I also find the following in the document. However, I don't think it helps in the above example, because IIUC CloudKit can only detect conflict when the changes on the same record but the changes in step 1 and step 2 are on different records. Because records can change between the time you fetch them and the time you save them, the save policy determines whether new changes overwrite existing changes. By default, the operation reports an error when there’s a newer version on the server. If the above understanding is correct, however, I don't understand why the same document has the following requirement, which implies CloudKit doesn't allow invalid reference: When creating two new records that have a reference between them, use the same operation to save both records at the same time. Q2) Suppose CloudKit allows invalid reference on cloud side (that is, device B successfully saves the change in step 2 to cloud) , I wonder what's the best practice to deal with it? I think the issue is different from the optional relation requirement in Core Data when using CloudKit, because in that case the data is consistent on cloud side and eventually the client will receive complete data. In the above example, however, the data on cloud is inconsistent so the client has to remedy it somehow (although client has little information helping it). One approach I think of is to avoid the issue in the first place. My idea is to maintain a counter in the database and requires client to increase the counter (it's not Lamport clock. BTW, is it possible to use Lamport clock in this case?) when making any change. This should help CloudKit to detect conflict (though I can't think out a good strategy on how client should deal with it. A simple one is perhaps to prompt user to select one copy). However, this approach effectively uses cloud as a centralized server, which I suspect isn't the typical way how people use CloudKit, and it requires clients to maintain local counter value in various situations. I wonder what's the typical approach? Am I missing something? Thanks for any help.
2
0
232
2w
SwiftData ModelConfigurations always sync to iCloud if one of them has iCloud enabled
I'm facing a weird issue with SwiftData. I want to have one database that's local to the device and one that syncs to iCloud. In this example, LTRLink should be synced via iCloud while LTRMetadata should stay on-device only. I've it configured like the following: let schema = Schema([LTRLink.self, LTRMetadata.self]) let cloudkitConfiguration = ModelConfiguration("Remote", schema: schema, url: FileManager.remoteDatabaseFolderURL.appending(path: "Remote.sqlite"), cloudKitDatabase: .private("iCloud.com.xavimoll.abyss3")) let localConfiguration = ModelConfiguration("Local", schema: schema, url: FileManager.localDatabaseFolderURL.appending(path: "Local.sqlite"), cloudKitDatabase: .none) return try ModelContainer(for: schema, configurations: [cloudkitConfiguration, localConfiguration]) For some reason, when I create the iCloud schema, both models end up appearing as records on iCloud. I create the schema like this: let schema = Schema([LTRLink.self, LTRMetadata.self]) let cloudkitConfiguration = ModelConfiguration("Remote", schema: schema, url: FileManager.remoteDatabaseFolderURL.appending(path: "Remote.sqlite"), cloudKitDatabase: .private("iCloud.com.xavimoll.abyss3")) #if DEBUG // Needed to create the schema on iCloud try autoreleasepool { let desc = NSPersistentStoreDescription(url: cloudkitConfiguration.url) let opts = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudkitConfiguration.cloudKitContainerIdentifier!) desc.cloudKitContainerOptions = opts desc.shouldAddStoreAsynchronously = false if let mom = NSManagedObjectModel.makeManagedObjectModel(for: [LTRLink.self]) { let container = NSPersistentCloudKitContainer(name: "Remote", managedObjectModel: mom) container.persistentStoreDescriptions = [desc] container.loadPersistentStores {_, err in if let err { fatalError(err.localizedDescription) } } try container.initializeCloudKitSchema() if let store = container.persistentStoreCoordinator.persistentStores.first { try container.persistentStoreCoordinator.remove(store) } } } #endif let localConfiguration = ModelConfiguration("Local", schema: schema, url: FileManager.localDatabaseFolderURL.appending(path: "Local.sqlite"), cloudKitDatabase: .none) return try ModelContainer(for: schema, configurations: [cloudkitConfiguration, localConfiguration]) The logic to initialize the CloudKit schema follows the documentation found here: https://developer.apple.com/documentation/swiftdata/syncing-model-data-across-a-persons-devices#Initialize-the-CloudKit-development-schema It looks like setting cloudKitDatabase: .none on the init for the ModelConfiguration doesn't do anything, and ends up being synced with iCloud either way. When I go to the iCloud console, I see the following: Does anyone know if there's any workaround that would allow me to have two databases where only one of them syncs to iCloud when using SwiftData?
1
0
202
1w
CKContainer shareParticipant() 'missing' nameComponents
My App requires access to iCloud. I used to be able to get the User's name components (family+given name) using: let dummyZone = CKRecordZone (zoneName: UUID().uuidString) let dummyShare = CKShare (recordZoneID: dummyZone.zoneID) Persistence.logger.notice ("\(#function): Dummy Zone: \(dummyZone.zoneID.zoneName)") // Save the dummyZone and then the dummyShare (for/in the dummyZone) let _ = try await container.privateCloudDatabase.save (dummyZone) let _ = try await container.privateCloudDatabase.save (dummyShare) // Extract the dummyShare's owner's identity - which is 'us/me' let userIdentity = dummyShare.owner.userIdentity where the resulting userIdentity had a filled out nameComponents. Now, recently, it seems to be empty. Did something change in the interfaces? I've also tried, more directly: let userRecordID = try await container.userRecordID() let userParticipant = try await container.shareParticipant(forUserRecordID: userRecordID) let userIdentity = userParticipant.userIdentity and still nameComponents is empty. Given that my App requires iCloud, is there a way to get (familyName,givenName)?
2
0
201
2w
SwiftUI previews and CloudKit sharing
The SwiftUI templates provided by Xcode typically create an in-memory store for preview purposes. I started from one of these templates and added the necessary code for working with CloudKit shares, but now the existing preview store creation gets runtime errors I'm struggling to understand. The ultimate error is: FAULT: NSInternalInconsistencyException: Unsupported feature in this configuration; { store = "<NSSQLCore: 0x12e26b170> (URL: file:///dev/null)"; } There are other things in the log like: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Trip' so +entity is unable to disambiguate. This might be due in part to the normal full stack being instantiated during a unit test, but that was the only way I could step through the code to try to see what was causing the SwiftUI preview to crash. I can't build the full core data stack as normal, because then the unit tests and previews pollute the real store.
1
0
145
1w
CloudKit sharing limits?
When you share records, they get put into a new zone. Creating a zone for the share makes sense to me, but I thought I read that there was a limit to the number of zones one could have (something like 1024). Does this mean a user can’t share more than 1024 separate items with 1024 different people? I assume any other items shared with the same group end up in an existing zone.
0
0
111
1w
Thumbnails for images saved in core data
I'm trying to look at what the best way to do thumbnails for images that are saved in core data, which are being synced across multiple devices. I know I can save a lower quality version into core data, but I'm wondering if there's a better way of doing it. I've come across quick look thumbnailing which looks like what I want, but I'm not sure if it can be adapted for core data as its using file paths, whereas the images are stored in a data type property in core data. From what I can tell, I'd have to save the image locally, produce the thumbnail, then delete the local image
0
0
103
1w
iOS 18 SwiftData Bug: Codable Models Cause Relationship Mapping Error
Here we have yet another bug, I suppose, in SwiftData that happens on iOS18 but it is not an issue on iOS17. There are 2 models defined as follows @Model final public class Note: Identifiable, Codable, Hashable { public private(set) var uuid = UUID().uuidString var heading: String = "" var tags: [Tag]? init(heading: String = "") { self.heading = heading } required public init(from decoder: Decoder) throws { ... } public func encode(to encoder: Encoder) throws { ... } } @Model final public class Tag: Identifiable, Codable { var name: String = "" @Relationship(deleteRule: .nullify, inverse: \Note.tags) var notes: [Note]? init(_ name: String) { self.name = name } required public init(from decoder: Decoder) throws { … } public func encode(to encoder: Encoder) throws { ... } } and a function o add new tags as follows private func addTags(note: Note, tagNames: [String]) { if note.tags == nil { note.tags = [] } for tagName in tagNames { if let tag = fetchTag(tagName) { if !note.tags!.contains(where: {$0.name == tagName}) { note.tags!.append(tag) } } else { // The following line throws the exception on iOS18 when Tag conforms to Codable: // Illegal attempt to map a relationship containing temporary objects to its identifiers. note.tags!.append(Tag(tagName)) } } } This code works perfectly well on iOS17 but on iOS18 I get the exception “Illegal attempt to map a relationship containing temporary objects to its identifiers.” What I noticed that this happens only when Tag model conforms to Codable protocol. Is it a bug? It looks like, otherwise we've got some undocumented changes have been made. In my previous post I mentioned about the other issue about ModelContext that is broken too on iOS18 - I mean it works perfectly well on iOS17. Demo app with an example how to workaround this problem is available here on GitHub. Repro steps: Add a note with some tags (separated by space) Edit this note and add a new tag (tag that does not exists in database) and tap Save. You should noticed that the tag hasn't been added. It works occasionally but hardly to be seen.
3
5
427
Sep ’24
UIImage causes memory to run out
I have a project that currently has data saved locally and I'm trying to get it to sync over multiple devices. Currently basic data is syncing perfectly fine, but I'm having issues getting the images to convert to data. From what I've researched it because I'm using a UIImage to convert and this caches the image It works fine when there's only a few images, but if there's several its a pain The associated code func updateLocalImages() { autoreleasepool { let fetchRequest: NSFetchRequest&lt;Project&gt; = Project.fetchRequest() fetchRequest.predicate = NSPredicate(format: "converted = %d", false) fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Project.statusOrder?.sortOrder, ascending: true), NSSortDescriptor(keyPath: \Project.name, ascending: true)] do { let projects = try viewContext.fetch(fetchRequest) for project in projects { currentPicNumber = 0 currentProjectName = project.name ?? "Error loading project" if let pictures = project.pictures { projectPicNumber = pictures.count for pic in pictures { currentPicNumber = currentPicNumber + 1 let picture : Picture = pic as! Picture if let imgData = convertImage(picture: picture) { picture.pictureData = imgData } } project.converted = true saveContext() } } } catch { print("Fetch Failed") } } } func convertImage(picture : Picture)-&gt; Data? { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let path = paths[0] if let picName = picture.pictureName { let imagePath = path.appendingPathComponent(picName) if let uiImage = UIImage(contentsOfFile: imagePath.path) { if let imageData = uiImage.jpegData(compressionQuality: 0.5) { return imageData } } } return nil }```
2
0
156
2w
ckqueryoperation in CloudKit crashing
Use CloudKit's ckqueryoperation's recordmatchedblock in Swift 6.0, which always crashes, but works fine in Swift 5: func fetchAllRecords() async throws { let predicate = NSPredicate(format: "Topics = %@", "Integrations") let query = CKQuery(recordType: "PureMList", predicate: predicate) let operation = CKQueryOperation(query: query) operation.recordMatchedBlock = { recordID, result in switch result { case .success(let record): DispatchQueue.main.async { // Ensure UI updates happen here print("Fetched record: \(record)") // Update your UI elements here } case .failure(let error): // Handle the error print("Error fetching record with ID \(recordID): \(error)") } } // Ensure you're using the correct database publicDatabase.add(operation) }
2
0
173
2w
Error message when opening a SwiftData ModelContainer
I'm seeing these errors in the console when calling ModelContainer(for:migrationPlan:configurations) for iOS 18: error: Attempting to retrieve an NSManagedObjectModel version checksum while the model is still editable. This may result in an unstable verison checksum. Add model to NSPersistentStoreCoordinator and try again. CoreData: error: Attempting to retrieve an NSManagedObjectModel version checksum while the model is still editable. This may result in an unstable verison checksum. Add model to NSPersistentStoreCoordinator and try again. Is this anything to be concerned about? (Side note: "version" is misspelled in "verison checksum")
4
0
467
4w
Core Data not returning results in ShieldConfiguration Extension, but works fine in other extensions
Hi everyone, I’m using Core Data in several extensions (DeviceActivityMonitor, ShieldAction, and ShieldConfiguration). It works perfectly in DeviceActivityMonitor and ShieldAction. I’m able to successfully fetch data and log the correct count using a fetch request. However, when I try the same setup in the ShieldConfiguration extension, the fetch request always returns 0 results. The CoreData and App Group setup appears to be correct since the first two extensions fetch the expected data. I’ve also previously tested storing the CoreData objects separately in a JSON-FIle using FileManager and it worked without issues—though I’d prefer not to handle manual encoding/decoding if possible. The documentation mentions that the extension runs in a sandbox, restricting network requests or moving sensitive content. But shouldn’t reading data (from a shared App Group, for instance) still be possible within the sandbox, as it is the case with the Files, what is the difference there? In my case, I only need to read the data, as modifications can be handled via ShieldActionExtension. Any help would be greatly appreciated!
2
0
214
2w
SwiftData updates in the background are not merged in the main UI context
Hello, SwiftData is not working correctly with Swift Concurrency. And it’s sad after all this time. I personally found a regression. The attached code works perfectly fine on iOS 17.5 but doesn’t work correctly on iOS 18 or iOS 18.1. A model can be updated from the background (Task, Task.detached or ModelActor) and refreshes the UI, but as soon as the same item is updated from the View (fetched via a Query), the next background updates are not reflected anymore in the UI, the UI is not refreshed, the updates are not merged into the main. How to reproduce: Launch the app Tap the plus button in the navigation bar to create a new item Tap on the “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times Notice the time is updated Tap on the “Update from View” (once or many times) Notice the time is updated Tap again on “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times Notice that the time is not update anymore Am I doing something wrong? Or is this a bug in iOS 18/18.1? Many other posts talk about issues where updates from background thread are not merged into the main thread. I don’t know if they all are related but it would be nice to have 1/ bug fixed, meaning that if I update an item from a background, it’s reflected in the UI, and 2/ proper documentation on how to use SwiftData with Swift Concurrency (ModelActor). I don’t know if what I’m doing in my buttons is correct or not. Thanks, Axel import SwiftData import SwiftUI @main struct FB_SwiftData_BackgroundApp: App { var body: some Scene { WindowGroup { ContentView() .modelContainer(for: Item.self) } } } struct ContentView: View { @Environment(\.modelContext) private var modelContext @State private var simpleModelActor: SimpleModelActor! @Query private var items: [Item] var body: some View { NavigationView { VStack { if let firstItem: Item = items.first { Text(firstItem.timestamp, format: Date.FormatStyle(date: .omitted, time: .standard)) .font(.largeTitle) .fontWeight(.heavy) Button("Update from Task") { let modelContainer: ModelContainer = modelContext.container let itemID: Item.ID = firstItem.persistentModelID Task { let context: ModelContext = ModelContext(modelContainer) guard let itemInContext: Item = context.model(for: itemID) as? Item else { return } itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000)) try context.save() } } .buttonStyle(.bordered) Button("Update from Detached Task") { let container: ModelContainer = modelContext.container let itemID: Item.ID = firstItem.persistentModelID Task.detached { let context: ModelContext = ModelContext(container) guard let itemInContext: Item = context.model(for: itemID) as? Item else { return } itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000)) try context.save() } } .buttonStyle(.bordered) Button("Update from ModelActor") { let container: ModelContainer = modelContext.container let persistentModelID: Item.ID = firstItem.persistentModelID Task.detached { let actor: SimpleModelActor = SimpleModelActor(modelContainer: container) await actor.updateItem(identifier: persistentModelID) } } .buttonStyle(.bordered) Button("Update from ModelActor in State") { let container: ModelContainer = modelContext.container let persistentModelID: Item.ID = firstItem.persistentModelID Task.detached { let actor: SimpleModelActor = SimpleModelActor(modelContainer: container) await MainActor.run { simpleModelActor = actor } await actor.updateItem(identifier: persistentModelID) } } .buttonStyle(.bordered) Divider() .padding(.vertical) Button("Update from View") { firstItem.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000)) } .buttonStyle(.bordered) } else { ContentUnavailableView( "No Data", systemImage: "slash.circle", // 􀕧 description: Text("Tap the plus button in the toolbar") ) } } .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } private func addItem() { modelContext.insert(Item(timestamp: Date.now)) try? modelContext.save() } } @ModelActor final actor SimpleModelActor { var context: String = "" func updateItem(identifier: Item.ID) { guard let item = self[identifier, as: Item.self] else { return } item.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000)) try! modelContext.save() } } @Model final class Item: Identifiable { var timestamp: Date init(timestamp: Date) { self.timestamp = timestamp } }
0
0
275
2w
Invalid bundle ID for container
I'm trying to get the CoreDataCloudKitShare example to work, but having trouble. The first error I see when running the InitializeCloudKitSchema target (on macOS) is the following: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1242): <NSCloudKitMirroringDelegate: 0x60000229c0f0>: Failed to set up CloudKit integration for store: <NSSQLCore: 0x15b807830> (URL: file:///Users/rmann/Library/Application%20Support/InitializeCloudKitSchema/CoreDataStores/Private/private.sqlite) <CKError 0x600001311290: "Partial Failure" (2/1011); "Failed to modify some record zones"; uuid = 3E1B1380-AE1C-4B14-97A8-7F60B4A8F3EF; container ID = "iCloud.com.example.CoreDataCloudKitShareH6F2W964VK"; partial errors: { com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x60000132f810: "Permission Failure" (10/2007); server message = "Invalid bundle ID for container"; op = F3987848B25CEED7; uuid = 3E1B1380-AE1C-4B14-97A8-7F60B4A8F3EF> }> I see a database in the Dashboard with that container ID, but don't know what it means by "Invalid bundle ID for container". I've seen several other posts about this across the web, and the only answer is ever "seems to be an Apple issue, wait a bit."
1
0
183
2w
Unexpected “OTHER” error in CloudKit despite app functioning normally
Hello everyone, I’ve recently encountered an issue where my app is working perfectly fine, but I’m seeing an “OTHER” error in the CloudKit dashboard under errors. I’ve checked the logs and there doesn’t seem to be any obvious failure or issue affecting the app’s functionality. The error doesn’t provide much detail, and I’m having trouble identifying the root cause since everything appears to be functioning as expected in the app. Has anyone else experienced this? Is this something that could be related to a server-side issue, or am I missing something on my end? Any insights or advice would be greatly appreciated! Thanks in advance!
5
1
284
Sep ’24
How does one create a DataStoreSnapshot for a custom data store ?
The WWDC2024 custom data store example doesn't provide any details on how one would go about creating a DataStoreSnapshot. The example uses a DefaultSnapshot for persisting the data in the DefaultSnapshot format directly in the JSON file. There appears to be no documentation or examples of how one might create a DataStoreSnapshot from data from another database. The Apple documentation for DefaultSnapshot provides no examples of how one might create such a snapshot from data retrieved elsewhere. Can anyone provide a simple example of how one might create such a snapshot from a remote database such that it can be returned as part of the response to a fetch request. For the purpose of this example let's assume I have a CSV file with rows of data and code to read the data from this file. How would I create a snapshot or snapshots for each of the rows of data.
0
0
169
2w
SwiftData - Problems with Predicates and Relations
Hello, I have a problem with SwiftData and Predicates that check for the persistentModelID of the relations. My data model looks simplified like this: Day -> TimeEntry[] -> Hashtag[] What I want to achieve is to query the days and associated time entries via assigned tags. This is my predicate: let identifier = filterHashtags.map(\.persistentModelID) ... #Predicate<TimeEntry> { timeEntry in identifiers.count == timeEntry.tags.filter { tag in identifiers.contains(tag.persistentModelID) }.count } It does not return any data when I check for the persistentModelID. However, if I use another property of the tags, e.g. the name or a generated UUID for the check, the predicate works. Is this a general problem with PersistentIdentifier in Predicates or am I missing something? Thanks in advance
3
0
173
2w