iCloud & Data

RSS for tag

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

CloudKit Documentation

Posts under iCloud & Data subtopic

Post

Replies

Boosts

Views

Activity

SwiftData equivalent of NSFetchedResultsController
As far as I can tell, there’s no equivalent to Core Data’s NSFetchedResultsController in SwiftData. It would be very helpful to have a way to respond to sets of changes in a ModelContext outside of a SwiftUI view. For instance, this is very helpful when using a Model-View-ViewModel architecture. The @Query property wrapper is great for use in a SwiftUI view, but sometimes it’s helpful to process data outside of the view itself. The fetch() method on ModelContext is helpful for one-time operations, but as far as I can't tell it doesn't address receiving changes on an ongoing basis. Am I missing some equivalent for this use case? Also filed as feedback FB12288916
2
3
2.1k
Nov ’24
Persistent CloudKit Internal Error
I have integrated CloudKit into a CoreData application and am ready to deploy the schema to production but keep getting an "internal error" when trying to deploy to production or reset my CloudKit environment. I have attached images of what I am seeing including one of the console error. Is there any way to resolve this?
5
1
639
Jan ’25
SwiftUI & SwiftData: Fatal Error "Duplicate keys of type" Occurs on First Launch
I'm developing a SwiftUI app using SwiftData and encountering a persistent issue: Error Message: Thread 1: Fatal error: Duplicate keys of type 'Bland' were found in a Dictionary. This usually means either that the type violates Hashable's requirements, or that members of such a dictionary were mutated after insertion. Details: Occurrence: The error always occurs on the first launch of the app after installation. Specifically, it happens approximately 1 minute after the app starts. Inconsistent Behavior: Despite no changes to the code or server data, the error occurs inconsistently. Data Fetching Process: I fetch data for entities (Bland, CrossZansu, and Trade) from the server using the following process: Fetch Bland and CrossZansu entities via URLSession. Insert or update these entities into the SwiftData context. The fetched data is managed as follows: func refleshBlandsData() async throws { if let blandsOnServer = try await DataModel.shared.getBlands() { await MainActor.run { blandsOnServer.forEach { blandOnServer in if let blandOnLocal = blandList.first(where: { $0.code == blandOnServer.code }) { blandOnLocal.update(serverBland: blandOnServer) } else { modelContext.insert(blandOnServer.bland) } } } } } This is a simplified version of my StockListView. The blandList is a @Query property and dynamically retrieves data from SwiftData: struct StockListView: View { @Environment(\.modelContext) private var modelContext @Query(sort: \Bland.sname) var blandList: [Bland] @Query var users: [User] @State private var isNotLoaded = true @State private var isLoading = false @State private var loadingErrorState = "" var body: some View { NavigationStack { List { ForEach(blandList, id: \.self) { bland in NavigationLink(value: bland) { Text(bland.sname) } } } .navigationTitle("Stock List") .onAppear { doIfFirst() } } } // This function handles data loading when the app launches for the first time func doIfFirst() { if isNotLoaded { loadDataWithAnimationIfNotLoading() isNotLoaded = false } } // This function ensures data is loaded with an animation and avoids multiple triggers func loadDataWithAnimationIfNotLoading() { if !isLoading { isLoading = true Task { do { try await loadData() } catch { // Capture and store any errors during data loading loadingErrorState = "Data load failed: \(error.localizedDescription)" } isLoading = false } } } // Fetch data from the server and insert it into the SwiftData model context func loadData() async throws { if let blandsOnServer = try await DataModel.shared.getBlands() { for bland in blandsOnServer { // Avoid inserting duplicate keys by checking for existing items in blandList if !blandList.contains(where: { $0.code == bland.code }) { modelContext.insert(bland.bland) } } } } } Entity Definitions: Here are the main entities involved: Bland: @Model class Bland: Identifiable { @Attribute(.unique) var code: String var sname: String @Relationship(deleteRule: .cascade, inverse: \CrossZansu.bland) var zansuList: [CrossZansu] @Relationship(deleteRule: .cascade, inverse: \Trade.bland) var trades: [Trade] } CrossZansu: @Model class CrossZansu: Equatable { @Attribute(.unique) var id: String var bland: Bland? } Trade: @Model class Trade { @Relationship(deleteRule: .nullify) var user: User? var bland: Bland } User: class User { var id: UUID @Relationship(deleteRule: .cascade, inverse: \Trade.user) var trades: [Trade] } Observations: Error Context: The error occurs after the data is fetched and inserted into SwiftData. This suggests an issue with Hashable requirements or duplicate keys being inserted unintentionally. Concurrency Concerns: The fetch and update operations are performed in asynchronous tasks. Could this cause race conditions? Questions: Could this issue be related to how @Relationship and @Attribute(.unique) are managed in SwiftData? What are potential pitfalls with Equatable implementations (e.g., in CrossZansu) when used in SwiftData entities? Are there any recommended approaches for debugging "Duplicate keys" errors in SwiftData? Additional Info: Error Timing: The error occurs only during the app's first launch and consistently within the first minute.
3
1
544
Apr ’25
Xcode 16 broke my SwiftData application
I'm building an application with SwiftUI and SwiftData. Up until a couple days ago, everything was working fine. Then, Xcode auto-updated to v16 in the background, and the next time I opened Xcode and tried to build my app it wouldn't build anymore, citing some errors in expanding the SwiftData @Model macro on one of my objects. Attached are the errors specifically, shown where Xcode shows them (in the expanded @Model macro). In text, they are: Instance method 'access(_:keyPath:)' requires that 'Member' conform to 'Observable' Cannot convert value of type 'Risers.Member' to expected argument type 'Member' Instance method 'withMutation(of:keyPath:_:)' requires that 'Member' conform to 'Observable' Cannot convert value of type 'Risers.Member' to expected argument type 'Member' Here is the SwiftData class in full: import SwiftData import SwiftUI @Model class Member: Identifiable, Hashable { var chorus: Chorus? var id = UUID() var firstName: String var lastName: String var fullName: String { "\(firstName) \(lastName)" } var voicePart: Int var voicePartString: String? { chorus?.voicePartType.prettyName(forPart: voicePart) } @Attribute(.externalStorage) var pictureData: Data init(chorus: Chorus? = nil, firstName: String = "", lastName: String = "", voicePart: Int = 1, pictureData: Data = Data()) { self.chorus = chorus self.firstName = firstName self.lastName = lastName self.voicePart = voicePart self.pictureData = pictureData } init(member: Member) { self.chorus = member.chorus self.firstName = member.firstName self.lastName = member.lastName self.voicePart = member.voicePart self.pictureData = member.pictureData } I tried building again on Xcode 15.4, and it still builds successfully there. Xcode 16.1 beta has not made a difference. Is this my fault, or is Xcode 16 broken?
4
1
1k
Sep ’24
Persistent CloudKit Server-to-Server INTERNAL_ERROR (500) Despite Correct Key Parsing & Request Formatting for /users/current
Hello Devs, I'm encountering a persistent INTERNAL_ERROR (HTTP 500) when making Server-to-Server API calls to CloudKit, specifically when trying to hit the /users/current endpoint, even after meticulously verifying all client-side components. I'm hoping someone might have insight into what could cause this. Context: Goal: Authenticate to CloudKit from a Vercel Serverless Function (Node.js) to perform operations like record queries. Problem Endpoint: POST https://api.apple-cloudkit.com/database/1/iCloud.com.dannybaseball.Danny-Baseball/production/public/users/current Key Generation Method: Using the CloudKit Dashboard's "Tokens & Keys" -> "New Server-to-Server Key" flow, where I generate the private key using openssl ecparam -name prime256v1 -genkey -noout -out mykey.pem, then extract the public key using openssl ec -in mykey.pem -pubout, and paste the public key material (between BEGIN/END markers) into the dashboard. The private key was then converted to PKCS#8 format using openssl pkcs8 -topk8 -nocrypt -in mykey.pem -out mykey_pkcs8.pem. Current Setup Being Tested (in a Vercel Node.js function): CLOUDKIT_CONTAINER: iCloud.com.dannybaseball.Danny-Baseball CLOUDKIT_KEY_ID: 9368dddf141ce9bc0da743b9f69bc3eda132b9bb3e62a4167e428d4f320b656e (This is the Key ID generated from the CloudKit Dashboard for the public key I provided). CLOUDKIT_P8_KEY (Environment Variable): Contains the base64 encoded string of the entire content of my PKCS#8 formatted private key file. Key Processing in Code: const p8Base64 = process.env.CLOUDKIT_P8_KEY; const privateKeyPEM = Buffer.from(p8Base64, 'base64').toString('utf8'); // This privateKeyPEM string starts with "-----BEGIN PRIVATE KEY-----" and ends with "-----END PRIVATE KEY-----" const privateKey = crypto.createPrivateKey({ key: privateKeyPEM, format: 'pem' }); // This line SUCCEEDS without DECODER errors in my Vercel function logs. Use code with caution. JavaScript Request Body for /users/current: "{}" Signing String (message = Date:BodyHash:Path): Date: Correct ISO8601 format (e.g., "2025-05-21T19:38:11.886Z") BodyHash: Correct SHA256 hash of "{}", then Base64 encoded (e.g., "RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=") Path: Exactly /database/1/iCloud.com.dannybaseball.Danny-Baseball/production/public/users/current Headers: X-Apple-CloudKit-Request-KeyID: Set to the correct Key ID. X-Apple-CloudKit-Request-ISO8601Date: Set to the date used in the signature. X-Apple-CloudKit-Request-SignatureV1: Set to the generated signature. X-Apple-CloudKit-Environment: "production" Content-Type: "application/json" Observed Behavior & Logs: The Node.js crypto.createPrivateKey call successfully parses the decoded PEM key in my Vercel function. The request is sent to CloudKit. CloudKit responds with HTTP 500 and the following JSON body (UUID varies per request): { "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "serverErrorCode": "INTERNAL_ERROR" } Use code with caution. Json This happens consistently. Previously, with other key pairs or different P8 processing attempts, I was getting AUTHENTICATION_FAILED (401) or local DECODER errors. Now that the key parsing is successful on my end with this current key pair and setup, I'm hitting this INTERNAL_ERROR. Troubleshooting Done: Verified Key ID (9368dddf...) is correct and corresponds to the key generated via CloudKit Dashboard. Verified Container ID (iCloud.com.dannybaseball.Danny-Baseball) is correct. Successfully parsed the private key from the environment variable (after base64 decoding) within the Vercel function. Meticulously checked the signing string components (Date, BodyHash, Path) against Apple's documentation. Path format is /database/1////. Ensured all required headers are present with correct values. Local Node.js tests (bypassing Vercel but using the same key data and signing logic) also result in this INTERNAL_ERROR. Question: What could cause CloudKit to return an INTERNAL_ERROR (500) for a /users/current request when the client-side key parsing is successful and all request components (path, body hash for signature, date, headers) appear to conform exactly to the Server-to-Server Web Services Reference? Are there any known subtle issues with EC keys generated via openssl ecparam (and then converted to PKCS#8) that might lead to this, even if crypto.createPrivateKey parses them in Node.js? Could there be an issue with my specific Key ID or container that would manifest this way, requiring Apple intervention? Any insights or suggestions would be greatly appreciated. I can provide more detailed logs of the request components if needed. Thank you!
1
1
75
May ’25
SwiftData migration crashes when working with relationships
The following complex migration consistently crashes the app with the following error: SwiftData/PersistentModel.swift:726: Fatal error: What kind of backing data is this? SwiftData._KKMDBackingData<SwiftDataMigration.ItemSchemaV1.ItemList> My app relies on a complex migration that involves these optional 1 to n relationships. Theoretically I could not assign the relationships in the willMigrate block but afterwards I am not able to tell which list and items belonged together. Steps to reproduce: Run project Change typealias CurrentSchema to ItemSchemaV2 instead of ItemSchemaV1. Run project again -> App crashes My setup: Xcode Version 16.2 (16C5032a) MacOS Sequoia 15.4 iPhone 12 with 18.3.2 (22D82) Am I doing something wrong or did I stumble upon a bug? I have a demo Xcode project ready but I could not upload it here so I put the code below. Thanks for your help typealias CurrentSchema = ItemSchemaV1 typealias ItemList = CurrentSchema.ItemList typealias Item = CurrentSchema.Item @main struct SwiftDataMigrationApp: App { var sharedModelContainer: ModelContainer = { do { return try ModelContainer(for: ItemList.self, migrationPlan: MigrationPlan.self) } catch { fatalError("Could not create ModelContainer: \(error)") } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(sharedModelContainer) } } This is the migration plan enum MigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [ItemSchemaV1.self, ItemSchemaV2.self] } static var stages: [MigrationStage] = [ MigrationStage.custom(fromVersion: ItemSchemaV1.self, toVersion: ItemSchemaV2.self, willMigrate: { context in print("Started migration") let oldlistItems = try context.fetch(FetchDescriptor<ItemSchemaV1.ItemList>()) for list in oldlistItems { let items = list.items.map { ItemSchemaV2.Item(timestamp: $0.timestamp)} let newList = ItemSchemaV2.ItemList(items: items, name: list.name, note: "This is a new property") context.insert(newList) context.delete(list) } try context.save() // Crash indicated here print("Finished willMigrate") }, didMigrate: { context in print("Did migrate successfully") }) ] } The versioned schemas enum ItemSchemaV1: VersionedSchema { static var versionIdentifier = Schema.Version(1, 0, 0) static var models: [any PersistentModel.Type] { [Item.self] } @Model final class Item { var timestamp: Date var list: ItemSchemaV1.ItemList? init(timestamp: Date) { self.timestamp = timestamp } } @Model final class ItemList { @Relationship(deleteRule: .cascade, inverse: \ItemSchemaV1.Item.list) var items: [Item] var name: String init(items: [Item], name: String) { self.items = items self.name = name } } } enum ItemSchemaV2: VersionedSchema { static var versionIdentifier = Schema.Version(2, 0, 0) static var models: [any PersistentModel.Type] { [Item.self] } @Model final class Item { var timestamp: Date var list: ItemSchemaV2.ItemList? init(timestamp: Date) { self.timestamp = timestamp } } @Model final class ItemList { @Relationship(deleteRule: .cascade, inverse: \ItemSchemaV2.Item.list) var items: [Item] var name: String var note: String init(items: [Item], name: String, note: String = "") { self.items = items self.name = name self.note = note } } } Last the ContentView: struct ContentView: View { @Query private var itemLists: [ItemList] var body: some View { NavigationSplitView { List { ForEach(itemLists) { list in NavigationLink { List(list.items) { item in Text(item.timestamp.formatted(date: .abbreviated, time: .complete)) } .navigationTitle(list.name) } label: { Text(list.name) } } } .navigationTitle("Crashing migration demo") .onAppear { if itemLists.isEmpty { for index in 0..<10 { let items = [Item(timestamp: Date.now)] let listItem = ItemList(items: items, name: "List No. \(index)") modelContext.insert(listItem) } try! modelContext.save() } } } detail: { Text("Select an item") } } }
1
1
102
Apr ’25
SwiftData crashes on fetchHistory
Hi, would it be possible that instead of crashing when calling fetchHistory that function simply throws an error instead? fetchHistory seems to crash when it cannot understand the models if they are not compatible etc… which is understandable, but it makes it really difficult to handle and debug, there's not a lot of details, and honestly I would just rather that it throws an error and let me ignore a history entry that might be useless rather than crashing the entire app. Thank you!
1
1
56
Apr ’25
How to diagnose spurious SwiftDataMacros error
I have a Package.swift file that builds and runs from Xcode 15.2 without issue but fails to compile when built from the command line ("swift build"). The swift version is 6.0.3. I'm at wits end trying to diagnose this and would welcome any thoughts. The error in question is error: external macro implementation type 'SwiftDataMacros.PersistentModelMacro' could not be found for macro 'Model()'; plugin for module 'SwiftDataMacros' not found The code associated with the module is very vanilla. import Foundation import SwiftData @Model public final class MyObject { @Attribute(.unique) public var id:Int64 public var vertexID:Int64 public var updatedAt:Date public var codeUSRA:Int32 init(id:Int64, vertexID:Int64, updatedAt:Date, codeUSRA:Int32) { self.id = id self.vertexID = vertexID self.updatedAt = updatedAt self.codeUSRA = codeUSRA } public static func create(id:Int64, vertexID:Int64, updatedAt:Date, codeUSRA:Int32) -> MyObject { MyObject(id: id, vertexID: vertexID, updatedAt: updatedAt, codeUSRA: codeUSRA) } } Thank you.
1
1
302
Apr ’25
CloudKit: Records not indexing
Since publishing new record types to my CloudKit schema in production, a previously unchanged record type has stopped indexing new records. While records of this type are successfully saved without errors, they are not returned in query results—they can only be accessed directly via their recordName. This issue occurs exclusively in the Production environment, both in the CloudKit Console and our iOS app. The problem began on July 21, 2025, and continues to persist. The issue affects only new records of this specific record type; all other types are indexing and querying as expected. The affected record's fields are properly configured with the appropriate index types (e.g., QUERYABLE) and have been not been modified prior to publishing the schema. With this, are there any steps I should take to restore indexing functionality for this record type in Production? There have been new records inserted, and I would prefer to not have to reset the production database, if possible.
4
1
182
4w
Suspicious CloudKit Telemetry Data
Starting 20th March 2025, I see an increase in bandwidth and latency for one of my CloudKit projects. I'm using NSPersistentCloudKitContainer to synchronise my data. I haven't changed any CloudKit scheme during that time but shipped an update. Since then, I reverted some changes from that update, which could have led to changes in the sync behaviour. Is anyone else seeing any issues? I would love to file a DTS and use one of my credits for that, but unfortunately, I can't because I cannot reproduce it with a demo project because I cannot travel back in time and check if it also has an increase in metrics during that time. Maybe an Apple engineer can green-light me filing a DTS request, please.
0
1
96
Apr ’25
File Provider Extension trouble
Hi everyone, I am a beginner in iOS/Swift programming. I'm trying to develop a mobile application that allows to mount a network drive in the iphone Files application via the WebDav protocol. I saw on the internet that WebDav is no longer implemented in iOS because considered deprecated by apple. To accomplish this task, I decided to separate responsibilities as follows: Framework: WebDav (responsible for communication with the WebDav server) FileProviderExtension: FileBridge (Responsible for bridging the gap between the WebDav Framework and the iOS Files app) Main App I also have an AppGroup that includes the main application and the fileproviderextension Initially, to measure the feasibility and complexity of this task, I'd like to make a simplistic version that simply displays the files on my drive in the Files app, without necessarily being able to interact with them. FileProviderExtension.swift: import FileProvider import WebDav class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension { private var webDavService: WebDavService? required init(domain: NSFileProviderDomain) { super.init() self.webDavService = WebDavService(baseURL: URL(string: "https://www.mydrive.com/drive")!) } func invalidate() { // TODO: cleanup any resources } func item(for identifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress { let progress = Progress(totalUnitCount: 1) Task { do { if let items = try await webDavService?.propfind(path: identifier.rawValue, depth: 1), let item = items.first(where: { $0.itemIdentifier == identifier }) { completionHandler(item, nil) } else { completionHandler(nil, NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)) } } catch { completionHandler(nil, error) } } return progress } func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress { let progress = Progress(totalUnitCount: 1) Task { do { guard let service = webDavService else { throw WebDavError.invalidResponse } let data = try await service.get(fileAt: itemIdentifier.rawValue) let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(itemIdentifier.rawValue) try data.write(to: tempURL) completionHandler(tempURL, nil, nil) } catch { completionHandler(nil, nil, error) } } return progress } func createItem(basedOn itemTemplate: NSFileProviderItem, fields: NSFileProviderItemFields, contents url: URL?, options: NSFileProviderCreateItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress { // TODO: a new item was created on disk, process the item's creation completionHandler(itemTemplate, [], false, nil) return Progress() } func modifyItem(_ item: NSFileProviderItem, baseVersion version: NSFileProviderItemVersion, changedFields: NSFileProviderItemFields, contents newContents: URL?, options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress { // TODO: an item was modified on disk, process the item's modification completionHandler(nil, [], false, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])) return Progress() } func deleteItem(identifier: NSFileProviderItemIdentifier, baseVersion version: NSFileProviderItemVersion, options: NSFileProviderDeleteItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (Error?) -> Void) -> Progress { // TODO: an item was deleted on disk, process the item's deletion completionHandler(NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])) return Progress() } func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest) throws -> NSFileProviderEnumerator { return FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier, service: webDavService) } } Here's the code I use to initialize my domain in the main app files: fileprivate func registerFileProviderDomain() { let domainIdentifier = NSFileProviderDomainIdentifier("FileProviderExtension Bundle Identifier") let domain = NSFileProviderDomain(identifier: domainIdentifier, displayName: "My Drive") NSFileProviderManager.add(domain) { error in NSFileProviderManager.add(domain) { error in if let error = error { print("Error cannot add domain file provider : \(error.localizedDescription)") } else { print("Success domain file provider added") } } } I can't get rid of the Error : Error cannot add domain file provider : The operation couldn’t be completed. Invalid argument. I don't know what I'm missing Please help me understand
1
1
962
Jan ’25
SwiftData does not work on a background Task even inside a custom ModelActor.
I have created an actor for the ModelContainer, in order to perform a data load when starting the app in the background. For this I have conformed to the ModelActor protocol and created the necessary elements, even preparing for test data. Then I create a function of type async throws to perform the database loading processes and everything works fine, in that the data is loaded and when loaded it is displayed reactively. actor Container: ModelActor { nonisolated let modelContainer: ModelContainer nonisolated let modelExecutor: ModelExecutor static let modelContainer: ModelContainer = { do { return try ModelContainer(for: Empleados.self) } catch { fatalError() } }() let context: ModelContext init(container: ModelContainer = Container.modelContainer) { self.modelContainer = container let context = ModelContext(modelContainer) self.modelExecutor = DefaultSerialModelExecutor(modelContext: context) self.context = context Task { do { try await loadData() } catch { print("Error en la carga \(error)") } } } } The problem is that, in spite of doing the load inside a Task and that there is no problem, when starting the app it stops responding the UI while loading to the user interactions. Which gives me to understand that actually the task that should be in a background thread is running somehow over the MainActor. As I have my own API that will provide the information to my app and refresh it at each startup or even send them in Batch when the internet connection is lost and comes back, I don't want the user to be continuously noticing that the app stops because it is performing a heavy process that is not really running in the background. Tested and compiled on Xcode 15 beta 7. I made a Feedback for this: FB13038621. Thanks Julio César
8
1
9k
Dec ’24
Any way to force-refresh @Query properties in SwiftUI?
I'm wondering if there is a way to force a re-fetch of a @Query property inside of a SwiftUI view so I could offer a pull-to-refresh mechanism for users to force-refresh a view. Why would I want this? iOS 18.0 and 18.1 currently contain some regressions that prevent SwiftData from properly gathering model updates caused by ModelActor's running on background threads and the suggested workarounds (listening for .NSManagedObjectContextDidSave) don't work well in most scenarios and do not cause queries in the current view to be fully re-evaluated (see Importing Data into SwiftData in the Background Using ModelActor and @Query).
1
2
694
Oct ’24
Is it possible to track history using HistoryDescriptor in SwiftData?
Is it possible to track history using the new HistoryDescriptor feature in SwiftData? Or can I only get the current most recent data? Or is it possible to output the changed data itself, along with timestamps? I am hoping that it is possible to track by a standard feature like NSPersistentHistoryTransaction in CoreData. Do we still have to use a method in SwiftData that creates more tracking data itself?
4
0
1.2k
Feb ’25
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
909
Oct ’24
Swiftdata cloudkit synchronization issues
Hi, I did cloudkit synchronization using swiftdata. However, synchronization does not occur automatically, and synchronization occurs intermittently only when the device is closed and opened. For confirmation, after changing the data in Device 1 (saving), when the data is fetched from Device 2, there is no change. I've heard that there's still an issue with swiftdata sync and Apple is currently troubleshooting it, is the phenomenon I'm experiencing in the current version normal?
1
0
464
Oct ’24
Swift 6 Concurrency errors with ModelActor, or Core Data actors
In my app, I've been using ModelActors in SwiftData, and using actors with a custom executor in Core Data to create concurrency safe services. I have multiple actor services that relate to different data model components or features, each that have their own internally managed state (DocumentService, ImportService, etc). The problem I've ran into, is that I need to be able to use multiple of these services within another service, and those services need to share the same context. Swift 6 doesn't allow passing contexts across actors. The specific problem I have is that I need a master service that makes multiple unrelated changes without saving them to the main context until approved by the user. I've tried to find a solution in SwiftData and Core Data, but both have the same problem which is contexts are not sendable. Read the comments in the code to see the issue: /// This actor does multiple things without saving, until committed in SwiftData. @ModelActor actor DatabaseHelper { func commitChange() throws { try modelContext.save() } func makeChanges() async throws { // Do unrelated expensive tasks on the child context... // Next, use our item service let service = ItemService(modelContainer: SwiftDataStack.shared.container) let id = try await service.expensiveBackgroundTask(saveChanges: false) // Now that we've used the service, we need to access something the service created. // However, because the service created its own context and it was never saved, we can't access it. let itemFromService = context.fetch(id) // fails // We need to be able to access changes made from the service within this service, /// so instead I tried to create the service by passing the current service context, however that results in: // ERROR: Sending 'self.modelContext' risks causing data races let serviceFromContext = ItemService(context: modelContext) // Swift Data doesn't let you create child contexts, so the same context must be used in order to change data without saving. } } @ModelActor actor ItemService { init(context: ModelContext) { modelContainer = SwiftDataStack.shared.container modelExecutor = DefaultSerialModelExecutor(modelContext: context) } func expensiveBackgroundTask(saveChanges: Bool = true) async throws -> PersistentIdentifier? { // Do something expensive... return nil } } Core Data has the same problem: /// This actor does multiple things without saving, until committed in Core Data. actor CoreDataHelper { let parentContext: NSManagedObjectContext let context: NSManagedObjectContext /// In Core Data, I can create a child context from a background context. /// This lets you modify the context and save it without updating the main context. init(progress: Progress = Progress()) { parentContext = CoreDataStack.shared.newBackgroundContext() let childContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) childContext.parent = parentContext self.context = childContext } /// To commit changes, save the parent context pushing them to the main context. func commitChange() async throws { // ERROR: Sending 'self.parentContext' risks causing data races try await parentContext.perform { try self.parentContext.save() } } func makeChanges() async throws { // Do unrelated expensive tasks on the child context... // As with the Swift Data example, I am unable to create a service that uses the current actors context from here. // ERROR: Sending 'self.context' risks causing data races let service = ItemService(context: self.context) } } Am I going about this wrong, or is there a solution to fix these errors? Some services are very large and have their own internal state. So it would be very difficult to merge all of them into a single service. I also am using Core Data, and SwiftData extensively so I need a solution for both. I seem to have trapped myself into a corner trying to make everything concurrency save, so any help would be appreciated!
6
0
765
Mar ’25