I'm using SwiftData to persist my items in storage. I used .modelContext to pass in my shared context, and on iOS 18 (both on a physical device and a simulator), I discovered a bug where SwiftData doesn't automatically save my data. For example, I could add a new item, go to the next screen, change something that reloads a previous screen, and SwiftData just forgets the item that I added. Please find the fully working code attached.
While writing this post, I realized that if I use .modelContainer instead of .modelContext, the issue is solved. So I have two questions:
It seems like .modelContainer is the go-to option when working with SwiftData, but why did an issue occur when I used .modelContext and passed in a shared container? When should we use .modelContext over .modelContainer?
What was the bug? It's working fine in iOS 17, but not in iOS 18. Or is this expected?
Here's the fully working code so you can copy and paste:
import SwiftUI
import SwiftData
typealias NamedColor = (color: Color, name: String)
extension Color {
init(r: Double, g: Double, b: Double) {
self.init(red: r/255, green: g/255, blue: b/255)
}
static let namedColors: [NamedColor] = [
(.blue, "Blue"),
(.red, "Red"),
(.green, "Green"),
(.orange, "Orange"),
(.yellow, "Yellow"),
(.pink, "Pink"),
(.purple, "Purple"),
(.teal, "Teal"),
(.indigo, "Indigo"),
(.brown, "Brown"),
(.cyan, "Cyan"),
(.gray, "Gray")
]
static func name(for color: Color) -> String {
return namedColors.first(where: { $0.color == color })?.name ?? "Blue"
}
static func color(for name: String) -> Color {
return namedColors.first(where: { $0.name == name })?.color ?? .blue
}
}
@main
struct SwiftDataTestApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
@AppStorage("accentColor") private var accentColorName: String = "Blue"
var body: some Scene {
WindowGroup {
NavigationStack {
HomeView()
}
.tint(Color.color(for: accentColorName))
}
.modelContainer(sharedModelContainer) // This works
// .modelContext(ModelContext(sharedModelContainer)) // This doesn't work
}
}
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
struct HomeView: View {
@State private var showSettings = false
@Environment(\.modelContext) var modelContext
@AppStorage("accentColor") private var accentColorName: String = "Blue"
@Query private var items: [Item]
var body: some View {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
Button {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
} label: {
Image(systemName: "plus")
.frame(maxWidth: .infinity)
.frame(maxHeight: .infinity)
}
}
.navigationTitle("Habits")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showSettings = true }) {
Label("", systemImage: "gearshape.fill")
}
}
}
.navigationDestination(isPresented: $showSettings) {
colorPickerView
}
}
private var colorPickerView: some View {
Form {
Section(header: Text("Accent Color")) {
Picker("Accent Color", selection: $accentColorName) {
ForEach(Color.namedColors, id: \.name) { namedColor in
Text(namedColor.name)
.tag(namedColor.name)
.foregroundColor(namedColor.color)
}
}
.pickerStyle(.wheel)
}
}
.navigationTitle("Settings")
}
}
iCloud & Data
RSS for tagLearn how to integrate your app with iCloud and data frameworks for effective data storage
Post
Replies
Boosts
Views
Activity
I have developed the following two App: app1 and app2. Both App have the function of using iCloud. The iCloud container id of app1 and app2 is different. I use the CloudKit storage function of iCloud.
In the storage management of "Settings", iCloud, the display name of app1 is app2's name, not app1's name. This causes many of our users to delete the iCloud data of the application by mistake, resulting in losses.
Now my question is: What caused the name in app1's iCloud storage management list to be displayed as the name of app2? How should it be solved?
Ever since I got a new MacBook (in 2023) and synced with my other Mac and the iCloud, all of the assets from projects created on my old Mac are missing when I open in Xcode on either computer. What's worse is this has also somehow affected my GitHub repos for these projects too. The assets are missing from my repos when cloned onto new machines. It hasn't affected assets in App Store deployment but the project which holds my deployed app was missing its assets too. I am able to locate the missing assets by digging through my Time Machine backups to find what I need and moving the folders/assets into Xcode project.
Is there anyway to restore my assets in a cleaner more complete way (short of full Time Machine restore)?
Why does this happen?
How do I avoid this in the future?
I’m writing test apps using SwiftData. Running migrations locally works fine. But I couldn’t find anything on how to handle a situation, where my local app is running on e.g. ‘MigrationSchemaV2’ and an iOS app is still running on ‘MigrationSchemaV1’ hence needs to be updated before the data migration takes place.
I expect the local migration to be synced to iCloud automatically therefore the iOS app would crash.
I’m looking for documentation, tutorials, best practice or lessons learned in order to understand the basic implementation idea, its dependencies and implications.
modelContext.fetchIdentifiers(descriptor) errors when using a SortDescriptor to sort by a variable and returns no models. The fetch works fine without a SortDescriptor, thus
FetchDescriptor<MyModel>()
works fine, but
FetchDescriptor<MyModel>(sortBy: [.init(\.title)])
or
FetchDescriptor<MyModel>(sortBy: [SortDescriptor(\.title)])
errors with console message
The operation couldn’t be completed. (SwiftData.SwiftDataError error 1.)
I am using Xcode Version 16.0 beta 6 (16A5230g).
I am receiving:
<CKError 0x30201abb0: "Service Unavailable" (6/2009); "Request failed with http status code 503"; uuid = B6454A02-15FF-4FC1-B124-E5478A9C8BA7; Retry after 28.0 seconds>
This seems to be an issue with all users. What is happening? Seems like CloudKit is down.
How can one observe changes in the SwiftData DB?
I'm aware that this is possible via Queries, but I need to fetch the data in background, so a query is not an option.
I'm aware of the ModelContext.didSave / .willSave notifications, but these don't work with iOS 17.
-> How can I observe changes to models of a certain type? I don't want to observe the whole database.
I’m working on a project where I’m using CKSyncEngine to sync different types of SwiftData models, specifically User and Organization, to CloudKit. Here’s how I schedule these models to be synced:
For the User model:
let pendingSaves: [CKSyncEngine.PendingRecordZoneChange] = [.saveRecord(user.recordID)]
syncEngine.state.add(pendingRecordZoneChanges: pendingSaves)
For the Organization model:
let pendingSaves: [CKSyncEngine.PendingRecordZoneChange] = [.saveRecord(organization.recordID)]
syncEngine.state.add(pendingRecordZoneChanges: pendingSaves)
The problem arises in my CKSyncEngineDelegate's nextRecordZoneChangeBatch method where from CKRecord.ID alone I need to create the actual CKRecord that will be synced to CloudKit. This recordID alone doesn’t provide enough information to determine 1) in which local model table I need to fetch actual data to build whole CKRecord; and 2) what to put in CKRecord.recordType - whether it’s a User or an Organization.
Question:
What is the best practice for passing or determining the model type (e.g., User or Organization) in nextRecordZoneChangeBatch? How should I handle this in a way that effectively differentiates between the different model types being synced?
Any advice or examples would be greatly appreciated!
Few ideas:
embed the Model type in RecordID.recordName string, but this makes my recordNames longer (like resource_29af3932).
fetch data by recordID in all local persistent storage, but this seems slow and there is constraint that User and Organization IDs should never be the same.
introduce lookup table where from CKRecordID I can look up model type.
Somehow extend CKRecordID to add model type field?
https://github.com/ordo-one/package-benchmark/issues/264
Hi! I am seeing this error specifically when I try to run the Ordo One benchmarks package with a SwiftData context. I am not sure if there is something missing in Ordo One or if this is some kind of legit SwiftData error. My benchmarks seem to be running fine even after the error prints.
Any idea where that error might be coming from (and why I am not seeing that error when running SwiftData from other package executables)?
'Remove Download' button in right click menu for iCloud folder in Finder on macOS does not work if total number of selected files/ folders at the time are more than 10 nos. If more than 10 non of files/ folders are selected at any time, then the 'Remove Download' button disappears from the right click context menu in macOS Finder for iCloud folders.
To click and act on the 'Remove Download' button on context menu of Finder upon right click, the total number of files and folders together must not exceed 10 nos.
Is this the behaviour expected, or am I missing something and this observed behaviour is a bug of Finder?
I came across of something I'm struggling to comprehend. I've got an iOS app based on SwiftUI and SwiftData + CloudKit. I wrote it using Xcode 15 and the target was iOS 17. Everything works fine in this environment, but after upgrading my phone to iOS 18 beta 7 something very strange started to happen with SwiftData on a physical device and in the simulator.
Every time when data is updated, to be precise - when the relationship is modified, the change is reverted after 15 seconds!
I've got the following settings on and nothing can be seen it's going on there in the logs
-com.apple.CoreData.Logging.stderr 1
-com.apple.CoreData.CloudKitDebug 1
-com.apple.CoreData.SQLDebug 1
-com.apple.CoreData.ConcurrencyDebug 1
Here you are some simplified code extraction:
@Model
final public class Note: Identifiable, Hashable
{
public private(set) var uuid = UUID().uuidString
var notification: Notification?
...
}
@Model
final public class Notification: Identifiable, Hashable
{
var dateId: String = ""
@Relationship(deleteRule: .nullify, inverse: \Note.notification) var notes: [Note]?
init(_ dateId: String) {
self.dateId = dateId
}
}
@ModelActor
final public actor DataModelActor : DataModel
{
public func updateNotification(oldDate: Date, newDate: Date? = nil, persistentModelId: PersistentIdentifier) {
if let note = modelContext.model(for: persistentModelId) as? Note {
updateNotification(oldDate: oldDate, newDate: newDate, note: note)
}
try? self.modelContext.save()
}
private func updateNotification(oldDate: Date? = nil, newDate: Date? = nil, note: Note) {
if let oldDate = oldDate {
let notifications = fetchNotifications()
let oldDateId = NotificationDateFactory.getId(from: oldDate)
// removing the note from the collection related to oldDate
if let notification = notifications.first(where: { $0.dateId == oldDateId }) {
if let notificationNotes = notification.notes {
if let notificationNoteIndex = notification.notes!.firstIndex(of: note) {
notification.notes!.remove(at: notificationNoteIndex)
}
if notification.notes == nil || notification.notes!.isEmpty {
self.modelContext.delete(notification)
}
}
}
}
if let newDate = newDate, newDate > Calendar.current.startOfToday() {
// adding to a new collection related to newDate
let notifications = fetchNotifications()
let newDateId = NotificationDateFactory.getId(from: newDate)
if let notification = notifications.first(where: { $0.dateId == newDateId }) {
note.notification = notification
} else {
let notification = Notification(newDateId)
note.notification = notification
}
}
}
}
Spreading save method here and there does not help :(
I've used Core Data Lab software to look into database and I can clearly see data changes are reverted for relationship property.
Example:
In Notification database there is one element:
2024-08-26 (3)
with 3 notes attached. I modified one note to send notification on 2024-08-27. Changes in database reflects situation correctly showing:
2014-08-26 (2)
2024-08-27 (1)
BUT!!! After 15 seconds doing noting database looks like this:
2024-08-26 (3)
2024-08-27 (0)
All changes were reverted and all notes are still attached to the same date as they were at the first place.
Any thoughts?
I have a new app I’m working on. I just ran the app on my own phone and noting was put in CloudKit. I get the following error:
com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x30305d530: "Permission Failure" (10/2007); server message = "Invalid bundle ID for container"; op = 5D9EC664D6A5C463; uuid = 990B1892-07E6-45C9-B718-0B1BD8DED75A>
}>
So none of my SwiftData models are being transferred up the CloudKit. My bundle ID is: com.tazmancoder.MyHomeInventory. I know that everything is setup correctly cause I have another app using SwiftData and I followed the same setup.
I have done the following and nothing has worked:
Go to https://developer.apple.com and sign in
Select Certificates, Identifiers & Profiles
Select Identifiers (App IDs)
Edit App ID for the app
Uncheck iCloud
Save
Check iCloud
Quit Xcode and Clear DerivedData
Run app
I am not sure why this is happening? Can someone please tell me why this happening and how to fix it?
Thanks,
-Mark/*
For example:
SELECT *
FROM accounts
WHERE (platform, innerID) NOT IN (
('platform_value1', 'innerID_value1'),
('platform_value2', 'innerID_value2'),
...
);
this is hard to use Swift Predicate:
func _fetchAccountNotIn(_ scope: [Account]) throws -> [Account] {
let scope = scope.map{ ($0.platform, $0.innerID) }
return try fetch(.init(predicate: #Predicate<Account> { !scope.contains(($0.platform, $0.innerID)) }))
}
shows compiler error: Cannot convert value of type '(String, String)' to expected argument type '((String, String)) throws -> Bool'
Account definition:
@Model
public final class Account {
#Unique<Account>([\.platform, \.innerID])
#Index<Account>([\.platform, \.innerID])
@Attribute(.preserveValueOnDeletion)
public private(set) var platform : String
@Attribute(.preserveValueOnDeletion)
public private(set) var innerID : String
}
My iOS app uses Core Data for local data management and NSPersistentCloudKitContainer to sync data with user’s private iCloud storage. The app has been functioning correctly for several years, but recently, some users have started encountering a CKError.partialFailure error when the app attempts to export data to iCloud. Due to the critical nature of export errors, several features in the app have been disabled to prevent potential data duplication.
Core Data Setup:
lazy var container: NSPersistentContainer = {
let container: NSPersistentContainer
if storeType == .inMemory {
// Used by unit tests
container = NSPersistentContainer(name: "models")
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
} else {
container = NSPersistentCloudKitContainer(name: "models")
}
container.loadPersistentStores { [weak self] _, error in
if let error = error {
self?.logger.error("Failed to load persistent store: \(error)")
fatalError()
}
}
return container
}()
lazy var context: NSManagedObjectContext = {
container.viewContext.name = "main"
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.undoManager = nil
container.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
return container.viewContext
}()
Error Handling:
Following the API documentation on partial failures, I have attempted to log the error’s userInfo property for the CKPartialErrorsByItemIDKey. However, the userInfo object appears to be empty:
guard let ckError = SyncMonitor.shared.lastError as? CKError else {
return
}
logger.error("ckError: \(ckError)")
if ckError.code == CKError.partialFailure {
if let dictionary = ckError.userInfo[CKPartialErrorsByItemIDKey] as? NSDictionary {
for (recordID, error) in dictionary {
logger.error("\(recordID): \(error)")
}
}
}
}
This code results in the following log:
ckError: Error Domain=CKErrorDomain Code=2 "(null)"
CloudKit Logs:
Upon reviewing the CloudKit Console logs, I observed two types of errors: QUOTA_EXCEEDED and BAD_REQUEST, both associated with the “returnedRecordTypes” field showing _pcs_data.
Log 1:
{
"time":"17/08/2024, 19:02:14 UTC",
"database":"PRIVATE",
"zone":"com.apple.coredata.cloudkit.zone",
"userId":<redacted>,
"operationId":"14F4FAE7F4B75973",
"operationType":"RecordSave",
"platform":"iPhone",
"clientOS":"iOS;17.5.x",
"overallStatus":"USER_ERROR",
"error":"QUOTA_EXCEEDED",
"requestId":"12EB47C3-08A9-439B-9560-E38C32EE4643",
"executionTimeMs":"259",
"interfaceType":"NATIVE",
"recordInsertBytes":300418,
"recordInsertCount":25,
"returnedRecordTypes":"_pcs_data"
}
Log 2:
{
"time":"17/08/2024, 18:41:31 UTC",
"database":"PRIVATE",
"zone":"com.apple.coredata.cloudkit.zone",
"userId":<redacted>,
"operationId":"8AC0CDC966F6E903",
"operationType":"RecordSave",
"platform":"iPhone",
"clientOS":"iOS;17.5.x",
"overallStatus":"USER_ERROR",
"error":"BAD_REQUEST",
"requestId":"75AC88E2-BFB7-4A41-977D-8E4067A0F40A",
"executionTimeMs":"283",
"interfaceType":"NATIVE",
"returnedRecordTypes":"_pcs_data"
}
It is worth noting that all RecordSave logs containing my app’s data models in the returnedRecordTypes field have been successful. Additionally, I can confirm that users experiencing this error have ample unused iCloud storage.
Despite extensive research on this topic, I have been unable to find relevant information to resolve this issue. It’s unclear whether this _pcs_data error can be ignored, what kind of quota this error refers to, or where I can find more information about how much space my app has consumed. I would greatly appreciate any help in pointing me in the right direction.
I use Core Data and CloudKit in my iOS app, and everything has worked flawlessly so far. I just got a new Mac with an M-chip and now have to run my app in a Rosetta Simulator. iOS17 Rosetta Simulator works great. But iOS16 Rosetta Simulator crashes as soon as I run any CloudKit code, console prints:
[CK] BUG IN CLIENT OF CLOUDKIT: Not entitled to listen to push notifications. Please add the 'aps-connection-initiate' entitlement.
Although I have "Push Notifications" capability enabled in "Signing and Capabilities" of the project.
OK, I open the .entitlements file as a source code and add:
<key>aps-connection-initiate</key>
<true/>
Can confirm, that it started working in the iOS16 Rosetta Simulator. But now I have an error in the Signing and Capabilities:
Provisioning profile "iOS Team Provisioning Profile: com.###" doesn't include the aps-connection-initiate entitlement.
What exactly is this aps-connection-initiate entitlement? And why haven't I needed it ever before? Should I upload it to App Store ASAP or remove it (since my current version works on iOS16 without this entitlement)
Tried searching the web, couldn't find anything about this 'aps-connection-initiate' :'(
After installing iOS 18.1 and iPados 18.1 we get a consuste failure when trying to add to our one to many data model. This was working well until we installed 18.1
when trying to add a entry to the many relationship we get this error
Illegal attempt to map a relationship containing temporary objects to its identifiers.
I'm using SwiftData with an @Model and am also using an @ModelActor. I've fixed all concurrency issues and have migrated to Swift 6. I am getting a console error that I do not understand how to clear. I get this error in Swift 6 and Swift 5. I do not experience any issue with the app. It seems to be working well. But I want to try to get all issues taken care of. I am using the latest Xcode beta.
error: the replacement path doesn't exist:
"/var/folders/1q/6jw9d6mn0gx1znh1n19z2v9r0000gp/T/swift-generated-sources/@_swiftmacro_17MyAppName14MyModelC4type18_PersistedPr> opertyfMa.swift"
Hi,
when using CKSynEgine it is the responsibility of the app to implement CKSyncEngineDelegate. One of the methods of CKSyncEngineDelegate is nextFetchChangesOptions. The implementation of this method should return a batch of CKRecords so that CKSyncEngine can do the syncing whenever it thinks it should sync. A simple implementation might look like this:
func nextRecordZoneChangeBatch(
_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch?{
await CKSyncEngine.RecordZoneChangeBatch(pendingChanges: syncEngine.state.pendingRecordZoneChanges) { recordID in
// here we should fetch to local representation of the value and map it to a CKRecord
}
}
The problem I am having is as follows:
If the CKRecords I am returning in a batch have dependencies between each other (using CKRecord.Reference or the parent property) but are not part of the same batch, the operation could fail. And as far as I understand, there is no way to prevent this situation because:
A:
The batch I can return is limited in size. If the number of CKRecords is too large, I have to split them into multiple batches.
B:
Splitting them is arbitrary, since I only have the recordID at this point, and there is no way to know about the dependencies between them just by looking at the recordID.
So basically my question is: how should the implementation of nextRecordZoneChangeBatch look like to handle dependencies between CKRecords?
Notes not syncing after Sequoia OS 15.0 update on my MacBook Pro.. My iPhone 13 Pro Max is running Beta iOS 17.6.1. I logged into iCloud.com and noticed the Notes is not in sync with my iPhone either. In checking it's not an item in the Sync List either.
User A shares zone with User B (influenced from https://github.com/apple/sample-cloudkit-zonesharing, but I have just one zone "Contacts" that I am sharing):
private func shareConfiguration() async throws -> (CKShare, CKContainer) {
let container = CKContainer(identifier: "iCloud.com.***.syncer")
let database = container.privateCloudDatabase
let zone = CKRecordZone(zoneName: "Contacts")
let fetchedZone = try await database.recordZone(for: zone.zoneID)
guard let existingShare = fetchedZone.share else {
print("Does not have existing share")
let share = CKShare(recordZoneID: zone.zoneID)
share[CKShare.SystemFieldKey.title] = "Resources"
_ = try await database.modifyRecords(saving: [share], deleting: [])
return (share, container)
}
print("Has existing share")
guard let share = try await database.record(for: existingShare.recordID) as? CKShare else {
throw NSError(domain: "", code: 0, userInfo: nil)
}
return (share, container)
}
...
let (share,container) = try! await shareConfiguration()
shareView = CloudSharingView(container: container, share: share) // UIViewControllerRepresentable implementation
User B accepts share invitation (borrowed from https://github.com/apple/sample-cloudkit-zonesharing)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
guard cloudKitShareMetadata.containerIdentifier == "iCloud.com.***.syncer" else {
print("Shared container identifier \(cloudKitShareMetadata.containerIdentifier) did not match known identifier.")
return
}
// Create an operation to accept the share, running in the app's CKContainer.
let container = CKContainer(identifier: "iCloud.com.***.syncer")
let operation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
debugPrint("Accepting CloudKit Share with metadata: \(cloudKitShareMetadata)")
operation.perShareResultBlock = { metadata, result in
let shareRecordType = metadata.share.recordType
switch result {
case .failure(let error):
debugPrint("Error accepting share: \(error)")
case .success:
debugPrint("Accepted CloudKit share with type: \(shareRecordType)")
}
}
operation.acceptSharesResultBlock = { result in
if case .failure(let error) = result {
debugPrint("Error accepting CloudKit Share: \(error)")
}
}
operation.qualityOfService = .utility
container.add(operation)
}
}
User B through CKSyncEngine is able to read all records. However, when User B tries to write to database through CKSyncEngine, User B on his device gets following error:
<CKSyncEngine 0x1282a1400> error fetching changes with context <FetchChangesContext reason=scheduled options=<FetchChangesOptions scope=all group=CKSyncEngine-FetchChanges-Automatic)>>: Error Domain=CKErrorDomain Code=2 "Failed to fetch record zone changes" UserInfo={NSLocalizedDescription=Failed to fetch record zone changes, CKPartialErrors={
"<CKRecordZoneID: 0x3024872a0; zoneName=Contacts, ownerName=_18fb98f978ce4e9c207daaa142be6024>" = "<CKError 0x30249ed60: \"Zone Not Found\" (26/2036); server message = \"Zone does not exist\"; op = DC9089522F9968CE; uuid = 4B3432A4-D28C-457A-90C5-129B24D258C0; container ID = \"iCloud.com.***.syncer\">";
}}
Also, in CloudKit console, if I go to Zones, I don't see any zones under Shared Database. Wasn't I supposed to see my zone here?
However, I see "Contacts" zone under Private Database. If I expand Zone details I see following:
Zone wide sharing is enabled. All records in this zone are being shared with the sharing participants below.
And under Participants I see both User A and User B. User B is marked as:
Permission READ_WRITE
Type USER
Acceptance INVITED
What puzzles me is why READ works, but not WRITE?