Hello,
I'm trying to understand how dangerous it is to read and/or update model properties from a thread different than the one that instantiated the model.
I know this is wrong when using Core Data and we should always use perform/performAndWait before manipulating an object but I haven't found any information about that for SwiftData.
Question: is it safe to pass an object from one thread (like MainActor) to another thread (in a detached Task for example) and manipulate it, or should we re fetch the object using its persistentModelID as soon as we cross threads?
When running the example app below with the -com.apple.CoreData.ConcurrencyDebug 1 argument passed at launch enabled, I don't get any Console warning when I tap on the "Update directly" button. I'm sure it would trigger a warning if I were using Core Data.
Thanks in advance for explaining.
Axel
--
@main
struct SwiftDataPlaygroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query private var items: [Item]
var body: some View {
VStack {
Button("Add") {
context.insert(Item(timestamp: Date.now))
}
if let firstItem = items.first {
Button("Update directly") {
Task.detached {
// Not the main thread, but firstItem is from the main thread
// No warning in Xcode
firstItem.timestamp = Date.now
}
}
Button("Update using persistentModelID") {
let container: ModelContainer = context.container
let itemIdentifier: Item.ID = firstItem.persistentModelID
Task.detached {
let backgroundContext: ModelContext = ModelContext(container)
guard let itemInBackgroundThread: Item = backgroundContext.model(for: itemIdentifier) as? Item else { return }
// Item on a background thread
itemInBackgroundThread.timestamp = Date.now
try? backgroundContext.save()
}
}
}
}
}
}
@Model
final class Item: Identifiable {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
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'm trying to write a SwiftUI iOS/macOS app that allows users to collaborate, but I keep running into limitations. The latest is that I can't figure out what the UICloudSharingContainer equivalent is on macOS. It doesn't seem like there’s a SwiftUI version of this, so I have to write a lot of platform-specific code to handle it, but it's not clear what the AppKit equivalent is.
I have a working ValueTransformer that runs fine in simulator/device, but crashes in SwiftUI Preview. Even though they are the same code.
Here is my code
import Foundation
final class StringBoolDictTransformer: ValueTransformer {
override func transformedValue(_ value: Any?) -> Any? {
guard let stringBoolDict = value as? [String: Bool] else { return nil }
let nsDict = NSMutableDictionary()
for (key, bool) in stringBoolDict {
nsDict[key] = NSNumber(value: bool)
}
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: nsDict, requiringSecureCoding: true)
return data
} catch {
debugPrint("Unable to convert [Date: Bool] to a persistable form: \(error.localizedDescription)")
return nil
}
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
do {
guard let nsDict = try NSKeyedUnarchiver.unarchivedDictionary(ofKeyClass: NSString.self, objectClass: NSNumber.self, from: data) else {
return nil
}
var result = [String: Bool]()
for (key, value) in nsDict {
result[key as String] = value.boolValue
}
return result
} catch {
debugPrint("Unable to convert persisted Data to [Date: Bool]: \(error.localizedDescription)")
return nil
}
}
override class func allowsReverseTransformation() -> Bool {
true
}
override class func transformedValueClass() -> AnyClass {
NSDictionary.self
}
}
and here is the container
public struct SwiftDataManager {
public static let shared = SwiftDataManager()
public var sharedModelContainer: ModelContainer
init() {
ValueTransformer.setValueTransformer(
StringBoolDictTransformer(), forName: NSValueTransformerName("StringBoolDictTransformer")
)
let schema = Schema([,
Plan.self
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
sharedModelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
}
and the model
@Model
final class Plan {
@Attribute(.transformable(by: StringBoolDictTransformer.self))
var dict: [String: Bool] = [:]
}
I would get that container and pass it in appdelegate and it works fine. I would get that container and pass it inside a #Preview and it would crash with the following:
Runtime: iOS 17.5 (21F79) - DeviceType: iPhone 15 Pro
CoreFoundation:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for attribute: property = "dict"; desired type = NSDictionary; given type = _NSInlineData; value = {length = 2, bytes = 0x7b7d}.'
libsystem_c.dylib:
abort() called
Version 16.0 (16A242d)
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?
I am trying to get my head around SwiftData, and specifically some more "advanced" ideas that I have not seen covered in the various tutorials.
Specifically, I have a class that includes a collection that may or may not contain elements. For now I am experimenting with a simple array of Date, and I don't know if I should make it an optional, or an empty array. Without SwiftData in the mix it seems like it's probably programmers choice, but I wonder if SwiftData handles those two scenarios differently, that would suggest one over the other.
Recently I've been working on a demo project called iLibrary. The main goal was to learn more about CloudKit and SwiftData. After a while I noticed that there were some hangs/freezes when running the app in debug mode.
I first tried this with Xcode 15.4 and iOS 17.5. Here the hang only appears at the beginning, but only for a few seconds. But when I exit debug mode, there are no more hangs.
With Xcode 16 beta 4 and iOS 18 it looks completely different. In this case, the hangs and freezes are always present, whether in debug mode or not. And it's not just at the beginning, it's throughout the app. I'm aware that this is still a beta, but I still find this weird. And when I profile this I see that the main thread gets quite overloaded. Interestingly, my app doesn't have that many operations going on. So I guess something with the sync of SwiftData or my CloudKitManger where I fetch some records from the public database is not running fine.
Lastly, I wanted to delete the iCloud app data. So I went to Settings and tried to delete it, but it didn't work. Is this normal?
Does anyone have any idea what this could be? Or has anyone encountered this problem as well? I'd appreciate any support.
My project: https://github.com/romanindermuehle/iLibrary
First off, given that I didn't find a tag for Code Review, I hope I am not out of scope for the forums here.
Second, some background. I am a long time Windows Power Shell developer, moving to Swift because I don't like self loathing. :)
Currently I am trying to get my head around SwiftData, and experimenting with creating a Service to handle the actual SwiftData functionality, and a Manager to handle various tasks that relate to instances of the Model. I am doing this realizing that it MAY NOT be the best approach, but it gives me reps both producing code and thinking about how to solve a problem, which I think is useful even if the actual product in throw away. That said, I am hoping someone with more experience than I can comment on this approach, especially with respect to expanding to more models, more complex models, lots of data and a desire to use ModelActor eventually.
DataManagerApp.swift
import SwiftData
import SwiftUI
@main
struct DataManagerApp: App {
let container: ModelContainer
init() {
let schema = Schema([DataModel.self])
let config = ModelConfiguration("SwiftDataStore", schema: schema)
do {
let modelContainer = try ModelContainer(for: schema, configurations: config)
DataService.instance.assignContainer(modelContainer)
container = modelContainer
} catch {
fatalError("Could not configure SwiftData ModelContainer.")
}
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(container)
}
}
}
DataModel.swift
import Foundation
import SwiftData
@Model
final class DataModel {
var date: Date
init(date: Date) {
self.date = date
}
}
final class DataService {
static let instance = DataService()
private var modelContainer: ModelContainer?
private var modelContext: ModelContext?
private init() {}
func assignContainer(_ container: ModelContainer) {
if modelContainer == nil {
modelContainer = container
modelContext = ModelContext(modelContainer!)
} else {
print("Attempted to assign ModelContainer more than once.")
}
}
func addModel(_ dataModel: DataModel) {
modelContext?.insert(dataModel)
}
func removeModel(_ dataModel: DataModel) {
modelContext?.delete(dataModel)
}
}
final class ModelManager {
static let instance = ModelManager()
let dataService: DataService = DataService.instance
private init() {}
func newModel() {
let newModel = DataModel(date: Date.now)
DataService.instance.addModel(newModel)
}
}
ContentView.swift
import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var sortOrder = SortDescriptor(\DataModel.date)
@Query(sort: [SortDescriptor(\DataModel.date)]) var models: [DataModel]
var body: some View {
VStack {
addButton
List {
ForEach(models) { model in
modelRow(model)
}
}
.listStyle(.plain)
}
.padding()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
addButton
}
}
}
}
private extension ContentView {
var addButton: some View {
Button("+ Add") {
ModelManager.instance.newModel()
}
}
func modelRow(_ model: DataModel) -> some View {
HStack {
Text(model.date.formatted(date: .numeric, time: .shortened))
Spacer()
}
}
}
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?
I've been struggling to get a ValueTransformer to work while developing in Xcode 16 for iOS 18. Despite thinking I had everything set up correctly, I keep encountering the following error whenever I create a tag:
let tag = Tag(name: name, color: color.toPlatformColor()) // Converts it to NSColor or UIColor
modelContext.insert(tag)
SwiftData/DataUtilities.swift:184: Fatal error: Unable to determine the primitive for Attribute - name: color, options: [transformable with Optional("ColorTransformer")], valueType: UIColor, defaultValue: UIExtendedSRGBColorSpace 0 0 1 1, hashModifier: nil
Here’s what I’m dealing with:
Tag Model:
@Model
public final class Tag: Identifiable {
var name: String = ""
@Attribute(.transformable(by: ColorTransformer.self))
var color: PlatformColor = PlatformColor.blue
init(name: String, color: PlatformColor) {
self.name = name
self.color = color
}
}
ColorTransformer:
final class ColorTransformer: ValueTransformer {
override func transformedValue(_ value: Any?) -> Any? {
guard let color = value as? PlatformColor else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(
withRootObject: color, requiringSecureCoding: true)
return data
} catch {
assertionFailure("Failed to transform `PlatformColor` to `Data`")
return nil
}
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return PlatformColor.black }
do {
let color = try NSKeyedUnarchiver.unarchivedObject(
ofClass: PlatformColor.self, from: data as Data)
return color
} catch {
assertionFailure("Failed to transform `Data` to `PlatformColor`")
return nil
}
}
override class func transformedValueClass() -> AnyClass {
return PlatformColor.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
public static func register() {
ValueTransformer.setValueTransformer(ColorTransformer(), forName: .colorTransformer)
}
}
extension NSValueTransformerName {
static let colorTransformer = NSValueTransformerName(rawValue: "ColorTransformer")
}
Platform Alias:
#if os(macOS)
typealias PlatformColor = NSColor
#else
typealias PlatformColor = UIColor
#endif
The ValueTransformer is registered when the ModelContainer is created at app startup:
var sharedModelContainer: ModelContainer = {
ColorTransformer.register()
// Other configurations...
}()
I've also tried not aliasing the colors to see if that changes anything, but I still encounter the same issue. Any guidance or suggestions would be greatly appreciated!
Is this Relationship correct? Does this cause a circular reference? This runs on 18 but crashes on 17 in the swift data internals.
@Model
final class Item {
@Attribute(.unique) var id: UUID
var date: Date
@Relationship(deleteRule: .nullify, inverse: \Summary.item) var summary: Summary?
init(date: Date = Date.now) {
self.id = UUID()
self.date = Calendar.current.startOfDay(for: date)
self.summary = Summary(self)
}
}
@Model
final class Summary {
@Attribute(.unique) var id = UUID()
@Relationship var item: Item?
init(_ item: Item) {
self.item = item
}
}
Hi,
I have a mac os app that I am developing. It is backed by a SwiftData database. I'm trying to set up cloudkit so that the app's data can be shared across the user's devices. However, I'm finding that every tutorial i find online makes it sound super easy, but only discusses it from the perspective of ios.
The instructions typically say:
Add the iCloud capability.
Select CloudKit from its options.
Press + to add a new CloudKit container, or select one of your existing ones.
Add the Background Modes capability.
Check the box "Remote Notifications" checkbox from its options.
I'm having issue with the following:
I don't see background modes showing up or remote notifications checkbox since i'm making a mac os app.
If i do the first 3 steps only, when i launch my app i get an app crash while trying to load the persistent store. Here is the exact error message:
Add the iCloud capability.
Select CloudKit from its options.
Press + to add a new CloudKit container, or select one of your existing ones.
Add the Background Modes capability.
Check the box "Remote Notifications" checkbox from its options.
Any help would be greatly appreciated.
var sharedModelContainer: ModelContainer = {
let schema = Schema([One.self, Two.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
The fatal error in the catch block happens when i run the app.
I created a new index on two record types on Oct 12th. I still cannot query the records using the new queryable index on records that were created before that date. There is no indication in the schema history that the reindexing has started, completed, failed, or still in progress.
What is the expectation for new indices being applied to existing records? Well over a week seems unacceptable for a database that has maybe 5000 records across a few record types.
When I query my data using an old index and an old record field, I get hundreds of matching results so I know the data is there.
FB15554144 - CloudKit / CloudKit Console: PRODUCTION ISSUE - Query against index created two weeks ago not returning all data as expected
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).
I'm having some trouble with the following function from the CKSyncEngineDelegate protocol.
func nextRecordZoneChangeBatch(_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine) async -> CKSyncEngine.RecordZoneChangeBatch? {
The sample code from the documentation is
func nextRecordZoneChangeBatch(
_ context: CKSyncEngine.SendChangesContext,
syncEngine: CKSyncEngine
) async -> CKSyncEngine.RecordZoneChangeBatch? {
// Get the pending record changes and filter by the context's scope.
let pendingChanges = syncEngine.state.pendingRecordZoneChanges
.filter { context.options.zoneIDs.contains($0) }
// Return a change batch that contains the corresponding materialized records.
return await CKSyncEngine.RecordZoneChangeBatch(
pendingChanges: pendingChanges) { self.recordFor(id: $0) }
}
init?(pendingChanges: [CKSyncEngine.PendingRecordZoneChange], recordProvider: (CKRecord.ID) -> (CKRecord?)) works fine for the sample app which only has one record type, but it seems incredible inefficient for my app which has a dozen different record types. The recordProvider gives you a CKRecord.ID, but not the CKRecord.RecordType. Searching each record type for a matching ID seems very inefficient.
Doesn't the CKSyncEngine.PendingRecordZoneChange contain an array of CKRecords, not just CKRecord.IDs? According to the documentation CKSyncEngine.RecordZoneChangeBatch has a recordsToSave property, but Xcode reports 'CKSyncEngine.PendingRecordZoneChange' has no member 'recordsToSave'
I'm looking for someway to get the CKRecords from syncEngine.state.pendingRecordZoneChanges.
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?
I've been working with SwiftData and encountered a perplexing issue that I hope to get some insights on.
When using a @Model that has a one-to-many relationship with another @Model, I noticed that if there are multiple class variables involved, SwiftData seems to struggle with correctly associating each variable with its corresponding data.
For example, in my code, I have two models: Book and Page. The Book model has a property for a single contentPage and an optional array of pages. However, when I create a Book instance and leave the pages array as nil, iterating over pages unexpectedly returns the contentPage instead.
You can check out the code for more details here. Has anyone else faced this issue or have any suggestions on how to resolve it? Any help would be greatly appreciated!
I dont understand. How does using appended help here? I am not adding anything to the array. Here is the summary
The following code defines two SwiftData models: Book and Page. In the Book class, there is a property contentPage of type Page, and an optional array pages that holds multiple Page instances.
@Model
class Book {
var id = UUID()
var title: String
var contentPage: Page
var pages: [Page]?
init(id: UUID = UUID(), title: String, contentPage: Page) {
self.id = id
self.title = title
self.contentPage = contentPage
contentPage.book = self
}
func addPage(page: Page) {
if pages == nil {
pages = []
}
page.book = self
pages?.append(page)
}
}
enum PageType: String, Codable {
case contentsPage = "Contents"
case picturePage = "Picture"
case textPage = "Text"
case blankPage = "Blank"
}
@Model
class Page {
var id = UUID()
var pageType: PageType
var pageNumber: Int
var content: String
var book: Book?
init(id: UUID = UUID(), pageType: PageType, content: String, pageNumber: Int) {
self.id = id
self.pageType = pageType
self.pageNumber = pageNumber
self.content = content
}
}
Observed Behavior:
With the code above, I created a Book instance and populated all fields except for the pages, which was left as nil. However, when I attempt to iterate over the pages, I receive the contentPage instead. This indicates that there may be an issue with how SwiftData handles these associations.
Expected behavior - when iterating over pages I should not see contentPage since it is a separate property
When trying to run my document-based iPad app using iPadOS 18 beta and Xcode 16 beta, I get an error like the following after opening a document:
Thread 1: Fatal error: Failed to identify a store that can hold instances of SwiftData._KKMDBackingData<MyProject.MyModel> from [:]
In order to help track down what is going wrong, I downloaded the sample app from WWDC23 session "Build an app with SwiftData" found here: https://developer.apple.com/documentation/swiftui/building-a-document-based-app-using-swiftdata
When I try to run the end-state of that sample code, I get a similar error when running the app on my iPad and creating a new deck:
Thread 1: Fatal error: Failed to identify a store that can hold instances of SwiftData._KKMDBackingData<SwiftDataFlashCardSample.Card> from [:]
Given that the sample project is generating the same error as my own project, is this a problem with SwiftData and document-based apps in general? Or is there a change of approach that I should try?
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.
Many years ago I put an attribute named throws in a core data entity.
Now I want to extract the data and move it to a new swift data with more function. I try to rename the entity to avoid compile problems with throws being a swift keyword, now banned as a SwiftData field.
I need a code path to extract from the original core data using swift. The SwiftData macros seem to choke on the throws keyword and substitute blanks.
DB rename of the attribute still uses the original throws name at the code level.
I frequently referred to Apple’s tutorial on SwiftData in Swift, but recently I haven’t been able to access it. Is there a reason why?
Create, update, and delete data - De velop in Swift Tutorials
https://developer.apple.com/tutorials/develop-in-swift/create-update-and-delete-data