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

Core Data slow to save
iOS 18.2, Swift, Xcode 16.2 I have a Core Data model with two entities - WarehouseArea (of which there is only one object) and StockReeipt (of which there are a couple of hundred thousand). Each StockReceipt must be linked to a WarehouseArea, and a WarehouseArea can be linked to zero, one or many StockReceipts. My problem is that when I create and add one more StockReceipt, the Core Data save takes over 3 seconds to complete. I don't understand why this is so slow. Saving the initial 200,000 StockReceipts only takes 5-6 seconds. When I enable SQL logging I can see that when the WarehouseArea attribute is being set on a StockReceipt, Core Data fetches all of the other StockReceipts (I don't know why) but that only takes 0.2 seconds and none of those StockReceipts are modified, so there shouldn't be any need to process them when saving the context. I have prepared a test project which can be found at https://github.com/DaleReilly/CoreDataSaveTester . Running the project will produce NSLog output showing the times before and after the slow save. Please help me understand what is going on in the background and tell me if there is any way I can speed this up?
3
0
417
Jan ’25
How to Drop Entity in SwiftData and CloudKit?
I'm using SwiftData with CloudKit and have been trying to migrate from SchemaV1 to SchemaV2, but it seems reducing the Entities crashes my app. // Example of migrating from V1 to V2 // Dropping `Person` because it's no longer needed do { // SchemaV1: Person.self, Author.self // SchemaV2: Author.self let schema = Schema(versionedSchema: SchemaV2.self) return try ModelContainer( for: schema, migrationPlan: AppSchemaMigrationPlan.self, configurations: ModelConfiguration( cloudKitDatabase: .automatic) ) } catch { fatalError("Could not create ModelContainer: \(error)") } Is it possible to drop Entities in the Schema Migration Plan? How can I delete the Person model from my Schema and CloudKit?
3
0
448
Jan ’25
SwiftData Relationship Delete Not Working (SwiftData/PersistentModel.swift:359)
SwiftData delete isn't working, when I attempt to delete a model, my app crashes and I get the following error: SwiftData/PersistentModel.swift:359: Fatal error: Cannot remove My_App.Model2 from relationship Relationship - name: model2, options: [], valueType: Model2, destination: Model2, inverseName: models3, inverseKeypath: Optional(\Model2.models3) on My_App.Model3 because an appropriate default value is not configured. I get that it's saying I don't have a default value, but why do I need one? Isn't @Relationship .cascade automatically deleting the associated models? And onto of that, why is the error occurring within the do block, shouldn't it be caught by the catch, and printed? I have put together a sample project below. import SwiftUI import SwiftData @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .modelContainer(for: Model3.self) } } } @Model class Model1 { var name: String @Relationship(deleteRule: .cascade, inverse: \Model2.model1) var models2: [Model2] = [] init(name: String) { self.name = name } } @Model class Model2 { var name: String var model1: Model1 @Relationship(deleteRule: .cascade, inverse: \Model3.model2) var models3: [Model3] = [] init(name: String, model1: Model1) { self.name = name self.model1 = model1 } } @Model class Model3 { var name: String var model2: Model2 init(name: String, model2: Model2) { self.name = name self.model2 = model2 } } struct ContentView: View { @Query var models1: [Model1] @Environment(\.modelContext) var modelContext var body: some View { NavigationStack { List(models1) { model1 in Text(model1.name) .swipeActions { Button("Delete", systemImage: "trash", role: .destructive) { modelContext.delete(model1) do { try modelContext.save() //SwiftData/PersistentModel.swift:359: Fatal error: Cannot remove My_App.Model2 from relationship Relationship - name: model2, options: [], valueType: Model2, destination: Model2, inverseName: models3, inverseKeypath: Optional(\Model2.models3) on My_App.Model3 because an appropriate default value is not configured. } catch { print(error.localizedDescription) } } } } .toolbar { Button("Insert", systemImage: "plus") { modelContext.insert(Model3(name: "model3", model2: Model2(name: "model2", model1: Model1(name: "model1")))) } } } } }
1
0
468
Jan ’25
CKSyncEngine keeps attempting to sync the same record
I am attempting to migrate a cloudkit module that calls on manual cloudkit methods for fetching record zone changes, modifying records, etc to one that utilizes CKSyncEngine. I've got a basic implementation working with just a create method for one of my data models, however it seems like the sync engine keeps calling sync events on the same pending changes. Here is my current flow: The user will hit some button that lets them fill out a form to create a data model. The user saves the form. This triggers a method that takes the resulting data model and queues it to the sync engine's state (engine.state.add(pendingRecordZoneChanges: pendingChanges) I have my delegate method nextRecordZoneChangeBatch(_ context:...) implemented where it fetches the corresponding data model using the record ID and returns a batch containing the corresponding populated record from the data model. I have the handleEvent(_ event:...) delegate method implemented where I handle both .fetchRecordZoneChanges and .sentRecordZoneChanges. I have set up .sentRecordZoneChanges to merge the server record into my local record (and persisted locally) so that the record change tags are the same. After this last portion, it seems that the sync engine continues to keep pushing syncs/updates and I end up with numerous handleEvent(_ event:) calls that keep returning savedRecords (and occasionally failedRecordSaves). Am I missing some step to remove the record from the changes after the sync engine recognizes that I have properly saved the record to the server?
2
0
261
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.
2
1
286
Jan ’25
Not able to save with SwiftData. "The file “default.store” couldn’t be opened."
I get this message when trying to save my Models. CoreData: error: SQLCore dispatchRequest: exception handling request: <NSSQLSaveChangesRequestContext: 0x303034540> , I/O error for database at /var/mobile/Containers/Data/Application/726ECA8C-6C67-4BFE-89E7-AFD8A83CAA5D/Library/Application Support/default.store. SQLite error code:1, 'no such table: ZCALENDARMODEL' with userInfo of { NSFilePath = "/var/mobile/Containers/Data/Application/726ECA8C-6C67-4BFE-89E7-AFD8A83CAA5D/Library/Application Support/default.store"; NSSQLiteErrorDomain = 1; } SwiftData.DefaultStore save failed with error: Error Domain=NSCocoaErrorDomain Code=256 "The file “default.store” couldn’t be opened." UserInfo={NSFilePath=/var/mobile/Containers/Data/Application/726ECA8C-6C67-4BFE-89E7-AFD8A83CAA5D/Library/Application Support/default.store, NSSQLiteErrorDomain=1} The App has Recipes and Calendars and the user can select a Recipe for each Calendar day. The recipe should not be referenced, it should be saved by SwiftData along with the Calendar. import SwiftUI import SwiftData enum CalendarSource: String, Codable { case created case imported } @Model class CalendarModel: Identifiable, Codable { var id: UUID = UUID() var name: String var startDate: Date var endDate: Date var recipes: [String: RecipeData] = [:] var thumbnailData: Data? var source: CalendarSource? // Computed Properties var daysBetween: Int { let days = Calendar.current.dateComponents([.day], from: startDate.midnight, to: endDate.midnight).day ?? 0 return days + 1 } var allDates: [Date] { startDate.midnight.allDates(upTo: endDate.midnight) } var thumbnailImage: Image? { if let data = thumbnailData, let uiImage = UIImage(data: data) { return Image(uiImage: uiImage) } else { return nil } } // Initializer init(name: String, startDate: Date, endDate: Date, thumbnailData: Data? = nil, source: CalendarSource? = .created) { self.name = name self.startDate = startDate self.endDate = endDate self.thumbnailData = thumbnailData self.source = source } // Convenience initializer to create a copy of an existing calendar static func copy(from calendar: CalendarModel) -> CalendarModel { let copiedCalendar = CalendarModel( name: calendar.name, startDate: calendar.startDate, endDate: calendar.endDate, thumbnailData: calendar.thumbnailData, source: calendar.source ) // Copy recipes copiedCalendar.recipes = calendar.recipes.mapValues { $0 } return copiedCalendar } // Codable Conformance private enum CodingKeys: String, CodingKey { case id, name, startDate, endDate, recipes, thumbnailData, source } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) name = try container.decode(String.self, forKey: .name) startDate = try container.decode(Date.self, forKey: .startDate) endDate = try container.decode(Date.self, forKey: .endDate) recipes = try container.decode([String: RecipeData].self, forKey: .recipes) thumbnailData = try container.decodeIfPresent(Data.self, forKey: .thumbnailData) source = try container.decodeIfPresent(CalendarSource.self, forKey: .source) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) try container.encode(startDate, forKey: .startDate) try container.encode(endDate, forKey: .endDate) try container.encode(recipes, forKey: .recipes) try container.encode(thumbnailData, forKey: .thumbnailData) try container.encode(source, forKey: .source) } } import SwiftUI struct RecipeData: Codable, Identifiable { var id: UUID = UUID() var name: String var ingredients: String var steps: String var thumbnailData: Data? // Computed property to convert thumbnail data to a SwiftUI Image var thumbnailImage: Image? { if let data = thumbnailData, let uiImage = UIImage(data: data) { return Image(uiImage: uiImage) } else { return nil // No image } } init(recipe: RecipeModel) { self.name = recipe.name self.ingredients = recipe.ingredients self.steps = recipe.steps self.thumbnailData = recipe.thumbnailData } } import SwiftUI import SwiftData @Model class RecipeModel: Identifiable, Codable { var id: UUID = UUID() var name: String var ingredients: String var steps: String var thumbnailData: Data? // Store the image data for the thumbnail static let fallbackSymbols = ["book.pages.fill", "carrot.fill", "fork.knife", "stove.fill"] // Computed property to convert thumbnail data to a SwiftUI Image var thumbnailImage: Image? { if let data = thumbnailData, let uiImage = UIImage(data: data) { return Image(uiImage: uiImage) } else { return nil // No image } } // MARK: - Initializer init(name: String, ingredients: String = "", steps: String = "", thumbnailData: Data? = nil) { self.name = name self.ingredients = ingredients self.steps = steps self.thumbnailData = thumbnailData } // MARK: - Copy Function func copy() -> RecipeModel { RecipeModel( name: self.name, ingredients: self.ingredients, steps: self.steps, thumbnailData: self.thumbnailData ) } // MARK: - Codable Conformance private enum CodingKeys: String, CodingKey { case id, name, ingredients, steps, thumbnailData } required init(from decoder: Decoder) throws { ... } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(name, forKey: .name) try container.encode(ingredients, forKey: .ingredients) try container.encode(steps, forKey: .steps) try container.encode(thumbnailData, forKey: .thumbnailData) } }
1
0
439
Jan ’25
How to Share a CloudKit Record with Multiple Participants While Keeping Individual Records Private?
In a CloudKit private database, the Owner creates a custom zone and performs the following actions: Creates CKRecord1 with CKShare1 and invites Participant1 to it. Creates CKRecord2 with CKShare2 and invites Participant2 to it. Creates CKRecordShared, which should be accessible to both Participant1 and Participant2. How can I achieve step 3? I observed that: Setting a regular reference from CKRecord1 (or CKRecord2) to CKRecordShared does not automatically make CKRecordShared accessible to Participant1 (or Participant2). CKRecordShared can only have one parent, so it cannot be directly linked via parent reference to both Participant1 and Participant2 at the same time. One potential solution I see is to have the Owner create a separate CKShare for CKRecordShared and share it explicitly with each participant. However, this approach could lead to user errors, as it requires careful management of multiple shares for each participant. Is there a better way to handle this scenario, ensuring that CKRecordShared is accessible to multiple participants without introducing unnecessary complexity or potential errors?
2
0
446
Jan ’25
Accessing "iCloud Drive" folder in Files on iPhone from App written in Swift
I am trying to read and write a text file from an App written in Swift in XCode directly to the "iCloud Drive" folder in Files on the iPhone. The app worked readlly reading and writing to the Documents folder in the App container, and then readily to the "On My iPhone" folder in Files after adding 2 lines to the plist that I found in a search online. But I have been unable to get to the iCloud Drive folder. I found an item called "Enabling Document Storage in iCloud Drive" in "iCloud Design Guide" with additional plist entries that states "These settings allow iCloud Drive to provide public access to the files stored in your app’s container": NSUbiquitousContainers iCloud.com.example.MyApp NSUbiquitousContainerIsDocumentScopePublic NSUbiquitousContainerSupportedFolderLevels Any NSUbiquitousContainerName MyApp I think I changed the MyApp items appropriately. I have enabled iCloud in my App and the XCode General, and Signing entries. But this does not work. There are no error messages and no "Steps" shown in the "Capabilities" entry in Xcode. A little help? :-)
3
0
537
Jan ’25
SwiftData relationshipKeyPathsForPrefetching not working
relationshipKeyPathsForPrefetching in SwiftData does not seem to work here when scrolling down the list. Why? I would like all categories to be fetched while posts are fetched - not while scrolling down the list. struct ContentView: View { var body: some View { QueryList( fetchDescriptor: withCategoriesFetchDescriptor ) } var withCategoriesFetchDescriptor: FetchDescriptor<Post> { var fetchDescriptor = FetchDescriptor<Post>() fetchDescriptor.relationshipKeyPathsForPrefetching = [\.category] return fetchDescriptor } } struct QueryList: View { @Query var posts: [Post] init(fetchDescriptor: FetchDescriptor<Post>) { _posts = Query(fetchDescriptor) } var body: some View { List(posts) { post in VStack { Text(post.title) Text(post.category?.name ?? "") .font(.footnote) } } } } @Model final class Post { var title: String var category: Category? init(title: String) { self.title = title } } @Model final class Category { var name: String init(name: String) { self.name = name } }
2
0
402
Jan ’25
Why did NSPersistentCloudKitContainer added CKAsset fields for each String in Core Data
Hi, I've been using Core Data + CloudKit via NSPersistentCloudKitContainer for several years now. Back then I just created my Core Data AND CloudKit fields by hand. Now the time has come for a little lightweight migration to a new Core Data model, let's say I just needed to add one String attribute. So I've done the Core Data local migration as usual, then added this to container code: try? persistentContainer.initializeCloudKitSchema(options: NSPersistentCloudKitContainerSchemaInitializationOptions()) Run. And everything worked great. but… Now I've noticed that CloudKit created new CKAsset fields for each String attribute that I had in Core Data (about 5 new CKAsset fields). Is this normal!? Why? ! Is it safe to deploy these changes to prod? ty. ChatGPT said: "This field is used internally by CloudKit to handle large string values. If the string value is small enough, it is stored in the normal String field, but if it exceeds the size limit (about 1KB), the string is automatically stored as a CKAsset."
1
1
445
Jan ’25
How can I test CloudKit User Keychain Reset?
My question Is there a way to perform an iCloud keychain reset in order to be able to test CKErrorUserDidResetEncryptedDataKey ? I found this section in the CloudKit documentation https://developer.apple.com/documentation/cloudkit/encrypting-user-data#Handle-a-User-Keychain-Reset I want to be prepared for the zoneNotFound / CKErrorUserDidResetEncryptedDataKey case. However, I can't find a way to actually reproduce this error with an iCloud (test-) user and can't find any Apple documentation on how to perform sucha "User Keychain Reset". The only thing that almost looked like it I came across was in the Keychain.app's Settings "Reset Default Keychains…". However, performing this didn't seem to affect the CloudKit data used in our App at all. I've been trying to do this with an Apple account that has 2FA active and a recovery account assigned. We're only targetting >= iOS 18, macOS >= 15.
9
0
590
Jan ’25
Can't batch delete with one-to-many to self relationship
I have a simple model that contains a one-to-many relationship to itself to represent a simple tree structure. It is set to cascade deletes so deleting the parent node deletes the children. Unfortunately I get an error when I try to batch delete. A test demonstrates: @Model final class TreeNode { var parent: TreeNode? @Relationship(deleteRule: .cascade, inverse: \TreeNode.parent) var children: [TreeNode] = [] init(parent: TreeNode? = nil) { self.parent = parent } } func testBatchDelete() throws { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: TreeNode.self, configurations: config) let context = ModelContext(container) context.autosaveEnabled = false let root = TreeNode() context.insert(root) for _ in 0..<10 { let child = TreeNode(parent: root) context.insert(child) } try context.save() // fails if first item doesn't have a nil parent, succeeds otherwise // which row is first is random, so will succeed sometimes try context.delete(model: TreeNode.self) } The error raised is: CoreData: error: Unhandled opt lock error from executeBatchDeleteRequest Constraint trigger violation: Batch delete failed due to mandatory OTO nullify inverse on TreeNode/parent and userInfo { NSExceptionOmitCallstacks = 1; NSLocalizedFailureReason = "Constraint trigger violation: Batch delete failed due to mandatory OTO nullify inverse on TreeNode/parent"; "_NSCoreDataOptimisticLockingFailureConflictsKey" = ( ); } Interestingly, if the first record when doing an unsorted query happens to be the parent node, it works correctly, so the above unit test will actually work sometimes. Now, this can be "solved" by changing the reverse relationship to an optional like so: @Relationship(deleteRule: .cascade, inverse: \TreeNode.parent) var children: [TreeNode]? The above delete will work fine. However, this causes issues with predicates that test counts in children, like for instance deleting only nodes where children is empty for example: try context.delete(model: TreeNode.self, where: #Predicate { $0.children?.isEmpty ?? true }) It ends up crashing and dumps a stacktrace to the console with: An uncaught exception was raised Keypath containing KVC aggregate where there shouldn't be one; failed to handle children.@count (the stacktrace is quite long and deep in CoreData's NSSQLGenerator) Does anyone know how to work around this?
5
0
448
Jan ’25
Cannot add participants to CoreData to share between multiple users
I have been trying to get this to work since it was announced a few years ago but with no joy. I'm struggling to get Apple's example code to behave itself too. Seems overly complex and buggy. So I set out to create a simplified version myself. I have got the database to sync with CloudKit and I can see my records in the developer dashboard. I'm trying to use container.record(for: object.objectID) to get the CKRecord for it, but this always fails. The next step would be to add the participant. I try to add the participant based on this code: Button { let record = fetchRecord(for: items[0]) //hack just to use the first record for dev testing let share = CKShare(rootRecord: record) let persistenceController = PersistenceController.shared persistenceController.addParticipant( emailAddress: "andrew@ambrit.com", permission: .readWrite, share: share) { share, error in if let error = error { print("Error: \(error.localizedDescription)") } else if let share = share { print("Share updated successfully: \(share)") } } } label: { Label("Participants", systemImage: "person") } and extension PersistenceController { func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare, completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) { let container = PersistenceController.shared.container let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress) let persistentStore = privatePersistentStore //share.persistentStore! container.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in guard let participants = results, let participant = participants.first, error == nil else { completionHandler?(share, error) return } participant.permission = permission participant.role = .privateUser share.addParticipant(participant) container.persistUpdatedShare(share, in: persistentStore) { (share, error) in if let error = error { print("\(#function): Failed to persist updated share: \(error)") } completionHandler?(share, error) } } } } My immediate problem is that when I call fetchRecord it doesn't find anything despite the record being available in the CloudKit dashboard. func fetchRecord(for object: NSManagedObject) -> CKRecord { let container = PersistenceController.shared.container print ("Fetching record \(object.objectID)") if let record = container.record(for: object.objectID) { print("CKRecord ID: \(record.recordID)") print("Record Name: \(record.recordID.recordName)") return record } else { fatalError("Record not found") } }
1
0
438
Jan ’25
CloudKit: Application has malformed entitlements
Hey, For some reason I see crashes for my iOS app related to CloudKit entitlements. The crash happens on start up and it says: "CKException - Application has malformed entitlements. Found value "*" for entitlement com.apple.developer.icloud-services, expected an array of strings" I have checked my entitlements of the same build on App Store Connect and it shows "com.apple.developer.icloud-services: ( "CloudKit" )" So I am not sure why users are having this issue. I haven't been able to reproduce it. Does anyone have any idea why this is happening? Thanks
5
0
542
Jan ’25
Collaboration of iCloud Drive document with CloudKit-based live sync
In Apple Numbers and similar apps, a user can save a document to iCloud Drive, and collaborate with other users. From what I can gather, it seems to use two mechanisms: the document as a whole is synced via iCloud Drive, but when a collaboration is started, it seems to use CloudKit records to do live updates. I am working on a similar app, that saves documents to iCloud Drive (on Mac, iPad, and iPhone). Currently it only syncs via iCloud Drive, re-reading the entire (often large) document when a remote change occurs. This can lead to a delay of several seconds (up to a minute) for the document to be saved, synced to the server, synced from the server, and re-read. I'm working on adding a "live sync", i.e. the ability to see changes in as near to real-time as feasible, like in Apple's apps. The document as a whole will remain syncing via iCloud Drive. My thought is to add a CloudKit CKRecord-based sync when two or more users are collaborating on a document, recording only the diffs for quick updates. The app would no longer re-read the entire document when iCloud Drive updates it while in use, and would instead read the CloudKit records and apply those changes. This should be much faster. Is my understanding of how Apple does it correct? Does my proposed approach seem sensible? Has anyone else implemented something like this, with iCloud Drive-based documents and a CloudKit live sync? In terms of technologies, I see that Apple now has a Shared with You framework, with the ability to use a NSItemProvider to start the collaboration. Which raises the question, should I use the iCloud Drive document for the collaboration (as I do now), or the CloudKit CKShare diff? I think I'd have to use the document as a whole, both so it works with the Send Copy option, and so a user that doesn't have the document gets it when using Collaborate. Once the collaboration is underway, I'd want to start the CloudKit channel. So I guess I'd save the CKShare to the server, get its URL, and save that in the document, so another user can read that URL as part of their initial load of the document from iCloud Drive? Once two (or more) users have the document via iCloud Drive, and the CKShare via the embedded URL, I should be able to do further live-sync updates via CloudKit. If a user closes the document and re-opens it, they'd get the updates via iCloud Drive, so no need to apply any updates from before the document was opened. Does all this sound reasonable, or am I overlooking some gotcha? I'd appreciate any advice from people who have experience with this kind of syncing.
1
0
325
Jan ’25
Fatal error: Duplicate keys of type 'AnyHashable' were found in a Dictionary
I get the following fatal error when the user clicks Save in AddProductionView. Fatal error: Duplicate keys of type 'AnyHashable' 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. As far as I’m aware, SwiftData automatically makes its models conform to Hashable, so this shouldn’t be a problem. I think it has something to do with the picker, but for the life of me I can’t see what. This error occurs about 75% of the time when Save is clicked. I'm using Xcode 16.2 and iPhone SE 2nd Gen. Any help would be greatly appreciated… Here is my code: import SwiftUI import SwiftData @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .modelContainer(for: Character.self, isAutosaveEnabled: false) } } } @Model final class Character { var name: String var production: Production var myCharacter: Bool init(name: String, production: Production, myCharacter: Bool = false) { self.name = name self.production = production self.myCharacter = myCharacter } } @Model final class Production { var name: String init(name: String) { self.name = name } } struct ContentView: View { @State private var showingSheet = false var body: some View { Button("Add", systemImage: "plus") { showingSheet.toggle() } .sheet(isPresented: $showingSheet) { AddProductionView() } } } struct AddProductionView: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) var modelContext @State var production = Production(name: "") @Query var characters: [Character] @State private var characterName: String = "" @State private var selectedCharacter: Character? var filteredCharacters: [Character] { characters.filter { $0.production == production } } var body: some View { NavigationStack { Form { Section("Details") { TextField("Title", text: $production.name) } Section("Characters") { List(filteredCharacters) { character in Text(character.name) } HStack { TextField("Character", text: $characterName) Button("Add") { let newCharacter = Character(name: characterName, production: production) modelContext.insert(newCharacter) characterName = "" } .disabled(characterName.isEmpty) } if !filteredCharacters.isEmpty { Picker("Select your role", selection: $selectedCharacter) { Text("Select") .tag(nil as Character?) ForEach(filteredCharacters) { character in Text(character.name) .tag(character as Character?) } } .pickerStyle(.menu) } } } .toolbar { Button("Save") { //Fatal error: Duplicate keys of type 'AnyHashable' 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. if let selectedCharacter = selectedCharacter { selectedCharacter.myCharacter = true } modelContext.insert(production) do { try modelContext.save() } catch { print("Failed to save context: \(error)") } dismiss() } .disabled(production.name.isEmpty || selectedCharacter == nil) } } } }
2
0
486
Jan ’25
Recovering Customer's Data After iCloud Migration
I have encountered an issue with a customer’s data access after they migrated to a different iCloud account, and I’m looking for guidance. The Situation: The customer was logged into their account on my app, which was associated with a specific iCloud account (iCloud A). They had all their app data available while using iCloud A. The customer then switched to a new iCloud account (iCloud B) on the same device, while still using the same app account. After switching iCloud accounts, their data is no longer visible in the app or my CloudKit dashboard. My Investigation: I accessed the customer’s CloudKit data via the CloudKit Console, acting as their iCloud account. I couldn’t find the private database zone or any of their records when accessing iCloud A through the console. I don’t believe the data was deleted since actions performed under iCloud B shouldn’t affect data stored in iCloud A. My Hypothesis: I suspect that the customer’s old iCloud account (iCloud A) may have downgraded or stopped paying for iCloud storage. If the iCloud subscription is inactive or expired, could that prevent me from accessing their CloudKit data? Would renewing the iCloud subscription for iCloud A restore access to the missing data? Questions: Does an unpaid or expired iCloud account restrict access to CloudKit records, even if they weren’t deleted? Would paying for iCloud storage again restore the data previously stored in CloudKit? Is there any way to recover the customer’s CloudKit data if they are unable to access their old iCloud account? If anyone has a simpler approach to recovering the customer’s iCloud-stored app data or has experience dealing with iCloud migrations like this, I’d appreciate your insights. Thank you in advance for any advice!
2
0
434
Jan ’25
swiftdata model polymorphism?
I have a SwiftData model where I need to customize behavior based on the value of a property (connectorType). Here’s a simplified version of my model: @Model public final class ConnectorModel { public var connectorType: String ... func doSomethingDifferentForEveryConnectorType() { ... } } I’d like to implement doSomethingDifferentForEveryConnectorType in a way that allows the behavior to vary depending on connectorType, and I want to follow best practices for scalability and maintainability. I’ve come up with three potential solutions, each with pros and cons, and I’d love to hear your thoughts on which one makes the most sense or if there’s a better approach: **Option 1: Use switch Statements ** func doSomethingDifferentForEveryConnectorType() { switch connectorType { case "HTTP": // HTTP-specific logic case "WebSocket": // WebSocket-specific logic default: // Fallback logic } } Pros: Simple to implement and keeps the SwiftData model observable by SwiftUI without any additional wrapping. Cons: If more behaviors or methods are added, the code could become messy and harder to maintain. **Option 2: Use a Wrapper with Inheritance around swiftdata model ** @Observable class ParentConnector { var connectorModel: ConnectorModel init(connectorModel: ConnectorModel) { self.connectorModel = connectorModel } func doSomethingDifferentForEveryConnectorType() { fatalError("Not implemented") } } @Observable class HTTPConnector: ParentConnector { override func doSomethingDifferentForEveryConnectorType() { // HTTP-specific logic } } Pros: Logic for each connector type is cleanly organized in subclasses, making it easy to extend and maintain. Cons: Requires introducing additional observable classes, which could add unnecessary complexity. **Option 3: Use a @Transient class that customizes behavior ** protocol ConnectorProtocol { func doSomethingDifferentForEveryConnectorType(connectorModel: ConnectorModel) } class HTTPConnectorImplementation: ConnectorProtocol { func doSomethingDifferentForEveryConnectorType(connectorModel: ConnectorModel) { // HTTP-specific logic } } Then add this to the model: @Model public final class ConnectorModel { public var connectorType: String @Transient public var connectorImplementation: ConnectorProtocol? // Or alternatively from swiftui I could call myModel.connectorImplementation.doSomethingDifferentForEveryConnectorType() to avoid this wrapper func doSomethingDifferentForEveryConnectorType() { connectorImplementation?.doSomethingDifferentForEveryConnectorType(connectorModel: self) } } Pros: Decouples model logic from connector-specific behavior. Avoids creating additional observable classes and allows for easy extension. Cons: Requires explicitly passing the model to the protocol implementation, and setup for determining the correct implementation needs to be handled elsewhere. My Questions Which approach aligns best with SwiftData and SwiftUI best practices, especially for scalable and maintainable apps? Are there better alternatives that I haven’t considered? If Option 3 (protocol with dependency injection) is preferred, what’s the best way to a)manage the transient property 2) set the correct implementation and 3) pass reference to swiftdata model? Thanks in advance for your advice!
0
0
245
Jan ’25
Can't access child entries of SwiftData class
When I tried to use a working project with iOS 18 installed on my device, it wouldn't work anymore and crash right away. Before with iOS 17 it was working fine. I can't access child variables that are saved in an Array in a parent object in SwiftData. The error is always somewhere in these hidden lines: { @storageRestrictions(accesses: _$backingData, initializes: _title) init(initialValue) { _$backingData.setValue(forKey: \.title, to: initialValue) _title = _SwiftDataNoType() } get { _$observationRegistrar.access(self, keyPath: \.title) return self.getValue(forKey: \.title) } set { _$observationRegistrar.withMutation(of: self, keyPath: \.title) { self.setValue(forKey: \.title, to: newValue) } } } The child classes are also inserted and saved into the modelContext when created and set to the parent instance, but I also can't fetch them via modelContext.fetch() - Error here is: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x243a62a4c) Maybe there is a problem with the relationship between two saved instances. The parent instances are saved correctly and it was working in iOS 17. The problem is similar to these two cases: https://forums.developer.apple.com/forums/thread/762679 https://forums.developer.apple.com/forums/thread/738983 I changed the logic after I reviewed these threads, as I am now linking the parent and child instances, that got rid of one warning in the console. button.canvas = canvas modelContext.insert(button) canvas.buttons = [button] But in the end those threads were not enough for me to find a fix for my problem. A small project can be found here: https://github.com/DonMalte/SwiftDataTest
3
0
541
Jan ’25