Update Core Data value, SwiftUI - swift

I have a list of object:
#FetchRequest(entity: ObjectDetail.entity(), sortDescriptors: []) var myObject: FetchedResults<ObjectDetail>
My ObjectDetail is an Entity in my .xcdatamodel with a structure like this:
listOfObject: Transformable (List of custom object)
objectId: UUID
objectName: String
objectDescription: String
ListOfObject:
public class ListOfObject: NSObject, NSCoding, ObservableObject {
...
}
I'm trying to update an element inside my listOfObject.
Here are 3 approaches I tried so far:
First approach:
mangedObjectContext.performAndWait {
self.elementFromListOfObject.objectWillChange.send()
elementFromListOfObject.weight = 0
try? mangedObjectContext.save()
}
Second approach:
if let selectedObject = self. myObject.first(where: {$0.objectName == self.currentSelectedObjectName}) {
if let selectedElementFromListOfObject = selectedObject.myList?.myListOfObject.first(where: {$0 == self.currentSelectedObject }) {
selectedElementFromListOfObject.weight = 0
self.excercise.objectWillChange.send()
saveContext()
}
}
Third approach
do {
let request = NSBatchUpdateRequest(entity: ObjectDetail.entity())
request.resultType = .updatedObjectIDsResultType
request.propertiesToUpdate = ["weight": 0]
let result = try mangedObjectContext.execute(request) as? NSBatchUpdateResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSUpdatedObjectsKey: objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes as [AnyHashable : Any], into: [mangedObjectContext])
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
The third one crashed because it says request.propertiesToUpdate is null, the other approaches save the value, but when I restart the app its the default one.
What is the best approach to update a value in Core Data in SwiftUI?

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
}
}

Fetching core data issue

I have an array with a SQLite with about 2000 records and all are listed on a tableview. When one of the records are selected, it goes to the "speciesDetailViewController" were it displays details of that item, including the common name of that species.
Currently, all displayed fields are not editable.
I am now adding the ability for the user to to change one of the fields, their common name and the ability to add notes per species.
The minor change is saved in CoreData as I have no experience with SQLite (hired someone).
I am fairly certain the data is being stored as I have print commands showing so.
My issue seems to be retrieving the data.
Note that as editing this field is optional, not every species will have a record in coreData, only the species that the user updated their common name.
class SpeciesDetailData: NSManagedObject
{
#NSManaged var speciesName: String
#NSManaged var commonName: String
#NSManaged var commonForeignName: String
#NSManaged var speciesNote: String
}
.
var speciesDetailData : SpeciesDetailData?
var speciesDataObject: [NSManagedObject] = []
var speciesNameToSave = String()
#IBAction func ckSaveCommonNameButton(_ sender: Any) {
speciesNameToSave = speciesLabel.text!
self.saveSpeciesName(speciesName: speciesNameToSave)
let commonNameToSave = ckCommonNameTextField.text
self.saveCommonName(commonName: commonNameToSave!)
}
func saveCommonName (commonName: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "SpeciesDetailData", in: managedContext)!
let saveEntity = NSManagedObject(entity: entity, insertInto: managedContext)
saveEntity.setValue(commonName, forKey: "commonName")
saveSpeciesName(speciesName: speciesNameToSave)
do {
try managedContext.save()
speciesDataObject.append(saveEntity)
print(commonName)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
func saveSpeciesName (speciesName: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "SpeciesDetailData", in: managedContext)!
let saveEntity = NSManagedObject(entity: entity, insertInto: managedContext)
saveEntity.setValue(speciesName, forKey: "speciesName")
do {
try managedContext.save()
speciesDataObject.append(saveEntity)
print(speciesName)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
Here is the retrieving function
func retrieveCoreDataSpecies () {
let context = (UIApplication.shared.delegate
as! AppDelegate).persistentContainer.viewContext
let entity = NSEntityDescription.entity(
forEntityName: "SpeciesDetailData", in: context)
let request: NSFetchRequest<SpeciesDetailData> = SpeciesDetailData.fetchRequest()
request.entity = entity
let pred = NSPredicate(format: "speciesName = %#", specieDetail.specie)
request.predicate = pred
do {
let results = try context.fetch(request as!
NSFetchRequest<NSFetchRequestResult>)
if (results.count > 0) {
let match = results[0] as! NSManagedObject
if speciesDetailData?.commonName != nil {
ckCommonNameTextField.text = match.value(forKey: "commonName") as? String
} else {
}
if ckNotesTextView.text == "" || ckNotesTextView.text == nil {
} else {
ckNotesTextView.text = match.value(forKey: "speciesNote") as! String
}
}
} catch let error {
print("Count not fetch \(error), \(error.localizedDescription)")
}
}
When it gets to the
if speciesDetailData?.commonName != nil
it thinks the record is empty and skips over the needed lines.
Any help is appreciated
You are creating separate objects in the two save functions. In each case you are setting only one of the properties, so after you call saveSpeciesName you will have created an object with species name set, and after you call saveCommonName you will have created a different object with the common name set. You need to set both the species name and the common name on the same object.

How to apply the type to a NSFetchRequest instance?

In Swift 2 the following code was working:
let request = NSFetchRequest(entityName: String)
but in Swift 3 it gives error:
Generic parameter "ResultType" could not be inferred
because NSFetchRequest is now a generic type. In their documents they wrote this:
let request: NSFetchRequest<Animal> = Animal.fetchRequest
so if my result class is for example Level how should I request correctly?
Because this not working:
let request: NSFetchRequest<Level> = Level.fetchRequest
let request: NSFetchRequest<NSFetchRequestResult> = Level.fetchRequest()
or
let request: NSFetchRequest<Level> = Level.fetchRequest()
depending which version you want.
You have to specify the generic type because otherwise the method call is ambiguous.
The first version is defined for NSManagedObject, the second version is generated automatically for every object using an extension, e.g:
extension Level {
#nonobjc class func fetchRequest() -> NSFetchRequest<Level> {
return NSFetchRequest<Level>(entityName: "Level");
}
#NSManaged var timeStamp: NSDate?
}
The whole point is to remove the usage of String constants.
I think i got it working by doing this:
let request:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Level")
at least it saves and loads data from DataBase.
But it feels like it is not a proper solution, but it works for now.
The simplest structure I found that works in 3.0 is as follows:
let request = NSFetchRequest<Country>(entityName: "Country")
where the data entity Type is Country.
When trying to create a Core Data BatchDeleteRequest, however, I found that this definition does not work and it seems that you'll need to go with the form:
let request: NSFetchRequest<NSFetchRequestResult> = Country.fetchRequest()
even though the ManagedObject and FetchRequestResult formats are supposed to be equivalent.
Here are some generic CoreData methods that might answer your question:
import Foundation
import Cocoa
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T
{
let entityName = T.description()
let context = app.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let record = T(entity: entity!, insertInto: context)
return record
}
func recordsInTable<T: NSManagedObject>(_ type : T.Type) -> Int
{
let recs = allRecords(T.self)
return recs.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func query<T: NSManagedObject>(_ type : T.Type, search: NSPredicate?, sort: NSSortDescriptor? = nil, multiSort: [NSSortDescriptor]? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let predicate = search
{
request.predicate = predicate
}
if let sortDescriptors = multiSort
{
request.sortDescriptors = sortDescriptors
}
else if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteRecord(_ object: NSManagedObject)
{
let context = app.managedObjectContext
context.delete(object)
}
func deleteRecords<T: NSManagedObject>(_ type : T.Type, search: NSPredicate? = nil)
{
let context = app.managedObjectContext
let results = query(T.self, search: search)
for record in results
{
context.delete(record)
}
}
func saveDatabase()
{
let context = app.managedObjectContext
do
{
try context.save()
}
catch
{
print("Error saving database: \(error)")
}
}
Assuming that there is a NSManagedObject setup for Contact like this:
class Contact: NSManagedObject
{
#NSManaged var contactNo: Int
#NSManaged var contactName: String
}
These methods can be used in the following way:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
let contacts = query(Contact.self, search: NSPredicate(format: "contactName == %#", name))
for contact in contacts
{
print ("Contact name = \(contact.contactName), no = \(contact.contactNo)")
}
deleteRecords(Contact.self, search: NSPredicate(format: "contactName == %#", name))
recs = recordsInTable(Contact.self)
print ("Contacts table has \(recs) records")
saveDatabase()
This is the simplest way to migrate to Swift 3.0, just add <Country>
(tested and worked)
let request = NSFetchRequest<Country>(entityName: "Country")
Swift 3.0 This should work.
let request: NSFetchRequest<NSFetchRequestResult> = NSManagedObject.fetchRequest()
request.entity = entityDescription(context)
request.predicate = predicate
I also had "ResultType" could not be inferred errors. They cleared once I rebuilt the data model setting each entity's Codegen to "Class Definition". I did a brief writeup with step by step instructions here:
Looking for a clear tutorial on the revised NSPersistentContainer in Xcode 8 with Swift 3
By "rebuilt" I mean that I created a new model file with new entries and attributes. A little tedious, but it worked!
What worked best for me so far was:
let request = Level.fetchRequest() as! NSFetchRequest<Level>
I had the same issue and I solved it with the following steps:
Select your xcdatamodeld file and go to the Data Model Inspector
Select your first Entity and go to Section class
Make sure that Codegen "Class Definition" is selected.
Remove all your generated Entity files. You don't need them anymore.
After doing that I had to remove/rewrite all occurences of fetchRequest as XCode seem to somehow mix up with the codegenerated version.
HTH
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func loadItemsCategory() {
let request: NSFetchRequest<Category> = Category.fetchRequest()
do {
categoryArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}

Cannot convert value of type 'String?!' to expected argument type 'Notifications'

I am trying to check the id of a record before I put it into the array, using xcode swift
here is the code. But, i get the following error
Notifications.swift:50:46: Cannot convert value of type 'String?!' to expected argument type 'Notifications'
on this line
*if (readRecordCoreData(result["MessageID"])==false)*
Please can some one help to explain this error
import CoreData
struct Notifications{
var NotifyID = [NSManagedObject]()
let MessageDesc: String
let Messageid: String
init(MessageDesc: String, Messageid:String) {
self.MessageDesc = MessageDesc
self.Messageid = Messageid
// self.MessageDate = MessageDate
}
static func MessagesWithJSON(results: NSArray) -> [Notifications] {
// Create an empty array of Albums to append to from this list
var Notification = [Notifications]()
// Store the results in our table data array
if results.count>0 {
for result in results {
//get fields from json
let Messageid = result["MessageID"] as! String
let MessageDesc = result["MessageDesc"] as? String
let newMessages = Notifications(MessageDesc: MessageDesc!, Messageid:Messageid)
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
}
}
return Notification
}
//check id
func readRecordCoreData(Jsonid: String) -> Bool {
var idStaus = false
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let fetchRequest = NSFetchRequest(entityName: "ItemLog")
//3
do {
let resultsCD = try! managedContext.executeFetchRequest(fetchRequest)
if (resultsCD.count > 0) {
for i in 0 ..< resultsCD.count {
let match = resultsCD[i] as! NSManagedObject
let id = match.valueForKey("notificationID") as! String
if (Jsonid as String! == id)
{
idStaus = true
}
else{
idStaus = false
}
}
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
return idStaus
}
One of your methods is static and the other one is not :
func readRecordCoreData(Jsonid: String) -> Bool
static func MessagesWithJSON(results: NSArray) -> [Notifications]
Depending on what you want to accomplish you could declare both static, none, or replace
//check with id's from core data
if (readRecordCoreData(result["MessageID"])==false)
{
Notification.append(newMessages)
}
By
//check with id's from core data
if (Notifications.readRecordCoreData(Messageid)==false)
{
Notification.append(newMessages)
}
Not sure if the code will work past compilation however as there are many readability issues

Swift: Unable to get array values from singleton class

Hi I retrived value from JSON and stored in NSMutableArray. I have tried this like Singleton. I have used empty swift file for this. Datas successfully retrieved and stored in NSMutableArray which is already declared in mainViewController. Then, if I use that NSMutableArray value in mainViewController, it shows empty array.
My coding is below. Kindly guide me.
Empty Swift File
public class json_file{
var prod_Obj = product_mainVC()
class var shared: json_file
{
struct Static
{
static let instance: json_file = json_file()
}
return Static.instance
}
func dataFromJSON()
{
let url = NSURL(string: "http://........--...4900a20659")!
var data : NSData = NSData(contentsOfURL: url, options: NSDataReadingOptions.DataReadingMapped, error: nil)!
var dict: NSDictionary! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
let dataArray = dict["data"] as [[String:AnyObject]] // The array of dictionaries
for object in dataArray {
let category_name = object["category_name"] as String
prod_Obj.ct_name_arr.addObject(category_name)
let category_child = object["category_child"] as [[String:AnyObject]]
for child in category_child {
let sub_category_name = child["sub_category_name"] as String
prod_Obj.sub_ct_name_arr.addObject(sub_category_name)
}
}
println(prod_Obj.ct_name_arr) //Here value is Getting
println(prod_Obj.sub_ct_name_arr) //Here value is Getting
}
}
viewDidLoad
{
json_file.shared.dataFromJSON()
println(ct_name_arr) //Prints Empty Array [Intially Declared as NSMutableArray]
println(sub_ct_name_arr) //Prints Empty Array [Intially Declared as NSMutableArray]
}
I was trying understand the problem, but I can't see the product_mainVC. Because this I remake your class with little modifications.
class JsonFile
{
private(set) var categoryNames:[String];
private(set) var subCategoryNames:[String];
class let shared:JsonFile = JsonFile();
private init()
{
self.categoryNames = [];
self.subCategoryNames = [];
}
func dataFromJson()
{
let url = NSURL(string: "http://........--...4900a20659")!
if let data : NSData = NSData(contentsOfURL: url, options: NSDataReadingOptions.DataReadingMapped, error: nil)
{
if let dict: NSDictionary! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as? NSDictionary
{
if let dataArray = dict["data"] as? [[String:AnyObject]] // The array of dictionaries
{
for object in dataArray {
let category_id = object["category_id"] as Int
let category_name = object["category_name"] as String
categoryNames.append(category_name);
let category_child = object["category_child"] as [[String:AnyObject]]
for child in category_child {
let sub_category_id = child["sub_category_id"] as Int
let sub_category_name = child["sub_category_name"] as String
subCategoryNames.append(sub_category_name);
}
}
}
}
}
println(categoryNames);
println(subCategoryNames);
}
}
I did
Modify your way to do Singleton to a safe and more simple mode, create the arrays categoryNames and subCategoryNames internal in class because this is better to manipulate, and protect your fetch data to safe from possibles crash.
Implementation
JsonFile.shared.dataFromJson();
println("count categoryNames");
println(JsonFile.shared.categoryNames.count);
println("count subCategoryNames");
println(JsonFile.shared.subCategoryNames.count);
You need think about
This code is sync, and because this if you have a big data or request slow, the main thread from your application will freeze waiting return and it is bad for your user. Think if is necessary be sync.
let category_id = object["category_id"] as Int is never used. Why do you stay with this in code?