1

I am able to create, insert and get an object using SwiftData from outside of SwiftUI. But I if try to change the value of some property of a previously saved and retrieved object, this change does not persist and a do not know why.

The idea is to use AppDelegate in order to later use notifications.

So when the app starts for the first time, appdelegate method "storeValue()" is called. This methods gets the AppSetting object (if the object does not exist, it is first created and succesfully inserted into SwiftData) and tries to change the property of this retrieved object.

But changing the property of this retrieved object is not persisted.

Below is a sample code using an Actor to manage saving, getting etc.

import SwiftUI
import SwiftData

class Persistance {
    static var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            AppSetting.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()
    
}


actor PersistanceActor: ModelActor {
    let modelContainer: ModelContainer
    let modelExecutor: any ModelExecutor
    
    init(modelContainer: ModelContainer) {
        self.modelContainer = modelContainer
        let context = ModelContext(modelContainer)
        modelExecutor = DefaultSerialModelExecutor(modelContext: context)
    }
    
    func save() {
        do {
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }

    func insert<T:PersistentModel>(_ value:T) {
        do {
            modelContext.insert(value)
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }
    
    func delete<T:PersistentModel>(_ value:T) {
        do {
            modelContext.delete(value)
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }
    
    func get<T:PersistentModel>(_ descriptor:FetchDescriptor<T>)->[T]? {
        var toReturn:[T]?
        do {
            toReturn=try modelContext.fetch(descriptor)
        }catch {
            print("error inserting")
        }
        return toReturn
    }
    
}


class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
        Task {
            await storeValue(name: "name from AppDelegate")
        }
        
        return true
    }
    
}

extension AppDelegate {
    
    func storeValue(name: String) async {
        
        print("storing new name: \(name)")
        let apps=await AppSetting.appSetting
        print("current value of stored appsetting: \(apps.name)")
        
        apps.name="value stored from AppDelegate"
        
        let apps2=await AppSetting.appSetting
        print("current value of stored appsetting after an attempt to save a new value: \(apps2.name)")
        
    }
}

@Model
class AppSetting {
    var name:String
    
    init(name: String) {
        self.name = name
    }
    
    init() {
        self.name="init"
    }
    
    private class func getAppSetting() async -> AppSetting? {
        var toReturn:AppSetting?
        
        let actor=PersistanceActor(modelContainer: Persistance.sharedModelContainer)
        toReturn=await actor.get(AppSetting.allFD)?.first
        
        return toReturn
    }
    
    static var appSetting:AppSetting {
        get async {
            if let found=await AppSetting.getAppSetting() {
                return found
            }else{
                let newVal=AppSetting()
                newVal.name="new value"
                
                let actor=PersistanceActor(modelContainer: Persistance.sharedModelContainer)
                await actor.insert(newVal)
                
                return newVal
            }
        }
    }
}

extension AppSetting {
    static var allFD:FetchDescriptor<AppSetting> = {
        let predicate = #Predicate<AppSetting> {value in
            true
        }
        
        let descriptor=FetchDescriptor<AppSetting>(predicate: predicate, sortBy: [SortDescriptor(\AppSetting.name)])
        return descriptor
    }()
}

@main
struct TestingSDApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            LaunchUIView()
        }
        .modelContainer(Persistance.sharedModelContainer)
    }
}

struct LaunchUIView: View {
    
    @Environment(\.modelContext) private var context
    @Query private var appSettings:[AppSetting]
    
    var body: some View {
        Text(appSettings.first?.name ?? "none appsetting name")
    }
}
2
  • This is all very complicated with the actor, app delegate etc. Have you considered using a simpler solution for instance without the actor? Commented Oct 8, 2023 at 19:22
  • i opted for the actor as i need to do some background initial inserts as well that seem to work fine in my sample project with the initial "insert". it's just that the update seems to be a problem for some reason. So for the moment, I am hoping that i will get some idea here on stackoverflow before I start looking for a different solution or I will stick to coredata for the time being. Commented Oct 9, 2023 at 18:45

1 Answer 1

2

I found a working solution:

I added the following function to the class with @Model macro:

func save() {
        if let context=self.modelContext {
            do {
                try context.save()
            }catch(let error) {
                print("error: \(error.localizedDescription)")
            }
        }
    }

after I retrieve the object, change its value, I have to call the "save()" function explicitely to persist the change.

The whole sample below:

import SwiftUI
import SwiftData

class Persistance {
    static var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            AppSetting.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()
    
}


actor PersistanceActor: ModelActor {
    let modelContainer: ModelContainer
    let modelExecutor: any ModelExecutor
    
    init(modelContainer: ModelContainer) {
        self.modelContainer = modelContainer
        let context = ModelContext(modelContainer)
        modelExecutor = DefaultSerialModelExecutor(modelContext: context)
    }
    
    func save() {
        do {
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }

    func insert<T:PersistentModel>(_ value:T) {
        do {
            modelContext.insert(value)
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }
    
    func delete<T:PersistentModel>(_ value:T) {
        do {
            modelContext.delete(value)
            try modelContext.save()
        }catch {
            print("error inserting")
        }
    }
    
    func get<T:PersistentModel>(_ descriptor:FetchDescriptor<T>)->[T]? {
        var toReturn:[T]?
        do {
            toReturn=try modelContext.fetch(descriptor)
        }catch {
            print("error inserting")
        }
        return toReturn
    }
    
}


class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
        //I want to be able to work with SwiftData objects outside of SwiftUI to "set up some application parameters"
        Task {
            await storeValue(name: "update from AppDelegate")
        }
        
        return true
    }
    
}

extension AppDelegate {
    
    
    func storeValue(name: String) async {
        
        print("storing new name: \(name)")                              //param passed correctly
        let apps=await AppSetting.appSetting                            //retrievs the object correctly
        print("current value of stored appsetting: \(apps.name)")       //correctly returns stored value
        
        apps.name=name                                                  //!!!!!! this change is not persisted during subsequent runs of the app
        print("current value: \(apps.name)")                            //although here the property is updated correctly
        apps.save()                                                     //!!! calling the save explicitely persists the change
        
        let apps2=await AppSetting.appSetting                           //retrieving the same stored object again
        print("current value of stored appsetting after an attempt to save a new value: \(apps2.name)")     //still set to original value, newly assigned value not retrieved
        
        
        
    }
}

@Model
class AppSetting {
    var name:String
    
    init(name: String) {
        self.name = name
    }
    
    init() {
        self.name="init"
    }
    
    private class func getAppSetting() async -> AppSetting? {
        var toReturn:AppSetting?
        
        let actor=PersistanceActor(modelContainer: Persistance.sharedModelContainer)
        let result=await actor.get(AppSetting.allFD)
        print("number of results: \(result?.count)")                                    //just checking number of stored objects
        toReturn=result?.first
        
        return toReturn
    }
    
    static var appSetting:AppSetting {
        get async {
            if let found=await AppSetting.getAppSetting() {
                print("object found")
                return found
            }else{
                print("object NOT found, creating a new one")
                let newVal=AppSetting()
                newVal.name="Newly created"
                
                let actor=PersistanceActor(modelContainer: Persistance.sharedModelContainer)
                await actor.insert(newVal)
                
                return newVal
            }
        }
    }
    
    func save() {
        if let context=self.modelContext {
            do {
                try context.save()
            }catch(let error) {
                print("error: \(error.localizedDescription)")
            }
        }
    }
}

extension AppSetting {
    static var allFD:FetchDescriptor<AppSetting> = {
        let predicate = #Predicate<AppSetting> {value in
            true
        }
        
        let descriptor=FetchDescriptor<AppSetting>(predicate: predicate, sortBy: [SortDescriptor(\AppSetting.name)])
        return descriptor
    }()
}

@main
struct TestingSDApp: App {
    
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            LaunchUIView()
        }
        .modelContainer(Persistance.sharedModelContainer)
    }
}

struct LaunchUIView: View {
    
    @Environment(\.modelContext) private var context
    @Query private var appSettings:[AppSetting]
    
    var body: some View {
        Text(appSettings.first?.name ?? "none appsetting name")
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.