I have an app that needs to store a SwiftUI Color within SwiftData and I was wondering if anyone had found a way to do so easily and accurately.
I'd prefer not to have to store the Color components (e.g. RGB values) and would ideally like to have a single variable in the @Model that stores the Color.
I had considered using an extension to the Color type to create a HEX encoded String of the Color and an initializer that creates a Color from the HEX encoded String. Unfortunately, doing so proved not to be accurate due data loss when converting component values to integers.
When testing this in Photoshop, the original color #FBAA1D became #FFAB00.
Is there a way to accurately store the Color in SwiftData, possibly using a binary conversion to Data or somehow storing the Color.Resolved, which itself does not appear to be compatible with SwiftData.
Any thoughts on how to best store the Color accurately within SwiftData would be greatly appreciated.
SwiftData
RSS for tagSwiftData is an all-new framework for managing data within your apps. Models are described using regular Swift code, without the need for custom editors.
Posts under SwiftData tag
200 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I have an app with the following model:
@Model class TaskList {
@Attribute(.unique)
var name: String
// Relationships
var parentList: TaskList?
@Relationship(deleteRule: .cascade, inverse: \TaskList.parentList)
var taskLists: [TaskList]?
init(name: String, parentTaskList: TaskList? = nil) {
self.name = name
self.parentList = parentTaskList
self.taskLists = []
}
}
If I run the following test, I get the expected results - Parent has it's taskLists array updated to include the Child list created. I don't explicitly add the child to the parent array - the parentList relationship property on the child causes SwiftData to automatically perform the append into the parent array:
@Test("TaskList with children with independent saves are in the database")
func test_savingRootTaskIndependentOfChildren_SavesAllTaskLists() async throws {
let modelContext = TestHelperUtility.createModelContext(useInMemory: false)
let parentList = TaskList(name: "Parent")
modelContext.insert(parentList)
try modelContext.save()
let childList = TaskList(name: "Child")
childList.parentList = parentList
modelContext.insert(childList)
try modelContext.save()
let fetchedResults = try modelContext.fetch(FetchDescriptor<TaskList>())
let fetchedParent = fetchedResults.first(where: { $0.name == "Parent"})
let fetchedChild = fetchedResults.first(where: { $0.name == "Child" })
#expect(fetchedResults.count == 2)
#expect(fetchedParent?.taskLists.count == 1)
#expect(fetchedChild?.parentList?.name == "Parent")
#expect(fetchedChild?.parentList?.taskLists.count == 1)
}
I have a subsequent test that deletes the child and shows the parent array being updated accordingly.
With this context in mind, I'm not seeing these relationship updates being observed within SwiftUI. This is an app that reproduces the issue. In this example, I am trying to move "Finance" from under the "Work" parent and into the "Home" list.
I have a List that loops through a @Query var taskList: [TaskList] array. It creates a series of children views and passes the current TaskList element down into the view as a binding.
When I perform the operation below the "Finance" element is removed from the "Work" item's taskLists array automatically and the view updates to show the removal within the List. In addition to that, the "Home" item also shows "Finance" within it's taskLists array - showing me that SwiftData is acting how it is supposed to - removed the record from one array and added it to the other.
The View does not reflect this however. While the view does update and show "Finance" being removed from the "Work" list, it does not show the item being added to the "Home" list. If I kill the app and relaunch I can then see the "Finance" list within the "Home" list. From looking at the data in the debugger and in the database, I've confirmed that SwiftData is working as intended. SwiftUI however does not seem to observe the change.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
try! modelContext.save()
dismiss()
}
}
To troubleshoot this, I modified the above code so that I explicitly add the "Finance" list to the "Home" items taskLists array.
ToolbarItem {
Button("Save") {
list.name = viewModel.name
list.parentList = viewModel.parentTaskList
if let newParent = viewModel.parentTaskList {
// MARK: Bug - This resolves relationship not being reflected in the View
newParent.taskLists?.append(list)
}
try! modelContext.save()
dismiss()
}
}
Why does my explicit append call solve for this? My original approach (not manually updating the arrays) works fine in every unit/integration test I run but I can't get SwiftUI to observe the array changes.
Even more strange is that when I look at viewModel.parentTaskList.taskLists in this context, I can see that the list item already exists in it. So my code effectively tries to add it a second time, which SwiftData is smart enough to prevent from happening. When I do this though, SwiftUI observes a change in the array and the UI reflects the desired state.
In addition to this, if I replace my custom list rows with an OutlineGroup this issue doesn't manifest itself. SwiftUI stays updated to match SwiftData when I remove my explicit array addition.
I don't understand why my views, which is passing the TaskList all the way down the stack via Bindable is not updating while an OutlineGroup does.
I have a complete reproducible ContentView file that demonstrates this as a Gist. I tried to provide the source here but it was to much for the post.
One other anecdote. When I navigate to the TaskListEditorScreen and open the TaskListPickerScreen I get the following series of errors:
error: the replacement path doesn't exist: "/var/folders/07/3px_03md30v9n105yh3rqzvw0000gn/T/swift-generated-sources/@_swiftmacro_09SwiftDataA22UIChangeDetectionIssue20TaskListPickerScreenV9taskLists33_A40669FFFCF66BB4EEA5302BB5ED59CELL5QueryfMa.swift"
I saw another post regarding these and I'm wondering if my issue is related to this.
So my question is, do I need to handle observation of SwiftData models containing arrays differently in my custom views? Why do bindings not observe changes made by SwiftData but they observe changes made explicitly by me?
Hey there,
I’m feeling pretty desperate at this point, as my most recent update to Xcode 16.1 and the new 18.1 simulators has basically made it impossible for me to work on my apps.
The same app and same code run fine in the 18.0 simulators with the same iCloud account logged in.
I’ve tried multiple simulators with the same results, even on different computers. I’ve also tried logging in repeatedly without any luck. The CloudKit database logs don’t show any errors or suspicious entries. Reinstalling the app on the simulator doesn't help either.
Whenever I launch the application in Xcode, I'm getting:
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1240): <NSCloudKitMirroringDelegate: 0x600003d213b0>: Failed to set up CloudKit integration for store: <NSSQLCore: 0x103f124e0> (URL: file:///Users/kerstenbroich/Library/Developer/CoreSimulator/Devices/57BC78CE-DB2A-4AC0-9D7A-43C386305F56/data/Containers/Data/Application/EFDE9B05-0584-47C5-80AE-F2FF5994860C/Library/Application%20Support/Model.sqlite)
<CKError 0x600000d3dfe0: "Partial Failure" (2/1011); "Failed to modify some record zones"; partial errors: {
com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x600000d7c090: "Internal Error" (1/5000); "Failed user key sync">
}>
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2310): <NSCloudKitMirroringDelegate: 0x600003d213b0> - Attempting recovery from error: <CKError 0x600000d3dfe0: "Partial Failure" (2/1011); "Failed to modify some record zones"; partial errors: {
com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x600000d7c090: "Internal Error" (1/5000); "Failed user key sync">
}>
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:]_block_invoke(2773): <NSCloudKitMirroringDelegate: 0x600003d213b0>: Found unknown error as part of a partial failure: <CKError 0x600000d7c090: "Internal Error" (1/5000); "Failed user key sync">
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:](2820): <NSCloudKitMirroringDelegate: 0x600003d213b0>: Error recovery failed because the following fatal errors were found: {
"<CKRecordZoneID: 0x600000d62340; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>" = "<CKError 0x600000d7c090: \"Internal Error\" (1/5000); \"Failed user key sync\">";
}
Any help/ideas would be much appreciated, because I have no clue what to try next.
Thanks a lot!
I'm using SwiftData to store data in my app and I recently had to store both image data and colors.
I have therefore added two variables to my model, one of type Data? and the other of type Color.Resolved?
If both are set to nil then I can call context.save() without any error but when providing a value of type Color.Resolved, the following error message occurs: Thread 1: Fatal error: Composite Coder only supports Keyed Container.
Any guidance on how to solve this and what needs to be done to store image data and colors with SwiftData?
Maybe I didn't find the relevant instructions.
In my code, I only want to get the first 7 elements.
At present, my code is as follows:
@Query(sort:\Record.date, order: .reverse) private var records:[Record]
But I wonder if once the number of records is large, will it affect the efficiency?
In View, it is enough for me to count the first 7 elements in records. What should I do?
Maybe I didn't find the relevant instructions.
In my code, I only want to get the first 7 elements.
At present, my code is as follows:
@Query(sort:\Record.date, order: .reverse) private var records:[Record]
But I wonder if once the number of records is large, will it affect the efficiency?
In View, it is enough for me to count the first 7 elements in records. What should I do?
Hi,
The dataModule in code below is a swiftData object being passed to the view and its property as key path but when trying to modify it I'm getting the error ."Cannot assign through subscript: 'self' is immutable" how to solve this issue ?
Kind Regards
struct ListSel<T: PersistentModel>: View {
@Bindable var dataModule: T
@Binding var txtValue: String
var keyPath: WritableKeyPath<T, String>
var turncate: CGFloat? = 94.0
var image = ""
var body: some View {
HStack {
Text(txtValue)
.foregroundColor(sysSecondary)
.font(.subheadline)
.onChange(of: txtValue) { value in
dataModule[keyPath: keyPath] = value
}
Image(systemName: image)
.foregroundColor(sysSecondary)
.font(.subheadline)
.imageScale(.small)
.symbolRenderingMode(.hierarchical)
.scaleEffect(0.8)
}
.frame(width: turncate, height: 20, alignment: .leading)
.truncationMode(.tail)
}
}
Hi,
I'm developing an app for iOS and MacOS with SwiftData and CloudKit syncing. I had sync working very well with a set of models. This schema was also pushed to CloudKit production.
Last week I added several models, and several relationship properties linking my existing models to newly added models. The schema updated just fine and everything worked.
Then things went sideways: earlier this week I decided I wanted to rename a property on one of my new models. So I renamed the property, removed the application support folder for my local debug app (on Mac OS), removed the app from the iOS Simulator (to clear its local database), and finally reset my CloudKit container to its Production schema. Basically, I tried to go back to the same state I had as when I first added the new models.
However, this time things don't go so smoothly, even after starting the app several times, rebooting my machine, turning iCloud on and off in Xcode and MacOS and iOS. When I look in CloudKit console, I see only my old models there: none of the new ones are added.
I'd love some pointers on how I can best debug this issue, as I feel completely stuck.
On MacOS
I have very little
mac-logs.txt
to go on. Since the logs are a bit lengthy I've added them as an attachment. I get a few warnings, but it is unclear what they are warning me about.
One thing that does stand out is that I am running the CloudKit in Development mode here. However, the logs do state accountPartition=Prod . And when I query CKContainer.default() for the container environment, the response is sandbox, which matches Development!
On iOS
The logs show a few errors, but I cannot make sense of them.
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(1240): <NSCloudKitMirroringDelegate: 0x600003d09860>: Failed to set up CloudKit integration for store: <NSSQLCore: 0x103325c90> (URL: file:///Users/bastiaan/Library/Developer/CoreSimulator/Devices/BF847CE5-A3E2-4B4C-8CD5-616B75B29AFE/data/Containers/Data/Application/0A916F67-B9B2-457B-8FA7-8C42819EA9AA/Library/Application%20Support/default.store)
<CKError 0x600000c433f0: "Partial Failure" (2/1011); "Failed to modify some record zones"; partial errors: {
com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x600000c956b0: "Internal Error" (1/5005); "Couldn't create new PCS blob for zone <CKRecordZoneID: 0x600000c475d0; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>">
}>
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate recoverFromError:](2310): <NSCloudKitMirroringDelegate: 0x600003d09860> - Attempting recovery from error: <CKError 0x600000c433f0: "Partial Failure" (2/1011); "Failed to modify some record zones"; partial errors: {
com.apple.coredata.cloudkit.zone:__defaultOwner__ = <CKError 0x600000c956b0: "Internal Error" (1/5005); "Couldn't create new PCS blob for zone <CKRecordZoneID: 0x600000c475d0; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>">
}>
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:]_block_invoke(2773): <NSCloudKitMirroringDelegate: 0x600003d09860>: Found unknown error as part of a partial failure: <CKError 0x600000c956b0: "Internal Error" (1/5005); "Couldn't create new PCS blob for zone <CKRecordZoneID: 0x600000c475d0; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>">
error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _recoverFromPartialError:forStore:inMonitor:](2820): <NSCloudKitMirroringDelegate: 0x600003d09860>: Error recovery failed because the following fatal errors were found: {
"<CKRecordZoneID: 0x600000c8fd50; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>" = "<CKError 0x600000c956b0: \"Internal Error\" (1/5005); \"Couldn't create new PCS blob for zone <CKRecordZoneID: 0x600000c475d0; zoneName=com.apple.coredata.cloudkit.zone, ownerName=__defaultOwner__>\">";
}
And in CloudKit logs I see:
06/11/2024, 9:09:59 UTC 738513AC-9326-42DE-B4E2-DA51F6462943 iOS;18.1 ZoneFetch EphemeralGroup
{
"time":"06/11/2024, 9:09:59 UTC"
"database":"PRIVATE"
"zone":"com.apple.coredata.cloudkit.zone"
"userId":"_0d9445f850459ec351330ca0fde4134f"
"operationId":"611BA98C9B10D3F2"
"operationGroupName":"EphemeralGroup"
"operationType":"ZoneFetch"
"platform":"iPhone"
"clientOS":"iOS;18.1"
"overallStatus":"USER_ERROR"
"error":"ZONE_NOT_FOUND"
"requestId":"738513AC-9326-42DE-B4E2-DA51F6462943"
"executionTimeMs":"53"
"interfaceType":"NATIVE"
}
Any pointers are greatly appreciated!
Bastiaan
Hi,
In this year WWDC 2024 conference a new update to SwiftData allows it to connect to other kind of data sources or databases, at ;east this is what I understood, since then didn't see any tutorial or help document on how to do that for example connecting SwiftData to Firebase if possible ?
Kind Regards
Hi
When connecting a SwiftData module property to a SwiftUI view such as a text field and the field changes by user the property get updated in the SwiftData database, now suppose I want to run a validation code or delay updates to Database till use click a submit button how to do that ? delay those auto updates if we can name it ?
Kind Regards
Code Example
import SwiftUI
import SwiftData
struct GListSel2: View {
@Bindable var patient: Patient
var body: some View {
HStack {
TextField("Gender", text: $patient.gender)
}
}
}
Xcode: 16.1
macOS: Sequoia
When I run widget preview, I got the following errors
CoreData: error: Store failed to load. <NSPersistentStoreDescription: 0x156237310> (type: SQLite, url: file:///Users/user/Library/Group%20Containers/group.com.app.name/Library/Application%20Support/default.store) with error = Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={reason=Unknown failure to access file: 1} with userInfo {
reason = "Unknown failure to access file: 1";
}
Unresolved error loading container Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." UserInfo={reason=Unknown failure to access file: 1}
with this code
let sharedModelContainer: ModelContainer = {
let schema = Schema([Company.self, Person.self])
do {
return try ModelContainer(for: schema, configurations: [.init(isStoredInMemoryOnly: false)])
} catch {
fatalError("error: \(error)")
}
}()
Does anyone know why this happens and how to fix this?
Hi all,
Is this allowed:
environment(\.container, MyContainer() as Any)
While injecting Model container into the environment we use Type . Is there any universal injection type that can inject container as set of any types rather than array similar to navigationPath() type-eraser ?
Re SwiftData: is my understanding correct : generally speaking and by default insert method inserts objects into the context and context automatically persist - e.g. inserts them into container while the delete method does not - it only deletes from context and context does not delete them from the container unless save is called ? It is not clear from the documentation nor from the definitions :
public func delete<T>(model: T.Type, where predicate: Predicate<T>? = nil, includeSubclasses: Bool = true) throws where T : PersistentModel
//How can I test it ?
I’m keen to learn where I can confirm this in Apple’s documentation or official articles, code definitions, apart from experimenting or consulting third-party materials. Where does it explicitly state that SwiftData includes an automatic saving feature but does not offer automatic deletion?
"Meet SwiftData" (WWDC23): Around the 14:30 mark, Apple mentions that SwiftData automatically saves changes "at opportune moments." But nothing is advised re deleting ?
Are we supposed to be taking hints :
"Build an app with SwiftData" (WWDC23): This session demonstrates using context.save() to persist changes after deleting an object, implies the idea that deletion isn't automatic
How to truly learn if you do not have official materials ? This is exact Science, not archeology or history.
I feel like a speleologist.
I am trying to convert a string field to an integer field in our database schema. However, the custom migration that I write doesn't seem to run.
My Model
//Run 1
//typealias Book = BookSchemaV1.Book
//Run 2
typealias Book = BookSchemaV2.Book
// MARK: - Migration Plan
enum BookModelMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
BookSchemaV1.self,
BookSchemaV2.self
]
static var stages: [MigrationStage] = [migrateV1toV2]
static var oldBooks: [BookSchemaV1.Book] = []
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: BookSchemaV1.self,
toVersion: BookSchemaV2.self,
willMigrate: nil,
didMigrate: { context in
oldBooks = try context.fetch(FetchDescriptor<BookSchemaV1.Book>())
oldBooks.forEach { oldBook in
do {
let newBook = BookSchemaV2.Book(
bookID: String(oldBook.bookID),
title: oldBook.title
)
context.insert(newBook)
context.delete(oldBook)
try context.save()
} catch {
print("New model not saved")
}
}
}
)
}
// MARK: - Schema Versions
enum BookSchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] = [Book.self]
static var versionIdentifier = Schema.Version(1, 0, 0)
@Model
final class Book {
@Attribute(.unique) var bookID: Int
var title: String
init(
bookID: Int,
title: String
) {
self.bookID = bookID
self.title = title
}
}
}
enum BookSchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] = [Book.self]
static var versionIdentifier = Schema.Version(2, 0, 0)
@Model
class Book {
@Attribute(.unique) var bookID: String
var title: String
init(
bookID: String,
title: String
) {
self.bookID = bookID
self.title = title
}
}
}
@MainActor
class AppDataContainer {
static let shared = AppDataContainer()
let container: ModelContainer
private init() {
do {
let schema = Schema([Book.self])
let config = ModelConfiguration(schema: schema)
container = try ModelContainer(for: schema, migrationPlan: BookModelMigrationPlan.self, configurations: [config])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
}
I'm facing a weird issue with SwiftData. I want to have one database that's local to the device and one that syncs to iCloud. In this example, LTRLink should be synced via iCloud while LTRMetadata should stay on-device only. I've it configured like the following:
let schema = Schema([LTRLink.self, LTRMetadata.self])
let cloudkitConfiguration = ModelConfiguration("Remote",
schema: schema,
url: FileManager.remoteDatabaseFolderURL.appending(path: "Remote.sqlite"),
cloudKitDatabase: .private("iCloud.com.xavimoll.abyss3"))
let localConfiguration = ModelConfiguration("Local",
schema: schema,
url: FileManager.localDatabaseFolderURL.appending(path: "Local.sqlite"),
cloudKitDatabase: .none)
return try ModelContainer(for: schema,
configurations: [cloudkitConfiguration, localConfiguration])
For some reason, when I create the iCloud schema, both models end up appearing as records on iCloud. I create the schema like this:
let schema = Schema([LTRLink.self, LTRMetadata.self])
let cloudkitConfiguration = ModelConfiguration("Remote",
schema: schema,
url: FileManager.remoteDatabaseFolderURL.appending(path: "Remote.sqlite"),
cloudKitDatabase: .private("iCloud.com.xavimoll.abyss3"))
#if DEBUG
// Needed to create the schema on iCloud
try autoreleasepool {
let desc = NSPersistentStoreDescription(url: cloudkitConfiguration.url)
let opts = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudkitConfiguration.cloudKitContainerIdentifier!)
desc.cloudKitContainerOptions = opts
desc.shouldAddStoreAsynchronously = false
if let mom = NSManagedObjectModel.makeManagedObjectModel(for: [LTRLink.self]) {
let container = NSPersistentCloudKitContainer(name: "Remote", managedObjectModel: mom)
container.persistentStoreDescriptions = [desc]
container.loadPersistentStores {_, err in
if let err {
fatalError(err.localizedDescription)
}
}
try container.initializeCloudKitSchema()
if let store = container.persistentStoreCoordinator.persistentStores.first {
try container.persistentStoreCoordinator.remove(store)
}
}
}
#endif
let localConfiguration = ModelConfiguration("Local",
schema: schema,
url: FileManager.localDatabaseFolderURL.appending(path: "Local.sqlite"),
cloudKitDatabase: .none)
return try ModelContainer(for: schema,
configurations: [cloudkitConfiguration, localConfiguration])
The logic to initialize the CloudKit schema follows the documentation found here: https://developer.apple.com/documentation/swiftdata/syncing-model-data-across-a-persons-devices#Initialize-the-CloudKit-development-schema
It looks like setting cloudKitDatabase: .none on the init for the ModelConfiguration doesn't do anything, and ends up being synced with iCloud either way. When I go to the iCloud console, I see the following:
Does anyone know if there's any workaround that would allow me to have two databases where only one of them syncs to iCloud when using SwiftData?
Hello,
SwiftData is not working correctly with Swift Concurrency. And it’s sad after all this time.
I personally found a regression. The attached code works perfectly fine on iOS 17.5 but doesn’t work correctly on iOS 18 or iOS 18.1.
A model can be updated from the background (Task, Task.detached or ModelActor) and refreshes the UI, but as soon as the same item is updated from the View (fetched via a Query), the next background updates are not reflected anymore in the UI, the UI is not refreshed, the updates are not merged into the main.
How to reproduce:
Launch the app
Tap the plus button in the navigation bar to create a new item
Tap on the “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice the time is updated
Tap on the “Update from View” (once or many times)
Notice the time is updated
Tap again on “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice that the time is not update anymore
Am I doing something wrong? Or is this a bug in iOS 18/18.1?
Many other posts talk about issues where updates from background thread are not merged into the main thread. I don’t know if they all are related but it would be nice to have
1/ bug fixed, meaning that if I update an item from a background, it’s reflected in the UI, and
2/ proper documentation on how to use SwiftData with Swift Concurrency (ModelActor). I don’t know if what I’m doing in my buttons is correct or not.
Thanks,
Axel
import SwiftData
import SwiftUI
@main
struct FB_SwiftData_BackgroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var simpleModelActor: SimpleModelActor!
@Query private var items: [Item]
var body: some View {
NavigationView {
VStack {
if let firstItem: Item = items.first {
Text(firstItem.timestamp, format: Date.FormatStyle(date: .omitted, time: .standard))
.font(.largeTitle)
.fontWeight(.heavy)
Button("Update from Task") {
let modelContainer: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task {
let context: ModelContext = ModelContext(modelContainer)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from Detached Task") {
let container: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task.detached {
let context: ModelContext = ModelContext(container)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor in State") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await MainActor.run {
simpleModelActor = actor
}
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Divider()
.padding(.vertical)
Button("Update from View") {
firstItem.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
}
.buttonStyle(.bordered)
} else {
ContentUnavailableView(
"No Data",
systemImage: "slash.circle", //
description: Text("Tap the plus button in the toolbar")
)
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
modelContext.insert(Item(timestamp: Date.now))
try? modelContext.save()
}
}
@ModelActor
final actor SimpleModelActor {
var context: String = ""
func updateItem(identifier: Item.ID) {
guard let item = self[identifier, as: Item.self] else {
return
}
item.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try! modelContext.save()
}
}
@Model
final class Item: Identifiable {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
The WWDC2024 custom data store example doesn't provide any details on how one would go about creating a DataStoreSnapshot. The example uses a DefaultSnapshot for persisting the data in the DefaultSnapshot format directly in the JSON file.
There appears to be no documentation or examples of how one might create a DataStoreSnapshot from data from another database.
The Apple documentation for DefaultSnapshot provides no examples of how one might create such a snapshot from data retrieved elsewhere.
Can anyone provide a simple example of how one might create such a snapshot from a remote database such that it can be returned as part of the response to a fetch request.
For the purpose of this example let's assume I have a CSV file with rows of data and code to read the data from this file. How would I create a snapshot or snapshots for each of the rows of data.
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
}
}
Hello,
I have a problem with SwiftData and Predicates that check for the persistentModelID of the relations.
My data model looks simplified like this:
Day -> TimeEntry[] -> Hashtag[]
What I want to achieve is to query the days and associated time entries via assigned tags.
This is my predicate:
let identifier = filterHashtags.map(\.persistentModelID)
...
#Predicate<TimeEntry> { timeEntry in
identifiers.count == timeEntry.tags.filter { tag in
identifiers.contains(tag.persistentModelID)
}.count
}
It does not return any data when I check for the persistentModelID. However, if I use another property of the tags, e.g. the name or a generated UUID for the check, the predicate works. Is this a general problem with PersistentIdentifier in Predicates or am I missing something?
Thanks in advance
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()
}
}
}