Batch updating CoreData without performance issues - swift

I have a tableView which I populate with cells. The data is being fetched as json from a private api and stored as CoreData. As the cells are being reused I fetched their imageView via a delegate method (from web URL). The problem occurs when I try to save that image data into that managed object. So when the image data is fetched, I fetch the managedObject, add the image data and re-save it. However this has a tremendous impact on the tableview performance (lagging tableview scrolling). I tried fetching and saving in background (privateQueueConcurrencyType) but nothing changes. Any recommendation is greatly appreciated.
func updateCoreDataImage(image:Data, for Url:String) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let priMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
priMOC.parent = context
priMOC.perform {
do{
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "DB")
request.predicate = NSPredicate(format: "url = %#", Url)
request.includesSubentities = false
request.returnsObjectsAsFaults = false
let result = try? context.fetch(request)
let object = result![0] as! NSManagedObject
object.setValue(image, forKey: "image")
try priMOC.save()
print("save")
} catch {
print(error.localizedDescription)
}
}
}

Related

UIImage saved in CoreData is not the same after loading

I have a CoreData entity that has a uiImage property, of type 'binary data' that is supposed to hold a Data object representing a UIImage.
I am saving the image in CoreData before making sure that it's the correct image, but debugging and looking at it with the preview button:
This is how I save the image in CoreData:
let myEntity = NSEntityDescription.insertNewObject(forEntityName: "MyEntity", into: context) as! MyEntity
let uiImage = ... // The image that I was able to inspect with a preview
myEntity.uiImage = uiImage.jpegData(compressionQuality: 1.0)
...
try context.save()
Then I fetch the image later with this code:
let context = PersistentContainer.shared.container.viewContext
let request = NSFetchRequest<SaliencyImage>(entityName: "MyEntity")
request.predicate = NSPredicate(
// This is the key that I use to identity my entity
format: "assetId == %#", assetId
)
let myEntity = try context.fetch(request).first
let uiImage = UIImage(data: myEntity.uiImage!)!
But if I inspect the image, this is what I get in a preview:
Like you can see, only the top left corner is saved/fetched. Even though I made sure that the image was the correct one before saving it in CoreData. Does anybody have an explanation/solution for this?
Your issue is at below line.
let myEntity = try context.fetch(request).first
You are just taking first record of the image. There are more. So you have to for loop for the same.
Code should be as below.
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: 'Your Entity Name')
do {
result = try managedContext.fetch(fetchRequest)
for data in result as! [NSManagedObject] {
storedImageData.append(data.value(forKey: "storedImage") as! Data)
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
let image = UIImage(data: storedImageData)
Reference

Correct method to get and set data using CoreData

I'm having an issue with CoreData, I set some data in the AppDelegate. Then later in a view controller's class I get those values and display them in a list view. When I try to fetch the values the in the view from CoreData I get nothing back.
This is the code I'm using to create the context:
public func createMainContext() -> NSManagedObjectContext {
let modelUrl = Bundle.main.url(forResource: "ItemModel", withExtension: "momd")
guard let model = NSManagedObjectModel.init(contentsOf: modelUrl!) else { fatalError("model not found") }
let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
try! psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = psc
return context
}
This is the code I'm using to get the values:
var fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Topic")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = createMainContext()
let result = try! context.fetch(fetchRequest)
debugPrint("---- \(result)")
Insert a new Entity
let context = self.createMainContext()
let entity = NSEntityDescription.insertNewObject(forEntityName: "Topic", into: context)
entity.setValue(name.stringValue, forKey: "name")
When I set the values initially I perform a fetch & I can see the values, however, when perform a fetch from another view I get nothing back.
Does anyone have a suggestion on what I'm doing wrong or a correct way to do this?
Thanks

How do you load a image via array index into UIImage from coreData

I have searched all over the place and am unable to figure out how would you load one image to a UIIMage using array index from coredata.
I have tried this:
imgDisplay.image = NSData["Image"][index]
but obviously I am getting errors as the above syntax is not correct.
You first need to creat an array of type NSManagedObject. You will fetch the data and append the fetched data to it.
var results = [NSManagedObject]()
Then, add the following method to perform the fetch request, call it from viewDidAppear() or wherever you want.
func loadData() {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "MY_ENTITY_NAME")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "created", ascending: true)]
do {
let results = try context.fetch(fetchRequest)
results = results as! [NSManagedObject]
} catch let err as NSError {
print("Error in fetching reuqest error: \(err)")
}
tableView.reloadData() // if you are adding the fetched data to a table view.
}
When you want to add a specific UIImage at indexPath to the UIImageView, call this:
if (results[3] as! MY_ENTITY_NAME).image != nil {
my_UIImageView.image = UIImage(data: (results[3] as! Task).image! as Data)
}
The .image here is the attribute name. And the 3 is just a random indexPath, you could use indexPath.row

Core Data Image with Custom TableViewCell in Swift

I am new to Swift and iOS Development, and have not found a specific answer to my question.
I am attempting to populate a custom tableview with 3 Labels and 2 Images using Core Data.
I read up on optionals and have even built something similar to what I am trying to do now using the Subtitle option when creating the cell in the storyboard. That works.
My Core Data set up looks like this at the top of the class:
// MARK: - Managed Object Context
var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
// Initialize Fetch Results
var fetchedResultsController = NSFetchedResultsController()
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Custom")
let sortDescriptor = NSSortDescriptor(key: "customFourImages", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
// MARK: - Retrieve Request
func getFetchRequest() -> NSFetchedResultsController {
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc!, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
My viewDidLoad and viewDidAppear look like this:
// MARK: - Fetch
fetchedResultsController = getFetchRequest()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
print("Failed to Perform Initial Fetch")
return
}
self.tableView.reloadData()
Finally, my cellForRowAtIndexPath looks like this:
// MARK: - Dequeue Reusable Cell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CustomTableViewCell
// MARK: - Initiate Fetch Results for Index Path
let custom = fetchedResultsController.objectAtIndexPath(indexPath) as! Custom
let customLabel = custom.customLabels
let customTwoLabel = custom.customTwoLabels
let customThreeLabel = custom.customThreeLabels
let image = UIImage(named: "Custom Food")
let imageData = UIImagePNGRepresentation(image!)
custom.customFourImages = imageData
let images = UIImage(named: "Custom Drink")
let imagesData = UIImagePNGRepresentation(images!)
custom.customFiveImages = imagesData
// MARK: - Set Items in Cell to Returned Fetched Results
cell.customLabel?.text = "\(customLabel)"
cell.customTwoLabel?.text = "\(customTwoLabel)"
cell.customThreeLabel?.text = "\(customThreeLabel)"
cell.imageView?.image = UIImage(data: custom.customFourImages!)
cell.imageView?.image = UIImage(data: custom.customFiveImages!)
return cell
}
I have tried every permutation that I can think of and have scoured the Internet and StackOverflow for a specific answer. I would also prefer not to use Strings. I am using imagePickerController. But even with images in my imageAssets, e.g. "Custom Food", "Custom Drink", image is still found nil.
I can't crack it!
From your comments I can't workout where you are actually saving your managed objects to Core Data. you mention that you use try.moc?.save() but are you saving your individual images to core data? Furthermore, if your images are stored locally in the device, and not fetched from an URL, then you don't really need core data for this.
In the case that you are fetching your images from a URL or whatever, and assuming that you have added an entity MyImage with a imageData property of type NSData, you need to save the object like so:
class func createInManagedObjectContext(moc: NSManagedObjectContext, imgData: NSData) ->MyImage {
let newItem = NSEntityDescription.insertNewObjectForEntityForName("MyImage", inManagedObjectContext: moc) as! MyImage
newItem.imageData = imagData
return newItem
}
Add that function to your MyImage.Swift file (generated by Core Data), so that when you need to save a new image to core data you just do:
func saveNewImage(data: {DATA}){
MyImage.createInManagedObjectContext(self.managedObjectContext, imgData: data);
}
Then, to fetch your objects:
func getSavedImages() -> [MyImage]? {
fetchRequest.sortDescriptors = [sortDescriptor]
do {
return try self.managedObjectContext.executeFetchRequest(fetchRequest) as? [MyImage]
} catch {
print("no records found")
return nil
}
}
If you are already doing this, can you please update your code with more details? pointing out where your error is happening (line number).
If your Image is failing here:
cell.imageView?.image = UIImage(data: custom.customFourImages!)
cell.imageView?.image = UIImage(data: custom.customFiveImages!)
Your fetch result is not getting records from Core Data, which is probably an issue with the above.
if your code is failing here:
let images = UIImage(named: "Custom Drink")
let imagesData = UIImagePNGRepresentation(images!)
You should be unwraping your images object with an if let img = images { ... } and most likely the error would be related to the name of the UIImage that you are getting from memory.
I hope this helps.
Cheers.
EDIT 1:
In reply to your comments:
try.moc.save will not save your images, it will persist any changes pending in the data model.
You need to actually create the managed object in core data, and you need to make sure that your fetch request is getting the relevant data out of Core Data.
Your code is failing because customFourImages is nil. Which in term comes from:
let image = UIImage(named: "Custom Food")
let imageData = UIImagePNGRepresentation(image!)
custom.customFourImages = imageData
....
let images = UIImage(named: "Custom Drink")
let imagesData = UIImagePNGRepresentation(images!)
custom.customFiveImages = imagesData
...
cell.imageView?.image = UIImage(data: custom.customFourImages!)
cell.imageView?.image = UIImage(data: custom.customFiveImages!)
This all seems a bit redundant.
Try this:
let image = UIImage(named: "Custom Food")
let imageData = UIImagePNGRepresentation(image!)
cell.imageView?.image = image
custom.createInManagedObjectContext(moc, imgData: imageData)
....
let images = UIImage(named: "Custom Drink")
let imagesData = UIImagePNGRepresentation(images!)
cell.imageView?.image = images //this is called in the same block? you are overwriting your imageView in the cell, maybe you have two?
custom.createInManagedObjectContext(moc, imgData: imagesData)
This will save the images to Core data and assign the right image to your cell view (I'm unsure if this will address your needs of saving images data to core data). Make sure you understand why you need to use core data and save the appropriate managed object that suits your requirements.

Update all value in one attribute Core Data

I know how to fetch all value from one attribute in Core Data using an array. I just need to press a button and -1 all the value and save it back to the Core Data.
How can I update all the value once in swift?
Thanks.
For swift 3
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Params")
do{
if let fetchResults = try managedContext.fetch(request) as? [NSManagedObject] {
if fetchResults.count != 0 {
for index in 0...fetchResults.count-1 {
let managedObject = fetchResults[index]
managedObject.setValue("-1", forKey: "value")
}
try managedContext.save()
}
}
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
Try following example it might be helpful.Replace you entity name.
var appDel:AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context: NSManagedObjectContext = appDel.managedObjectContext!
var request = NSFetchRequest(entityName: "Params")
var params = NSEntityDescription.insertNewObjectForEntityForName("Params", inManagedObjectContext: context) as! NSManagedObject
if let fetchResults = appDel.managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [NSManagedObject] {
if fetchResults.count != 0{
for (var v = 0 ; v < fetchResults.count ; v++) {
var managedObject = fetchResults[v]
println(fetchResults)
managedObject.setValue("-1", forKey: "value")
context.save(nil)
}
}
}
While the question is quiet old, I am writing it for those who wish to do this task in a more optimized manner. According to the documentation,
Batch updates run faster than processing the Core Data entities yourself in code because they operate in the persistent store itself, at the SQL level.
Hence using NSBatchUpdateRequest is the best way to achieve the result. Here's a swift code that could do the job in the given scenario with using an extension to help simplify the code while also taking into account the fact that changes made in batch update are not reflected in the objects currently in memory.
extension NSManagedObjectContext {
/// Executes the given `NSBatchUpdateRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchUpdateRequest: The `NSBatchUpdateRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchUpdateRequest: NSBatchUpdateRequest) throws {
batchUpdateRequest.resultType = .updatedObjectIDsResultType
let result = try execute(batchUpdateRequest) as? NSBatchUpdateResult
let changes: [AnyHashable: Any] = [NSUpdatedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
}
class MyCoreDataClass {
...
func updateAllParams() {
let request = NSBatchUpdateRequest(entityName: "Params")
request.propertiesToUpdate = ["value" : NSExpression(forConstantValue: -1)]
do {
try managedObjectContext.executeAndMergeChanges(using: request)
} catch {
print(error)
}
}
}
P.S: You need to be aware that validation rules enforced in data model are not considered while executing a batch update. According to the documentation,
When you use batch updates any validation rules that are a part of the data model are not enforced when the batch update is executed. Therefore, ensure that any changes caused by the batch update will continue to pass the validation rules.