I have an Image : NSManagedObject that has two properties: NSString* localPath and NSString* remoteUrl.
When I save the object by calling save:&error on the managed object context, I want it to download the file and when the download fails, I want the save operation to fail too.
Because I have a deeply nested DB structure with multiple references to my Image Entity it would be complicated to find all my images to trigger the download manually.
Is this possible, and if so, how can I cancel the save or delete operation so that it fails?
If it's bad practice to do this in the Model, where should I do this?
It's probably possible to do what you describe but it would be an incredibly bad idea. Downloading images can take a long time. Saving changes in Core Data can already take a while. Since saving will affect every instance that needs an image, you'd be taking a potentially long operation and turning it into a ridiculously, insanely, excessively long operation. Saving wouldn't complete until every image download had finished, and that's an extremely unreasonable dependency.
You'd be much, much, much better off having image downloading and saving changes completely decoupled from each other. Download images separately. If an object's image is unavailable, use a placeholder of some kind.
Instead of having save: start the download process, which by the way saves the entire managed object context not just a single object, I would start the download first. If the download succeeds, you can write the image to disk, update the localPath and save your changes, if it fails then you don't need to do a save at all.
I think that MVCS (Model View Controller Service / Model View Controller Store) might be of interest to you. You could move your logic to the Store layer. It would perform image download asynchronously and create NSManagedObject if download completed successfully.
You can find some information about it at: MVCS - Model View Controller Service and https://softwareengineering.stackexchange.com/questions/184396/mvcs-model-view-controller-store
Related
Background story
I am developing a big iOS app. This app works under specific assumptions. The main of them is that app should work offline with internal storage which is a snapshot of last synchronized state of data saved on server. I decided to use CoreData to handle this storage. Every time app launches I check if WiFi connection is enabled and then try to synchronize storage with server. The synchronization can take about 3 minutes because of size of data.
The synchronization process consists of several stages and in each of them I:
fetch some data from the server (XML)
deserialize it
save it in Core Data
Problem
Synchronization process can be interrupted for several reasons (internet connection, server down, user leaving application, etc). This may cause data to be out-of-sync.
Let's assume that synchronization process has 5 stages and it breaks after third. It results in 3/5 of data being updated in internal storage and the rest being out of sync. I can't allow it because data are strongly connected to each other (business logic).
Goal
I don't know if it is possible but I'm thinking about implementing one solution. On start of synchronization process I would like to create snapshot (some kind of copy) of current state of Core Date and during synchronization process work on it. When synchronization process completes with success then this snapshot could overwrite current CoreData state. When synchronization interrupts then snapshot can be simply aborted. My internal storage will be secured.
Questions
How to create CoreData snapshot?
How to work with CoreData snapshot?
How to overwrite CoreDate state with snapshot?
Thanks in advice for any help. Code examples, if it is possible, will be appreciated.
EDIT 1
The size of data is too big to handle it with multiple CoreData's contexts. During synchronization I am saving current context multiple times to cleanup memory. If I do not do it, the application will crash with memory error.
I think it should be resolved with multiple NSPersistentStoreCoordinators using for example this method: link. Unfortunately, I don't know how to implement this.
You should do exactly what you said. Just create class (lets call it SyncBuffer) with methods "load", "sync" and "save".
The "load" method should read all entities from CoreData and store it in class variables.
The "sync" method should make all the synchronisation using class variables.
Finally the "save" method should save all values from class variables to CoreData - here you can even remove all data from CoreData and save brand new values from SyncBuffer.
A CoreData stack is composed at its core by three components: A context (NSManagedObjectContext) a model (NSManagedObjectModel) and the store coordinator (NSPersistentStoreCoordinator/NSPersistentStore).
What you want is to have two different contexts, that shares the same model but use two different stores. The store itself will be of the same type (i.e. an SQLite db) but use a different source file.
At this page you can see some documentation about the stack:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html#//apple_ref/doc/uid/TP40001075-CH4-SW1
The NSPersistentContainer is a convenience class to initialise the CoreData stack.
Take the example of the initialisation of a NSPersistentContainer from the link: you can have the exact same code to initialise it, twice, but with the only difference that the two NSPersistentContainer use a different .sqlite file: i.e. you can have two properties in your app delegate called managedObjectContextForUI and managedObjectContextForSyncing that loads different .sqlite files. Then in your program you can use the context from one store to get current data to show to the user and you can use the context that use the other store with a different .sqlite if you are doing sync operations. When the sync operations are finally done you can eventually swap the two files and after clearing and reloading the NSPersistentContainer (this might be tricky, because you will want to invalidate and reload all managed objects: you are switching to an entirely new context) you can then show the newly synced data to the user and start syncing again on a new .sqlite file.
The way I understand the problem is that you wish to be able download a large "object graph". It is however so large that it cannot be loaded at once in memory, so you would have to break it in chunks and then merge it locally into to Core data.
If that is the case, I think that's not trivial. I am not sure I can think of direct solution without understanding the object relations and even then it may be really overwhelming.
An overly simplistic solution may be to generate the sqlite file on the backend then download it in chunks; it seems ugly, but it serves to separate the business logic from the sync, i.e. the sqlite file becomes the transport layer. So, I think the essence to the solution would be to find a way to physically represent the data you are syncing in a format that allows for splitting it in chunks and that can afterwards be merged into a sqlite file (if you insist on using Core data).
Please also note that as far as I know Amazon (https://aws.amazon.com/appsync/) and Realm (https://realm.io/blog/introducing-realm-mobile-platform/) provide background sync of you local database, but those are paid services and you would have to be careful not be locked in (should not depend on their libs in your model layer, instead have a translation layer).
My CoreData model has an entity that has an Image attribute.
I have always managed the images for these entities by storing them on the file system and just maintaining a reference to file in the CoreData attribute, i.e. path.
However I have recently shifted to using child managed contexts for handling editing (so that I can easily discard changes if the user should choose to cancel editing).
This is all well and good however I now have an issue of tracking any images changes, specifically if the user changes the image I can no longer just delete the old file (don't want orphaned files building up on the file system) and replace it with the new one, because if the user cancels the changes the old file is now lost.
As I see it I have two options:
I track the image changes in my business layer and only remove any old images once the context is saved, or conversely delete any new images if the context is discarded/cancelled.
I change my image attribute to a Binary Data type (checking 'allows external storage') and let CoreData manage the data... in which case everything should just work.
Looking for any guidance as to which is the better, and importantly - more performant, approach?
Or any other alternate solutions/options...
Thanks!
The first approach would be better. If the save is discardable, it makes sense to do it that way. And unless the images are generally small it's usually better to keep them external.
A good place to delete old images is probably in the managed object's willSave() method. Look at changedValues to find the old image name. If it's different from the current value, delete the old one.
To handle rolling back changes, a couple of possibilities come to mind.
Handle this in whatever code rolls back the change by looking at the about-to-be-rolled-back new instance and removing its image file.
Always put new images in NSTemporaryDirectory() and use willSave() to move them to a permanent location when saving changes. Then you don't need to do anything on a rollback-- you can let iOS handle clearing the temporary directory for you.
I'm building an app that gets posts from a WordPress blogging site and display on a tableView. Each table view cell displays the post image, title and excerpt text. That's a course project, and use of Core Data is required. So my question is, for a better user experience, should I display the image on the cell straight after downloaded and then save to the Store or should I save to the Store, fetch, and then display?
Some considerations:
when the app launches it will check the internet connectivity, if the connection is establish, the store will be cleaned and the latest post will be downloaded.
It will download 5 posts at a time
Scrolling up will perform the download of older posts.
This is a opinion oriented question. The best I can do is to let you know how I design my apps to deal with this case in the past.
My solution :
Use NSFetchedResultsController to read the data from core data and use the NSFetchedResultsController's fetchedObjects array as a data source to your UITableViewController or UICollectionViewController
Implement the delegates of NSFetchedResultsController which will get triggered when data in Core data changes. This way u can efficiently update your CollectionView and TableView and show the data changes on UI ASAP.
Use Background Contexts to modify the data that way your main thread will be free and application remains responsive.
In order to create Background Context, I prefer parent child context architecture rather than traditional multi context architecture. parent child context architecture is easy to understand, keeps the code clean.
Never save image into core data. Instead save the image downloaded to document directory and save the relative link to the downloaded files in CoreData.
Remember I said relative path to deleted file not the absolute file. Because your application folder/sandbox path changes on killing and relaunching the app. Saving the absolute path to the file in core data is a perfect recipe to screw up the logic.
Don't worry about the delay in saving data to core data. The delay might be in fraction of second which you won't even notice. Saving the data in a array and then fetching the data from core data and updating array is a complete no.
Personally, using an array to save data instead of NSFetchedResultsController's fetchedObjects is a complete NO. why ? Simple, Array is not thread safe, because you will background thread to make web service call and parse data you might have multiple threads accessing Array simultaneously. Because Array isn't thread safe, you might easily get into state of data inconsistency and might lead to crash as well.
Finally use libraries like SDWebImage to efficiently load the image. SDWebImage will not only loads the images but also caches them at various levels (In RAM and HardDisk as well) there by loading images fast and swift.
If you are planning to use pagination to fetch data, use scrollView's delegate of scrollViewDidScroll to figure out when user scrolls to bottom of the table/collectionView and make web service call and fetch data in background thread update core data. As soon as you update mainObjectContext NSFetchedResults controller's delegate will be triggered and you should be able to update the UI immediately.
How can I make the way I add images to the core data more efficient?
Its a pretty bad idea to save "Data" or Image here in core data persistance. Also i think you are running this code in the background queue, if not then thats also a bad thing. But then again, saving the image or data into core data persistant store is a very bad idea and should be avoided whenever you can.
As an alternate you can do this -
Save the image in the local directory with a path and a unique
filename.
Save the filename in core data except the path.
Next time when you retrieve the image, get the filename from the Data store.
Append the filename with the whole path untill the folder. Retrieve the image.
This is a much more efficient and better way to store images.
As the title says, I have an image that I'm saving as a PFFile and trying to attach to an object. About 5% of the time, it doesn't save properly, which kind of sucks. I've reduced the size by a factor of 4, which helped a lot, but I can't reduce it much more without sacrificing too much quality.
We have service providers who need to start going to their next job, so I can't just freeze them until the PFFile uploads. The issue is that they are in areas with poor cell signal. I don't know if it's getting cut off, or if it's just taking longer than the 3 seconds beforeSave allows before saving, but I need to find a solution to this.
Right now, I'm creating the imageFile from the image, attaching it to a PFObject, then saving that object in the background. I know that I could add a block that sees if the save was successful, and if not, retry the save until it is, but as I said earlier, I don't want to freeze our providers until they get somewhere with enough service to get the save to go through.
Is there a good way to create a background thread that attempts to save until the save is successful without being noticed by the main thread?
One tricky bit may be that the view controller gets popped off as soon as the provider leaves their "job completed" view after where I try uploading this photo. However, I use an SWRevealViewController, so if I could somehow pass this PFFile to that other view controller before it's been successfully saved to a file, that would be ideal...
Thanks for any tips and advice!