Reputation: 519
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")
}
}
Upvotes: 1
Views: 1436
Reputation: 519
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")
}
}
Upvotes: 2