I'm currently developing a SwiftUI application that utilizes SwiftData for data management. I am facing a challenge when trying to filter a query. Specifically, I want to filter a list of Item objects to match a Product instance that is passed to my View.
error :
Instance member 'product' cannot be used on type 'MainItemListEncap'; did you mean to use a value of this type instead
The view
//
// 311.1.1. MainRefToItem.swift
// ComparePrice
//
// Created by Herman VAN CAUWELAERT on 11/12/2024.
//
import SwiftUI
import SwiftData
struct MainItemListEncap: View {
@Bindable var product: Product
@Query(
filter: #Predicate { item in
item.productGroup == product
},
sort: [SortDescriptor(\Item.name)]
)
var items: [Item]
@Environment(\.modelContext) var modelContext
var body: some View {
ForEach(items) { item in
VStack(alignment: .leading) {
Text(item.name)
Text(item.description)
}
}
}
}
the product class.
import SwiftData
import Foundation
@Model
final class Product: CustomStringConvertible, CustomDebugStringConvertible {
@Attribute(.unique) var productName: String
var productDescription: String
var outputCurrency: String
// Gebruik een `String` als opslag voor `outputSystem`
var outputSystemRawValue: String = MeasurementSystem.metric.rawValue
// Computed property om `MeasurementSystem` te gebruiken
var outputSystem: MeasurementSystem {
get {
MeasurementSystem(rawValue: outputSystemRawValue)
}
set {
outputSystemRawValue = newValue.rawValue
}
}
// er zijn verschillend item versies voor een product
// als er een hoofdproduct gedelete wordt, dan zullen alle onderliggende items ook gedelete worden
@Relationship(deleteRule: .cascade, inverse: \Item.productGroup) var refToItems = [Item]()
init(productName: String = "", productDescription: String = "what is this product", outputCurrency: String = "EUR", outputSystem: MeasurementSystem = MeasurementSystem.metric) {
self.productName = productName
self.productDescription = productDescription
self.outputCurrency = outputCurrency
self.outputSystem = outputSystem
}
}
the item class
import Foundation
import SwiftData
@Model
final class Item: CustomStringConvertible, CustomDebugStringConvertible {
var timestamp: Date
@Attribute(.unique) var name: String
var productGroup: Product?
var shop: String //TODO: becomes Shop
var price: Double
var currency: String
var quantity: Double
var unit: String //TODO: becomes Unit
var isShown : Bool
var minimumQuantity: String
var rateStr: String {
conversionRate != nil ? " (rate: \(ValueFormatter.shared.format(conversionRate!)))" : ""
}
init(timestamp: Date = Date()
, name: String
, shop: String = "Colruyt"
, price:Double = 1.00
, currency: String = "EUR"
, quantity: Double = 1.0
, unit: String = "g"
, isShown:Bool = true
, conversionRate: Decimal? = 1
, minimumQuantity: String = "1"
) {
self.timestamp = timestamp
self.name = name
self.shop = shop
self.price = price
self.currency = currency
self.quantity = quantity
self.unit = unit
self.isShown = isShown
self.conversionRate = conversionRate
self.minimumQuantity = minimumQuantity
}
}
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 get this red warning in Xcode every time my app is syncing to the iCloud. My model has only basic types and enum that conform to Codable so i'm not sure what is the problem.
App is working well, synchronization works. But the warning doesn't look good.
Maybe someone has idea how to debug it.
Hello! 😊
I currently manage an app called MoneyKeeper that uses SwiftData for its data storage framework. Many users have requested a "sharing" feature, but unfortunately, SwiftData does not yet support this functionality, and it’s unclear when it will. 😭
Initially, I considered using CloudKit and CKSyncEngine to implement quick synchronization and sharing. However, due to the complexity of the current data model’s relationships, modeling it with CloudKit’s schema alone seemed overly complicated and likely to introduce bugs.
As a result, I’ve decided to implement the sharing feature using CoreData and CloudKit. My plan to avoid conflicts with SwiftData includes:
Keeping the local storage locations for SwiftData and CoreData separate.
Using entirely separate CloudKit containers for SwiftData and CoreData.
I believe these measures will minimize potential issues, but I’m wondering if there’s anything else I should consider.
Using both SwiftData (for the personal database) and CoreData (for the shared database) feels like it could lead to significant technical debt in the future, and I anticipate encountering even more challenges during actual implementation.
I’d greatly appreciate your valuable insights on this matter. 🙏
The app MoneyKeeper, currently operated using SwiftData.
https://apps.apple.com/app/id6514279917
Hi,
I'm building a habit tracking app for iOS and macOS. I want to use up to date technologies, so I'm using SwiftUI and SwiftData.
I want to store user data locally on device and also sync data between device and iCloud server so that the user could use the app conveniently on multiple devices (iPhone, iPad, Mac).
I already tried SwiftData + NSPersistentCloudKitContainer, but I need to control when to sync data, which I can't control with NSPersistentCloudKitContainer. For example, I want to upload data to server right after data is saved locally and download data from server on every app open, on pull-to-refresh etc. I also need to monitor sync progress, so I can update the UI and run code based on the progress. For example, when downloading data from server to device is in progress, show "Loading..." UI, and when downloading finishes, I want to run some app business logic code and update UI.
So I'm considering switching from NSPersistentCloudKitContainer to CKSyncEngine, because it seems that with CKSyncEngine I can control when to upload and download data and also monitor the progress.
My database schema (image below) has relationships - "1 to many" and "many to many" - so it's convenient to use SwiftData (and underlying CoreData).
Development environment: Xcode 16.1, macOS 15.1.1
Run-time configuration: iOS 18.1.1, macOS 15.1.1
My questions:
1-Is it possible to use SwiftData for local data storage and CKSyncEngine to sync this local data storage with iCloud?
2-If yes, is there any example code to implement this?
I've been studying the "CloudKit Samples: CKSyncEngine" demo app (https://github.com/apple/sample-cloudkit-sync-engine), but it uses a very primitive approach to local data storage by saving data to a JSON file on disk.
It would be very helpful to have the same demo app with SwiftData implementation!
3-Also, to make sure I don't run into problems later - is it okay to fire data upload (sendChanges) and download (fetchChanges) manually with CKSyncEngine and do it often? Are there any limits how often these functions can be called to not get "blocked" by the server?
4-If it's not possible to use SwiftData for local data storage and CKSyncEngine to sync this local data storage with iCloud, then what to use for local storage instead of SwiftData to sync it with iCloud using CKSyncEngine? Maybe use SwiftData with the new DataStore protocol instead of the underlying CoreData?
All information highly appreciated!
Thanks,
Martin
I have already built an app using SwiftData and now I want to introduce CloudKit features in it, what should I do? I'm totally new to this part. Thank you!
After the problems with the ModelActor in iOS 18, it seemed like the ModelActor became more stable with iOS 18.1 and macOS 15.1.
However, I’m still encountering many problems and crashes. I wanted to ask if these issues are related to my persistence layer architecture or if they’re still inherent to the ModelActor itself.
I’ve generally followed the blog posts:
https://fatbobman.com/en/posts/practical-swiftdata-building-swiftui-applications-with-modern-approaches/
and
https://brightdigit.com/tutorials/swiftdata-modelactor/
and aim to achieve the following:
I have a single DataProvider that holds the ModelContainer and uses it to configure and initialize a single DataHandler. These are created once at app launch and injected into the SwiftUI view hierarchy as EnvironmentObjects. Since I need to access the SwiftData models not only in SwiftUI but also indirectly in ViewModels or UIKit views, all read operations on the models should go through the DataProvider ( ModelContainrs MainContext), while all other CRUD operations are handled centrally via the single DataHandler (executed within a single ModelActor).
Additionally, I want to monitor the entire container using another ModelActor, initialized in the DataProvider, which tracks changes to objects using TransactionHistory.
I’ve managed to implement this to some extent, but I’m facing two main issues:
ModelActor and Main Actor Requirement
The ModelActor only updates SwiftUI views when initialized via the main context of the ModelContainer and therefore runs on the Main Actor. It would be ideal for this to work in the background, but the issue with the ModelActor that existed previously doesn’t seem to have been resolved in iOS 18.1/macOS 15.1—am I wrong about this?
Frequent Crashes (more severe)
Crashes occur, especially when multiple windows on macOS or iPadOS access the same DataHandler to update models. This often leads to crashes during read operations on models by a SwiftUI view, with logs like:
Object 0x111f15480 of class _ContiguousArrayStorage deallocated with non-zero retain count 3. This object's deinit, or something called from it, may have created a strong reference to self which outlived deinit, resulting in a dangling reference.
error: the replacement path doesn't exist: "/var/folders/gs/8rwdjczj225d1pj046w3d97c0000gn/T/swift-generated-sources/@__swiftmacro_12SwiftDataTSI3TagC4uuID18_PersistedPropertyfMa_.swift"
Can't show file for stack frame : <DBGLLDBStackFrame: 0x34d28e170> - stackNumber:1 - name:Tag.uuID.getter. The file path does not exist on the file system: /var/folders/gs/8rwdjczj225d1pj046w3d97c0000gn/T/swift-generated-sources/@__swiftmacro_12SwiftDataTSI3TagC4uuID18_PersistedPropertyfMa_.swift
This error usually happens when there are multiple concurrent accesses to the DataHandler/ModelActor. However, crashes also occur sporadically during frequent accesses from a single view with an error like "the replacement path doesn't exist."
It also seems like having multiple ModelActors, as in this case (one for observation and one for data changes), causes interference and instability. The app appears to crash less frequently when the observer is not initialized, but I can’t verify this—it might just be a coincidence.
My Question:
Am I fundamentally doing something wrong with the ModelActors or the architecture of my persistence layer?
After the significant issues with the ModelActor in iOS 18, it seemed like the ModelActor became more stable with iOS 18.1 and macOS 15.1.
However, I’m still encountering problems and crashes. I wanted to ask if these issues are related to my persistence layer architecture or if they’re still inherent to the ModelActor itself.
I’ve generally followed the blog posts:
https://fatbobman.com/en/posts/practical-swiftdata-building-swiftui-applications-with-modern-approaches/
and
https://brightdigit.com/tutorials/swiftdata-modelactor/
and aim to achieve the following:
I have a single DataProvider that holds the ModelContainer and uses it to configure and initialize a single DataHandler. These are created once at app launch and injected into the SwiftUI view hierarchy as EnvironmentObjects. Since I need to access the SwiftData models not only in SwiftUI but also indirectly in ViewModels or UIKit views, all read operations on the models should go through the DataProvider (and its MainContext), while all other CRUD operations are handled centrally via the single DataHandler (executed within a single ModelActor).
Additionally, I want to monitor the entire container using another ModelActor, initialized in the DataProvider, which tracks changes to objects using TransactionHistory.
I’ve managed to implement this to some extent, but I’m facing two main issues:
1. ModelActor and Main Actor Requirement
The ModelActor only updates SwiftUI views when initialized via the maincontext of the ModelContainer and therefore runs on the Main Actor. It would be ideal for this to work in the background, but the issue with the ModelActor that existed previously doesn’t seem to have been resolved in iOS 18.1/macOS 15.1—am I wrong about this?
2. Frequent Crashes (more severe)
Crashes occur, especially when multiple windows on macOS or on iPad access the same DataHandler to update models. This often leads to crashes during read operations on models by a SwiftUI view (but not only), with logs like:
error: the replacement path doesn't exist: "/var/folders/gs/8rwdjczj225d1pj046w3d97c0000gn/T/swift-generated-sources/@__swiftmacro_12SwiftDataTSI3TagC4uuID18_PersistedPropertyfMa_.swift"
Can't show file for stack frame : <DBGLLDBStackFrame: 0x34d28e170> - stackNumber:1 - name:Tag.uuID.getter. The file path does not exist on the file system: /var/folders/gs/8rwdjczj225d1pj046w3d97c0000gn/T/swift-generated-sources/@__swiftmacro_12SwiftDataTSI3TagC4uuID18_PersistedPropertyfMa_.swift
This error usually happens when there are multiple concurrent accesses to the DataHandler/ModelActor. However, crashes also occur sporadically during frequent accesses from a single view with an error like "the replacement path doesn't exist."
It also seems like having multiple ModelActors, as in this case (one for observation and one for data changes), causes interference and instability. The app appears to crash less frequently when the observer is not initialized, but I can’t verify this—it might just be a coincidence.
My Question:
Am I fundamentally doing something wrong with the ModelActors or the architecture of my persistence layer?
Are there any differences (either performance or memory considerations) between removing an array of model objects directly using .removeAll() vs using modelContext? Or, are they identical?
Attached below is an example to better illustrate the question (i.e., First Way vs Second Way)
// Model Definition
@Model
class GroupOfPeople {
let groupName: String
@Relationship(deleteRule: .cascade, inverse: \Person.group)
var people: [Person] = []
init() { ... }
}
@Model
class Person {
let name: String
var group: GroupOfPeople?
init() { ... }
}
// First way
struct DemoView: View {
@Query private groups: [GroupOfPeople]
var body: some View {
List(groups) { group in
DetailView(group: group)
}
}
}
struct DetailView: View {
let group: GroupOfPeople
var body: some View {
Button("Delete All Participants") {
group.people.removeAll()
}
}
// Second way
struct DemoView: View {
@Query private groups: [GroupOfPeople]
var body: some View {
List(groups) { group in
DetailView(group: group)
}
}
}
struct DetailView: View {
@Environment(\.modelContext) private var context
let group: GroupOfPeople
var body: some View {
Button("Delete All Participants") {
context.delete(model: Person.self, where: #Predicate { $0.group.name == group.name })
} // assuming group names are unique. more of making a point using modelContext instead
}
Hi! I believe there might be a small bug in the SwiftData Quakes Sample App.^1 The Quakes app requests a JSON feed from USGS.^2 What seems to be breaking is that apparently earthquake entities from USGS can return with null magnitudes. That is throwing errors from the decoder:
struct GeoFeatureCollection: Decodable {
let features: [Feature]
struct Feature: Decodable {
let properties: Properties
let geometry: Geometry
struct Properties: Decodable {
let mag: Double
let place: String
let time: Date
let code: String
}
struct Geometry: Decodable {
let coordinates: [Double]
}
}
}
which is expecting mag to not be nil.
Here is my workaround:
struct GeoFeatureCollection: Decodable {
let features: [Feature]
struct Feature: Decodable {
let properties: Properties
let geometry: Geometry
struct Properties: Decodable {
let mag: Double?
let place: String
let time: Date
let code: String
}
struct Geometry: Decodable {
let coordinates: [Double]
}
}
}
And then:
extension Quake {
/// Creates a new quake instance from a decoded feature.
convenience init(from feature: GeoFeatureCollection.Feature) {
self.init(
code: feature.properties.code,
magnitude: feature.properties.mag ?? 0.0,
time: feature.properties.time,
name: feature.properties.place,
longitude: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[1]
)
}
}
I have this actor
actor ConcurrentDatabase: ModelActor {
nonisolated let modelExecutor: any ModelExecutor
nonisolated let modelContainer: ModelContainer
init(modelContainer: ModelContainer) {
self.modelExecutor = DefaultSerialModelExecutor(modelContext: ModelContext(modelContainer))
self.modelContainer = modelContainer
}
/// Save pending changes in the model context.
private func save() {
if self.modelContext.hasChanges {
do {
try self.modelContext.save()
} catch {
...
}
}
}
}
I am getting a runtime crash on:
try self.modelContext.save()
when trying to insert something into the database and save
Thread 1: Fatal error: Incorrect actor executor assumption; Expected same executor as MainActor.
Can anyone explain why this is happening?
When the following models in SwiftData,
@Model
final class UndoRedoData {
var id: [Int]
init(id: [Int]) {
self.id = id
}
}
I created the following code.
struct ContentView: View {
@ObservedObject var swiftDataViewModel = SwiftDataArrayViewModel.shared
@State private var idArray: [Int] = [1,2,3,4]
@State private var firstviewSwich: Bool = true
@State private var twoviewSwich: Bool = false
@State private var threeviewSwich: Bool = false
var body: some View {
VStack {
if firstviewSwich == true {
Button(action: addItem) {
Text("1.New Item")
}
}
if twoviewSwich == true {
Button {
forArrayData()
} label: {
Text("2.Data Road")
}
}
if threeviewSwich == true {
Button(action: undoItem) {
Text("3.Undo")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = UndoRedoData(id: [1,2,3,4])
swiftDataViewModel.taskContext.insert(newItem)
do {
try swiftDataViewModel.taskContext.save()
} catch {
print(error)
}
swiftDataViewModel.fetchItems()
firstviewSwich.toggle()
twoviewSwich.toggle()
}
}
private func forArrayData() {
twoviewSwich.toggle()
for data in idArray {
swiftDataViewModel.idUndoCreate(id: data, undoManager: swiftDataViewModel.arrayItemUndoManager)
}
threeviewSwich.toggle()
}
private func undoItem() {
swiftDataViewModel.arrayItemUndoManager.undo()
threeviewSwich.toggle()
firstviewSwich.toggle()
}
}
class SwiftDataArrayViewModel: ObservableObject {
static let shared = SwiftDataArrayViewModel()
let modelContainer: ModelContainer
@ObservationIgnored
lazy var taskContext: ModelContext = {
return ModelContext(modelContainer)
}()
@Published var arrayItems = [UndoRedoData]()
@Published var arrayItemUndoManager = UndoManager()
init() {
let schema = Schema([UndoRedoData.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError(error)
}
fetchItems()
}
func fetchItems() {
let fetchDescriptor = FetchDescriptor<UndoRedoData>()
do {
arrayItems = try taskContext.fetch(fetchDescriptor)
} catch {
fatalError(error)
}
}
func idUndoCreate(id: Int, undoManager: UndoManager?) {
undoManager?.registerUndo(withTarget: self) { target in
target.removeID()
}
}
func removeID() {
if let firstUndoRedoData = arrayItems.first {
print("Before Delete:\(firstUndoRedoData.id)")
if !firstUndoRedoData.id.isEmpty {
firstUndoRedoData.id.removeLast()
}
print("After Delete:\(firstUndoRedoData.id)")
}
do {
try taskContext.save()
} catch {
print(error)
}
fetchItems()
}
}
In this code, 1. Create an Item in New Item, 2. Execute Data Road and register the data in the array that is the same value as the data created in New Item in SwiftData one by one in UndoManager by for data in idArray.
This is done because the data in the array and the data created by New Item in SwiftData can be known in advance.
private func forArrayData() {
twoviewSwich.toggle()
for data in idArray {
swiftDataViewModel.idUndoCreate(id: data, undoManager: swiftDataViewModel.arrayItemUndoManager)
}
// class SwiftDataArrayViewModel: ObservableObject
func idUndoCreate(id: Int, undoManager: UndoManager?) {
undoManager?.registerUndo(withTarget: self) { target in
target.removeID()
}
}
After registering in UndoManager, when Undo is executed with 3. Undo, instead of being able to perform Undo where one id is deleted each time, all the data of the id in SwiftData is deleted in a one-time Undo.
I would like to be able to delete one id each time Undo is performed and restore them in sequence, but I can only delete them all once. Does this mean that such registration to UndoManager should not be done with for statements, etc.? Or is there another problem in the code?
I want to make sure that one id is deleted for each Undo executed.
I have been dealing with an error for almost 2 days now that caused my programme to crash on runtime with Thread 10: EXC_BAD_ACCESS (code=1, address=0x0) error , only when using Release mode.
After many trial and errors and narrowing down the root problem I became suspicious to #Predicate and Generics being the root cause of the problem so I made these views to test it out.
import SwiftUI
import SwiftData
struct DataBaseTestGeneric<Model : PersistentModel>: View {
@State private var models: [Model] = []
var body: some View {
viewLoader{
let reporter = Reporter()
let pred = #Predicate<Model>{ model in
return true
}
models = await reporter.fetch(pred)
}content: {
List{
ForEach(models){ model in
Text("\(model.id)")
}
}
}
}
}
and a non-Generic version :
import SwiftData
struct DatabaseTest: View {
@State private var transactions: [Transaction] = []
var body: some View {
viewLoader {
let reporter = Reporter()
let pred = #Predicate<Transaction>{ dec in
return true
}
let decs = await reporter.fetch(pred)
transactions = decs
}content:{
List{
ForEach(transactions){transaction in
Text("\(transaction.id)")
}
}
}
}
}
to give you an insight viewLoader implentations is :
struct viewLoader<Content : View>: View {
var state : LoadingView.States = .loading
let loadingTask : () async -> Void
@State private var isLoading = true
@ViewBuilder var content : Content
var body: some View {
if isLoading{
LoadingView(state)
.task {
await Task.detached(priority:.high){
await loadingTask()
}.value
isLoading = false
}
}else{
content
}
}
}
and I am accessing SwiftData using a background thread ( by implementing @ModelActor . the problem is that the code always crash on runtime when I am trying to fetch the data using reporter.fetch function.
To make things even more weird I have to add the fact that Reporter class also have another function called fetchAll as follow :
func fetchAll<T>(_ model : T.Type) async -> [T] where T : PersistentModel {
let desc = FetchDescriptor<T>()
let result = try? await context.fetch(desc)
guard let result else {
assertionFailure("Error fetching \(model) from context")
return []
}
return result
}
if i replace this function with reporter.fetch (which takes a predicate) the code will not crash either with or without using Generics , which brings me to the point that #Predicate is causing mayhem somehow !
PS: I am using Xcode Version 16.1 (16B40) , on macOs Sequoia Version 15.2 Beta (24C5089c)
Currently, I have an Unversioned Schema, and lightweight changes are automatically applied to the Models.
However, I'm planning to transition to a VersionedSchema, and I have a few questions:
Do I need to write all lightweight migrations in the Migration Plan? or is it automatically applied like the Unversioned Schema?
What happens if I do not create a lightweight migration? and just directly do lightweight changes to the latest VersionedSchema (example: Add a new property)?
If I add items at the root level, this code works, but if I attempt to add a child to any item, it runs an infinite loop where it goes from the AddItemView back to the SubItemView and starts all over. I suspect it has something to do with the Predicate in SubItemView, but the debugger is crap, so I'm just guessing.
Minimal code:
@Model
final class Item {
var id: String
var name: String
var children: [Item] = []
@Relationship(deleteRule: .cascade) var parent: Item?
init(name: String, parent: Item?, id: String = UUID().uuidString) {
self.name = name
self.parent = parent
self.id = id
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(
filter: #Predicate<Item> {
item in
item.parent == nil
}, sort: \Item.name
) public var rootItems: [Item]
var body: some View {
NavigationStack {
List {
ForEach(rootItems) { item in
HStack {
NavigationLink ( destination: SubItemView(parent: item)) {
Text(item.name) }
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
NavigationLink(destination: AddItemView(itemParent: nil)) {
Text("Add To Do")
}
}
}
}
}
}
struct SubItemView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State var parent: Item
@State private var todo: String = ""
@State var selectedDate = Date()
@State var showPicker: Bool = false
@Query var children: [Item]
init(parent: Item) {
self.parent = parent
let parentID = parent.id
_children = Query(filter: #Predicate<Item> {
$0.parent.flatMap(\.id) == parentID && $0.parent.flatMap(\.id) != nil
}, sort: \Item.name )
}
var body: some View {
Form {
LabeledContent {
TextField("Name", text: $parent.name)
} label: {
Text("Name:")
}
}
Text("Parent: \(parent.name)\n")
NavigationStack {
Text("Child count: \(children.count)")
List(children) { child in
HStack {
if(child.children.isEmpty) {
Text(child.name)
NavigationLink ( destination: SubItemView(parent: child)) {
Text("").foregroundColor(.white).background(Color.blue)
}
.opacity(0)
.background(
Text("")
)
} else {
Text(child.name)
NavigationLink(destination: SubItemView(parent: child)) {
Text("")
}
}
}
}
}
.navigationTitle("Sub Items")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
NavigationLink(destination: AddItemView(itemParent: parent)) {
Text("Add To Do")
}
}
ToolbarItem {
Button("Save") {
try? modelContext.save()
dismiss()
}
}
}
}
}
struct AddItemView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var context
@State var itemParent: Item?
@State private var name = ""
@State private var showWarning: Bool = false
@State var child = Item(name: "", parent: nil)
var body: some View {
NavigationStack {
Form {
LabeledContent {
TextField("Item", text: $name)
} label: {
Text("Todo:")
}
}
.navigationTitle("Add New Item")
.toolbar {
Button("Save") {
let tmp = Item(name: name, parent: itemParent)
if(itemParent != nil) {
itemParent!.children.append(tmp)
}
context.insert(tmp)
try? context.save()
dismiss()
}
}
}
}
}
I'm using SwiftData, and I'm using iCloud's CloudKit feature to back up my data.
The problem here is that once you start backing up your data, you can't erase it completely.
Even if the user adds 4 data and erases 4 again, I'm using about 2.5kb.
I don't know how the user using the app will accept this.
I'm trying to provide the user with the ability to erase data at once, what should I do??
I have a Core Data app on the App Store that places its persistent store in the default location within the Application Support directory. At some point, I add widgets to the app while utilizing SwiftData. I create a widget extension and the app group to link both targets. I migrate my persistent store from the default location to the app group container directory. Now, the SwiftData code pointing to the shared container in the widget extension can query data. Here's the sample project for illustration up to this point.
Then, I decided to get rid of the Core Data code entirely. Move everything in the main target to SwiftData. I need to keep my persistence controller object, however, because I can never assume that the entirety of my user base has migrated. I do need to ditch the Core Data model file. Otherwise, my new SwiftData models would conflict with the auto-generated ones from that file. But once I do that, my migration code breaks. I can't set up a persistent container without the model. And I need the container for migration. SwiftData has no native interface to migrate the store to a new location. There is a line in the documentation stating that the framework copies the store to an app group container automatically. But I wasn't able to verify that claim.
I am trying to add a custom JSON DataStore and DataStoreConfiguration for SwiftData. Apple kindly provided some sample code in the WWDC24 session, "Create a custom data store with SwiftData", and (once updated for API changes since WWDC) that works fine.
However, when I try to add a relationship between two classes, it fails. Has anyone successfully made a JSONDataStore with a relationship?
Here's my code; firstly the cleaned up code from the WWDC session:
import SwiftData
final class JSONStoreConfiguration: DataStoreConfiguration {
typealias Store = JSONStore
var name: String
var schema: Schema?
var fileURL: URL
init(name: String, schema: Schema? = nil, fileURL: URL) {
self.name = name
self.schema = schema
self.fileURL = fileURL
}
static func == (lhs: JSONStoreConfiguration, rhs: JSONStoreConfiguration) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
final class JSONStore: DataStore {
typealias Configuration = JSONStoreConfiguration
typealias Snapshot = DefaultSnapshot
var configuration: JSONStoreConfiguration
var name: String
var schema: Schema
var identifier: String
init(_ configuration: JSONStoreConfiguration, migrationPlan: (any SchemaMigrationPlan.Type)?) throws {
self.configuration = configuration
self.name = configuration.name
self.schema = configuration.schema!
self.identifier = configuration.fileURL.lastPathComponent
}
func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult<DefaultSnapshot> {
var remappedIdentifiers = [PersistentIdentifier: PersistentIdentifier]()
var serializedData = try read()
for snapshot in request.inserted {
let permanentIdentifier = try PersistentIdentifier.identifier(for: identifier,
entityName: snapshot.persistentIdentifier.entityName,
primaryKey: UUID())
let permanentSnapshot = snapshot.copy(persistentIdentifier: permanentIdentifier)
serializedData[permanentIdentifier] = permanentSnapshot
remappedIdentifiers[snapshot.persistentIdentifier] = permanentIdentifier
}
for snapshot in request.updated {
serializedData[snapshot.persistentIdentifier] = snapshot
}
for snapshot in request.deleted {
serializedData[snapshot.persistentIdentifier] = nil
}
try write(serializedData)
return DataStoreSaveChangesResult<DefaultSnapshot>(for: self.identifier, remappedIdentifiers: remappedIdentifiers)
}
func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot> where T : PersistentModel {
if request.descriptor.predicate != nil {
throw DataStoreError.preferInMemoryFilter
} else if request.descriptor.sortBy.count > 0 {
throw DataStoreError.preferInMemorySort
}
let objs = try read()
let snapshots = objs.values.map({ $0 })
return DataStoreFetchResult(descriptor: request.descriptor, fetchedSnapshots: snapshots, relatedSnapshots: objs)
}
func read() throws -> [PersistentIdentifier : DefaultSnapshot] {
if FileManager.default.fileExists(atPath: configuration.fileURL.path(percentEncoded: false)) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = try decoder.decode([DefaultSnapshot].self, from: try Data(contentsOf: configuration.fileURL))
var result = [PersistentIdentifier: DefaultSnapshot]()
data.forEach { s in
result[s.persistentIdentifier] = s
}
return result
} else {
return [:]
}
}
func write(_ data: [PersistentIdentifier : DefaultSnapshot]) throws {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(data.values.map({ $0 }))
try jsonData.write(to: configuration.fileURL)
}
}
The data model classes:
import SwiftData
@Model
class Settings {
private(set) var version = 1
@Relationship(deleteRule: .cascade) var hack: Hack? = Hack()
init() {
}
}
@Model
class Hack {
var foo = "Foo"
var bar = 42
init() {
}
}
Container:
lazy var mainContainer: ModelContainer = {
do {
let url = // URL to file
let configuration = JSONStoreConfiguration(name: "Settings", schema: Schema([Settings.self, Hack.self]), fileURL: url)
return try ModelContainer(for: Settings.self, Hack.self, configurations: configuration)
}
catch {
fatalError("Container error: \(error.localizedDescription)")
}
}()
Load function, that saves a new Settings JSON file if there isn't an existing one:
@MainActor func loadSettings() {
let mainContext = mainContainer.mainContext
let descriptor = FetchDescriptor<Settings>()
let settingsArray = try? mainContext.fetch(descriptor)
print("\(settingsArray?.count ?? 0) settings found")
if let settingsArray, let settings = settingsArray.last {
print("Loaded")
} else {
let settings = Settings()
mainContext.insert(settings)
do {
try mainContext.save()
} catch {
print("Error saving settings: \(error)")
}
}
}
The save operation creates a JSON file, which while it isn't a format I would choose, is acceptable, though I notice that the "hack" property (the relationship) doesn't have the correct identifier.
When I run the app again to load the data, I get an error (that there wasn't room to include in this post).
Even if I change Apple's code to not assign a new identifier, so the relationship property and its pointee have the same identifier, it still doesn't load.
Am I doing something obviously wrong, or are relationships not supported in custom data stores?
The code below works, in that it successfully creates an item and attached it to the parent item, but the parent view doesn't show the new child until you navigate away from the parent and then back to it.
Parent:
Child1
Child2
// Add Child 3 and this does not refresh when returning through the dismiss() call, but if I navigate to the grand Parent, and then back to the Parent, Child 3 is there. Any ideas?
NavigationStack {
Form {
LabeledContent {
TextField("Item", text: $name)
} label: {
Text("Item:")
}
}
.navigationTitle("Add New Item")
.toolbar {
Button("Save") {
var tmp = Item(timestamp: Date(), name: name, parent: itemParent)
if(itemParent != nil) {
itemParent!.children.append(tmp)
}
context.insert(tmp)
try? context.save()
dismiss()
}
}
}
I got a preview crash info as this:
*** crashed due to fatalError in PersistentModel.swift at line 559.
This KeyPath does not appear to relate RecordInfo to anything - \RecordInfo.<computed 0x00000001921dfb4e (Array<SomeInfo>)>
If I change preview device to iPhoneSE, It is no crash.
I think the reason is I have some dirty data in iPhone16 Pro preview device.
If I'm right and how to delete it?
Model of RecordInfo like this:
@Model
class RecordInfo {
@Relationship(deleteRule: .cascade) var someInfo:[SomeInfo]
}
@Model
class SomeInfo {
var date: Date
var info: String
}
While reading the developer documentation article Adopting SwiftData for a Core Data App, one particular line piqued my interest.
For apps that evolve from a version that doesn’t have any app group container to a version that has one, SwiftData copies the existing store to the app group container.
Given how troublesome it has been to migrate the Core Data persistent store to an app group container, I decided to try this out myself. I created an Xcode project using the default Core Data template. I then added a few Item objects with timestamps. There, I had what we would consider a regular Core Data app.
I then created a widget extension for this app since this is one of the most common uses for adopting an app group in an Xcode project. After that, I linked the main target with the widget extension using an app group. In the widget extension, I tried to fetch the Item objects. I utilized the SwiftData code in the sample project associated with the article above.
struct Provider: TimelineProvider {
private let modelContainer: ModelContainer
init() {
let appGroupContainerID = "group.com.genebogdanovich.CoreDataSwiftDataAppGroup"
guard let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupContainerID) else {
fatalError("Shared file container could not be created.")
}
let url = appGroupContainer.appendingPathComponent("CoreDataSwiftDataAppGroup.sqlite")
print("\(url)")
do {
modelContainer = try ModelContainer(for: Item.self, configurations: ModelConfiguration(url: url))
} catch {
fatalError("Failed to create the model container: \(error)")
}
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task { @MainActor in
let fetchDescriptor = FetchDescriptor<Item>()
let items: [Item] = try! modelContainer.mainContext.fetch(fetchDescriptor)
print(items)
let entry = SimpleEntry(date: .now, emoji: "😀", count: items.count)
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
The fetch yielded no results. However, as I explored the app group directory in the file system, I found a .sqlite file. That is interesting because SwiftData creates .store files by default. So, I am guessing that SwiftData did copy something. Or the ModelContainer initializer just created another empty SQLite file since the fetch returned zero results.
I would highly appreciate someone elaborating on that quote from the documentation.