Swift Realm change only one object value - swift

class User: Object {
#objc dynamic var id = ""
#objc dynamic var dateFirstStart:TimeInterval = 0
//dates
#objc dynamic var dateLastStart:TimeInterval = 0
#objc dynamic var dateLastAppClose:TimeInterval = 0
#objc dynamic var dateLastDataUpdateCheck:TimeInterval = 0
#objc dynamic var dateLastFilesUpdateCheck:TimeInterval = 0
override class func primaryKey() -> String? {
return "id"
}
}
Do I really have to create a function for each value to change? Like this:
func updateUserDateFirstStart(date:Date){
do {
let realm = try Realm()
try realm.write {
let user = getUser()
user. dateLastStart = Date().timeIntervalSince1970
}
} catch let error as NSError {
print("ERROR \(error.localizedDescription)")
}
}
What I want is something like
let user = getUser()
user.dateLastStart = Date().timeIntervalSince1970
dataManager.updateUser(user)
And in my DataManager:
func updateUser(user:User){
do {
let realm = try Realm()
try realm.write {
realm.add(user, update: true)
}
} catch let error as NSError {
print("ERROR \(error.localizedDescription)")
}
}
But if I do it as you can see in my wishtohave solution I always get an Attempting to modify object outside of a write transaction error.
I tried to create a complete new Object and use the id from the object I want to change. This works but would need even more lines of code.

You can use KVO to update one value in realm object
how to call
let user = getUser()
self.update(ofType:user, value: Date().timeIntervalSince1970 as AnyObject, key: "dateLastStart")
Helper func
func update(ofType:Object,value:AnyObject,key:String)->Bool{
do {
let realm = try Realm()
try realm.write {
ofType.setValue(value, forKeyPath: key)
}
return true
}catch let error as NSError {
fatalError(error.localizedDescription)
}
return false
}

Related

Any clean way for batch inserting coredata objects with relationships?

I have been observing high CPU times in background threads while inserting the coredata objects, and from analyser i could find that it's coming majorly because of some relationships i was creating one by one, and those could be in thousands.
So i thought if i could create them with batch insert. I can do that easily for objects using without relationships using NSBatchInsertRequest, but with relationships, I can't seem to find any clean way.Without relationships, i can easily create dictionaries and insert using the above request.
​
With relationships, i also tried using the object handler method of NSBatchInsertRequest, but even that is giving me an exception
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'run' between objects in different contexts
This is how i am trying to make sure that the trackpoint getting added is using the run object from the same context as the one in which its being created
func addTrackPoints(run: RunModel, objectId: NSManagedObjectID) async throws {
let locations:[CLLocation] = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data = locations[index]
guard let run = try? StorageService.shared.getBackgroundContext().object(with: objectId) as? Run else {
fatalError("failed to get run object")
}
trackPoint.run = run
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext())
}
I also tried it without accessing the object from same context but instead tried directly using the Run object that i had created. It didn't crash, but it still didn't create the relationship.Also it forced me to remove the concurrencydebug run argument.
func addTrackPoints(run: RunModel, object: Run) async throws {
let locations = run.getLocations()
let count = run.getLocations().count
var index = 0
let batchInsert = NSBatchInsertRequest(entity: TrackPoint.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < count else { return true }
if let trackPoint = managedObject as? TrackPoint {
let data:CLLocation = locations[index]
trackPoint.run = object
}
index += 1
return false
}
try await StorageService.shared.batchInsert(entity: TrackPoint.entity(), batchInsertRequest: batchInsert, context: StorageService.shared.getBackgroundContext()) }
StorageService
public func batchInsert(entity: NSEntityDescription, batchInsertRequest: NSBatchInsertRequest, context: NSManagedObjectContext? = nil) async throws {
var taskContext:NSManagedObjectContext? = context
if(taskContext == nil) {
taskContext = StorageService.shared.newTaskContext()
// Add name and author to identify source of persistent history changes.
taskContext?.name = "importContext"
taskContext?.transactionAuthor = "import\(entity.name ?? "entity")"
}
/// - Tag: performAndWait
try await taskContext?.perform {
// Execute the batch insert.
do{
let fetchResult = try taskContext?.execute(batchInsertRequest)
if let batchInsertResult = fetchResult as? NSBatchInsertResult,
let success = batchInsertResult.result as? Bool, success {
return
}
} catch {
self.logger.error("Failed to execute batch insert request. \(error)")
}
throw SSError.batchInsertError
}
logger.info("Successfully inserted data for \(entity.name ?? "entity")")
}
Any help would be deeply appreciated :-)
How app works, I send request to server, get some results and want data to be saved in core data for further usage to send request to server only when needed. so next time I will query data from database.
Here is sample:
I always save data in background context, which is configured like this:
func getBgContext() -> NSManagedObjectContext {
let bgContext = self.persistenceController.container.newBackgroundContext()
bgContext.automaticallyMergesChangesFromParent = true
bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return bgContext
}
Next I construct my data models like this so decoder will handle entity creation and data parsing + insertion in dbContext:
public class SomeDataModel: NSManagedObject, Codable {
var entityName: String = "SomeDataModel"
enum CodingKeys: String, CodingKey {
case id = "id"
case someData = "someData"
}
public required convenience init(from decoder: Decoder) throws {
guard
let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "SomeDataModel", in: context)
else {
throw DecoderConfigurationError.missingManagedObjectContext
}
self.init(entity: entity, insertInto: context)
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int32.self, forKey: .id)
someData = try values.decodeIfPresent(String.self, forKey: .someData)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(someData, forKey: .someData)
}
func toExternalModel() -> SomeExternalUsableModel {
return SomeExternalUsableModel(id: id, someData: someData)
}
}
extension SomeDataModel {
#nonobjc public class func fetchRequest() -> NSFetchRequest<SomeDataModel> {
return NSFetchRequest<SomeDataModel>(entityName: "SomeDataModel")
}
#NSManaged public var someData: String?
#NSManaged public var id: Int32
}
extension SomeDataModel: Identifiable {
}
to pass dbcontext to decoder I do next:
extension CodingUserInfoKey {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
}
dbContext - create background context somewhere in API helper class, and use this context for all the parsings below.
and next I do parsing with decoder when respond from server comes:
let model = try self.dbContext.performAndWait {
let jsonDecoder = JSONDecoder()
let jsonEncoder = JSONEncoder()
// pass context to decoder/encoder
jsonDecoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
jsonEncoder.userInfo[CodingUserInfoKey.managedObjectContext] = self.dbContext
// parse model, used generic for reuse for other models
let model = try jsonDecoder.decode(T.self, from: result.data)
// after this line - all the data is parsed from response from server, and saved to dbContext, and contained in model as well
if self.dbContext.hasChanges {
do {
try self.dbContext.save()
self.dbContext.refreshAllObjects() // refresh context objects to ELIMINATE all outdated db objects in memory (specially when you will have relations, they could remain in memory until updated)
} catch let error {
// process error
}
}
return model
}
// do with saved and ready to use data in models whatever needed:
return model
and extensions used for performAndWait
extension NSManagedObjectContext {
func performAndWait<T>(_ block: () throws -> T) throws -> T? {
var result: Result<T, Error>?
performAndWait {
result = Result { try block() }
}
return try result?.get()
}
func performAndWait<T>(_ block: () -> T) -> T? {
var result: T?
performAndWait {
result = block()
}
return result
}
}

SwiftUI: keep data even after closing app

i have been trying to make that when a user adds a page to favorites or removes the page it saves it, so when a user closes the app it remembers it. I can't figure out how i can save the mushrooms table. I want to save it locally and is it done by using Prospects ?
class Favorites: ObservableObject {
public var mushrooms: Set<String>
public let saveKey = "Favorites"
init() {
mushrooms = []
}
func contains(_ mushroom: Mushroom) -> Bool {
mushrooms.contains(mushroom.id)
}
func add (_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.insert(mushroom.id)
save()
}
func remove(_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.remove(mushroom.id)
save()
}
func save() {
}
}
I was able to figure it out. Here is the code i did if someone else is struggling with this.
I added this to the save function
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(mushrooms) {
defaults.set(encoded, forKey: "Favorites")
}
}
And to the init() :
let decoder = JSONDecoder()
if let data = defaults.data(forKey: "Favorites") {
let mushroomData = try? decoder.decode(Set<String>.self, from: data)
self.mushrooms = mushroomData ?? []
} else {
self.mushrooms = []
}
EDIT:
and of course add the defaults
let defaults = UserDefaults.standard

Realm objects returns nil after adding

I created an Person object when the User logs in:
let creds = SyncCredentials.jwt(accessToken)
SyncUser.logIn(with: creds, server: Constants.syncAuthURL, onCompletion: { [weak self](user, err) in
if let user = user {
self?.setDefaultRealmConfiguration(with: user)
let realm = try! Realm()
let identity = (user.identity)!
let person = Person()
person.id = identity
try! realm.write {
realm.add(person, update: true)
}
self?.performSegue(withIdentifier: "showProfile", sender: self)
}
})
The Person successfully created on the cloud server.
In the next viewcontroller i like to fetch the object based on the id:
let realm = try! Realm()
guard let uuid = SyncUser.current?.identity! else { return }
let person = realm.objects(Person.self).filter("id = %#", uuid).first
try! realm.write {
person?.name = "test"
}
The person is always nil I also tried the query the object with the primary key, but with no success.
The Person class looks like:
class Person : Object {
#objc dynamic var id = ""
#objc dynamic var created: Date = Date()
#objc dynamic var name = ""
#objc dynamic var email = ""
#objc dynamic var avatar : Data?
override static func primaryKey() -> String? {
return "id"
}
}
UPDATE
I created a new app with just one Viewcontroller and the Person class:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let creds = SyncCredentials.usernamePassword(username: "admin", password: "test")
SyncUser.logIn(with: creds, server: Constants.AUTH_URL, onCompletion: { (user, err) in
if let user = user {
var config = user.configuration(realmURL: Constants.REALM_URL)
config.objectTypes = [Person.self]
Realm.asyncOpen(configuration: config, callback: { (realm, error) in
guard let realm = realm else {return}
let objects = realm.objects(Person.self)
print(objects) // always empty why???
try! realm.write {
let p = Person()
p.id = "test"
realm.add(p)
}
print(objects) // one object
})
}
})
}
}
as with my other problem the person is successfully added to the cloud. but when I restart the app the objects are empty on the first query. Maybe I miss understanding something with the synched realms?
let results = realm.objects(Person.self)
let subscription = results.subscribe()
print(results)
resolve my problem

Save state in Userdefaults

I have a class that saves the state of something, in my case some variable of the ViewController, but sometimes it loads wrong or old data, but I can't figure out why.
Maybe somebody can have a look of my code and see if it makes sense.
class TopFlopState: Codable, PersistenceState {
var group: Groups = .large {
didSet {
save()
}
}
var base: Bases = .usd {
didSet {
save()
}
}
var valueOne: StatIntervalBaseModel = StatIntervalBaseModel(stat: "ppc", interval: "24h", base: "usd") {
didSet {
save()
}
}
init(){
let savedValues = load()
if savedValues != nil {
self.group = savedValues!.group
self.base = savedValues!.base
self.valueOne = savedValues!.valueOne
}
}
}
This is the PersistenceState protocol:
/**
Saves and Loads the class, enum etc. with UserDefaults.
Has to conform to Codable.
Uses as Key, the name of the class, enum etc.
*/
protocol PersistenceState {
}
extension PersistenceState where Self: Codable {
private var keyUserDefaults: String {
return String(describing: self)
}
func save() {
saveUserDefaults(withKey: keyUserDefaults, myType: self)
}
func load() -> Self? {
return loadUserDefaults(withKey: keyUserDefaults)
}
private func saveUserDefaults<T: Codable>(withKey key: String, myType: T){
do {
let data = try PropertyListEncoder().encode(myType)
UserDefaults.standard.set(data, forKey: key)
print("Saved for Key:", key)
} catch {
print("Save Failed")
}
}
private func loadUserDefaults<T: Codable>(withKey key: String) -> T? {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return nil }
do {
let decoded = try PropertyListDecoder().decode(T.self, from: data)
return decoded
} catch {
print("Decoding failed for key", key)
return nil
}
}
}
If a value gets set to the value it should automatically save, but like I set sometimes it saves the right values but loads the wrong ones...
In my opinion, It return the cache. Because in Apple official documentation, it state
UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value
Maybe you can change the flow, when to save the data. In your code show that you call save() 3 times in init().

How To Save Only One Instance Of Class In Realm

So instead of using user defualts I want to persist some settings using Realm.
I've created a class for the settings
import Foundation
import RealmSwift
class NutritionSettings: Object {
#objc dynamic var calories: Int = 0
#objc dynamic var proteins: Int = 0
#objc dynamic var carbohydrates: Int = 0
#objc dynamic var fats: Int = 0
}
But in my view controller I don't know how to save just one instance of it
I've tried
let realm = try! Realm()
let settings = NutritionSettings()
do {
try realm.write{
settings.calories = cals!
settings.carbohydrates = carbs!
settings.fats = fats!
settings.proteins = proteins!
}
} catch {
print("error saving settings")
}
Since I know doing realm.add would just add another NutritionSettings object which is not what I want. I was unable to clarify anything using the documentation. Any help would be appreciated thanks.
I faced a similar issue in my project when I tried to save a user session object. If you want to save a unique object, override the primaryKey() class method and set the unique key for it.
#objcMembers class NutritionSettings: Object {
static let uniqueKey: String = "NutritionSettings"
dynamic var uniqueKey: String = NutritionSettings.uniqueKey
dynamic var calories: Int = 0
override class func primaryKey() -> String? {
return "uniqueKey"
}
}
Then to receive the object just use the unique key.
// Saving
let settings = NutritionSettings()
settings.calories = 100
do {
let realm = try Realm()
try realm.write {
realm.add(settings, update: .modified)
}
} catch {
// Error handling
}
// Reading
var settings: NutritionSettings?
do {
let realm = try Realm()
let key = NutritionSettings.uniqueKey
settings = realm.object(ofType: NutritionSettings.self, forPrimaryKey: key)
} catch {
// Error handling
}
if let settings = settings {
// Do stuff
}
Hope it will help somebody.
If you look at the example realm provides https://realm.io/docs/swift/latest you can see that in order to only save one object you still have to do an add. Once you have added the object to the database you can fetch that object and do a write that modifies the internal properties
let realm = try! Realm()
let settings = NutritionSettings()
settings.id = 1
do {
try realm.write{
realm.add(settings)
}
} catch {
print("error saving settings")
}
Next you can fetch and modify that single instance that you saved
let realm = try! Realm()
let settings = realm.objects(NutritionSettings.self).filter("id == 1").first
do {
try realm.write{
settings.calories = cals!
settings.carbohydrates = carbs!
settings.fats = fats!
settings.proteins = proteins!
}
} catch {
print("error saving settings")
}