iCloud + CoreData - how to avoid pre-filled data duplication? - ios

I have a problem with an iCloud shoebox application and hope some-one can help me (I've spent many hours fighting it in vain).
The App: - A simple library style application - containing set of categories (Cat1 .. CatN) each containing items (Item1...ItemM). I used Apple's iPhoneCoreDataRecipes to set up iCloud CoreData stack.
Everything works almost perfect with iCloud except - there should be a number of pre-filled empty categories the user can start using once he has opened the app for the first time (he can also be offline at that time). And here's the devil.
Here's what I do - Once my persistentStoreCoordinator is setup I send notification
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]
postNotificationName: #"RefetchAllDatabaseData"
object: self
userInfo: nil];
});
which is received by my MasterViewController. When the notification is received MasterViewController checks the number of categories in the storage. If the number of available categories equals 0 - the pre-filled categories are inserted.
FYI - I use NSMergeByPropertyObjectTrumpMergePolicy for my ManagedObjectContext
The problem: This works well for the 1st device. But for the 2nd device the default categories from iCloud are often received later than persistentStoreCoordinator has been setup (and default categories inserted by 2nd device). In the end I have 2 sets of categories with the same names on both devices.
Any ideas how this can be solved?
Tried solutions: I tried 2 strategies to solve this. Both start in the same way. After I call
[moc mergeChangesFromContextDidSaveNotification: note];
I call
[self materializeKeysWithUserInfo: note.userInfo forContext: moc];
many thanks to Jose Ines Cantu Arrambide from https://devforums.apple.com/thread/126670?start=400&tstart=0 for his reference code - In essence
materializeKeysWithUserInfo:forContext:
get managedObjectIds from note.userInfo and retrieves corresponding objects from ManagedObjectContext putting them into a dictionary.
Strategy 1:
All my categories have creation time-stamps.
On insert from iCloud, get pairs of categories with same name if any
Select older duplicate categories
move their items to newer duplicate categories
delete older duplicate categories
These strategy effectively removes duplicates on both devices even before they appear in the UI BUT
1) the items from 1st device are getting lost on the 2nd device - when they come to the 2nd device their parent category is absent and their category field equal nil so I don't know where to put them.
2) in some short time the items that got lost on the 2nd device are also getting lost on the first due to conflicts.
3) some items originating from the 2nd device are also lost due to conflicts.
I tried to prefer older categories against newer but it didn't give any effect
Strategy 2:
All my categories have creation time-stamps.
All categories have obsolete boolean field set to NO on creation
On insert from iCloud, get pairs of categories with same name if any
Select older duplicate categories
move their items to newer duplicate categories
mark older categories with obsolete = YES
These strategy almost always removes duplicates on both devices even before they appear in the UI BUT
the majority (or all) of the items from both devices are getting lost due to a bunch of conflicts on categories and items.
Some concluding thoughts:
It looks like these strategy doesn't work as we start simultaneously changing content ob both devices whereas iCloud is not suitable for such pattern.
In my tests I had both devices running simultaneously. I cannot neglect a case when a happy user who has just bought his 2nd iDevice installs my app on the 2nd device (with tre 1st device running the app) and get lost all his items in the matter of minutes.
Any ideas how this situation can be solved? Do you think iCloud + CoreData is ready for production?
Strategy 3
I've tried to put a pre-filled database (copying it from bundle) to the appropriate path. It worked out partly - I have no more pre-filled categories duplication BUT the items added to the pre-filled categories do not synchronize across the devices.
iCloud is not aware of the data that exists in the database prior to iCloud setup - my 2nd device receives items, inserted on the 1st device in pre-filled categories, with category = nil.
Items in additionally categories (as well as categories themselves) inserted into the storage after iCloud setup do synchronize properly.

Strategy 1 with some modifications appeared to be a working solutions (with some flaws though).
Legend:
1st device - started online without any content in the iCloud
2nd device - started later than first and OFFLINE. Then it gets online after some items added
So here's the updated strategy:
All my categories have creation time-stamps
The categories cannot be renamed (only added or deleted - this is crucial)
All my items have a string categoryName field which gets its value upon item creation and updated whenever item is moved to a different category - this redundant information helps to achieve success;
On insertion of new Categories:
On insert from iCloud, I get pairs of categories with same name if any
Select newer duplicate categories (they will most probably have less items than old ones so we will have less dance in iCloud)
Move their items if any to older duplicate categories
Delete newer duplicate categories
On insertion of new Items - if the item belongs to deleted category:
CoreData tries to merge it and fails as there's no parent category any more (lots of errors in console). It promisses to insert it later.
After some short time it does merge and insert the item into storage but with NIL category
Here we pick our item up, find out it's parent category from categoryName and put it to the correct category
VOILA! - no duplicates & everybody happy
A couple of notes:
I get a dance of items belonging to the 2nd device (those that will come with nil category to the 1st device) on both devices. After a couple of minutes everything is stabilized
No items is lost though
The dance happens only on the first iCloud sync of the 2nd (or any other subsequent device)
If the 2nd device is started online for the first time the chance that duplicate categories case appears is about 25% only - tested on 3G connection - so dance should not affect the majority of users

Related

How to optimize performance of Results change listeners in Realm (Swift) with a deep hierarchy?

We're using Realm (Swift binding currently in version 3.12.0) from the earliest days in our project. In some early versions before 1.0 Realm provided change listeners for Results without actually giving changeSets.
We used this a lot in order to find out if a specific Results list changed.
Later the guys at Realm exchanged this API with changeSet providing methods. We had to switch and are now mistreating this API just in order to find out if anything in a specific List changed (inserts, deletions, modifications).
Together with RxSwift we wrote our own implementation of Results change listening which looks like this:
public var observable: Observable<Base> {
return Observable.create { observer in
let token = self.base.observe { changes in
if case .update = changes {
observer.onNext(self.base)
}
}
observer.onNext(self.base)
return Disposables.create(with: {
observer.onCompleted()
token.invalidate()
})
}
}
When we now want to have consecutive updates on a list we subscribe like so:
someRealm.objects(SomeObject.self).filter(<some filter>).rx.observable
.subscribe(<subscription code that gets called on every update>)
//dispose code missing
We wrote the extension on RealmCollection so that we can subscribe to List type as well.
The concept is equal to RxRealm's approach.
So now in our App we have a lot of filtered lists/results that we are subscribing to.
When data gets more and more we notice significant performance losses when it comes to seeing a change visually after writing something into the DB.
For example:
Let's say we have a Car Realm Object class with some properties and some 1-to-n and some 1-to-1 relationships. One of the properties is a Bool, namely isDriving.
Now we have a lot of cars stored in the DB and bunch of change listeners with different filters listing to changes of the cars collection (collection observers listening for changeSets in order to find out if the list was changed).
If I take one car of some list and set the property of isDriving from false to true (important: we do writes in the background) ideally the change listener fires fast and I have the nearly immediate correct response to my write on the main thread.
Added with edit on 2019-06-19:
Let's make the scenario still a little more real:
Let's change something down the hierarchy, let's say the tires manufacturer's name. Let's say a Car has a List<Tire>, a Tire has a Manufacturer and a Manufacturer has aname.
Now we're still listing toResultscollection changes with some more or less complex filters applied.
Then we're changing the name of aManufacturer` which is connected to one of the tires which are connected to one of the cars which is in that filtered list.
Can this still be fast?
Obviously when the length of results/lists where change listeners are attached to gets longer Realm's internal change listener takes longer to calculate the differences and fires later.
So after a write we see the changes - in worst case - much later.
In our case this is not acceptable. So we are thinking through different scenarios.
One scenario would be to not use .observe on lists/results anymore and switch to Realm.observe which fires every time anything did change in the realm, which is not ideal, but it is fast because the change calculation process is skipped.
My question is: What can I do to solve this whole dilemma and make our app fast again?
The crucial thing is the threading stuff. We're always writing in the background due to our design. So the writes itself should be very fast, but then that stuff needs to synchronize to the other threads where Realms are open.
In my understanding that happens after the change detection for all Results has run through, is that right?
So when I read on another thread, the data is only fresh after the thread sync, which happens after all notifications were sent out. But I am not sure currently if the sync happens before, that would be more awesome, did not test it by now.

Showing random data from core-data using Swift 3

I need some help getting random data from core data using Swift 3, Xcode 8.3.1. I currently have an app that creates a list in tableview using data that is entered by the user.. (user enters a name and takes a picture of that person) The entity "Friend" holds the attributes "name", "image".
The first version of this app was just a name and I would use arc4random to randomly update a label with a name on a modally presented VC on a button click. The names were simply stored in an Array.
This version is including an image so I decided to try my hand at core-data (never used it before) and now I'm stuck at my random select button. Currently the app will store the data fine and then retrieve it and display everyone alphabetically along with their image in a tableview. As a new person is submitted the info gets stored and the tableview updates.
I need to show a randomly selected name and its image, but I don't know how to do this and research has failed me on getting it done.
If there is a better way of storing an image & name instead of core-data I'm open to changing as well. The app stores anywhere from 20-80 different names. It will never be used to store much more than that.
You can fetch your items from the context, which will give you an array of objects. Now you just use your favorite random function to get a random index for this array. And then use an object at that index.

Parse.com - Find deleted objects

I have an iOS app that synchronises to Parse.com.
It can find anything that was added to Parse and add it to Core Data using PFQuery. It can also check for any data that has been updated and update accordingly.
However, I'm not sure how to find objects that have been deleted on Parse.com.
Does anyone know of a query that will list the ObjectIDs that have been deleted and the date of their deletion? I can then remove them from the Core Data on the app.
I needed this function, too, but figured that marking rows as deleted will bloat the data and add a condition to every query. So I created a Deletion class. It records only the class name and ID of any deleted row, so it stays pretty small:
function recordDeletion(klass, identifier) {
var Deletion = Parse.Object.extend("Deletion");
var deletion = new Deletion();
deletion.set("klass", klass);
deletion.set("identifier", identifier);
return deletion.save();
}
// for every class that you want deletions recorded, add one of these...
Parse.Cloud.beforeDelete("MyClass", function(request, response) {
recordDeletion("MyClass", request.object.id).then(function() {response.success();});
});
My iOS clients record the date when they last fetched data, then get everything newly created/updated from MyClass (+ others) and Deletion. With that, the can delete the Deletions locally.
Over a longer period, the clients remove all of the locally cached data and get a fresh copy of everything (except Deletions). This allows me to have a scheduled job on the server that will empty the Deletion table (on a cycle that's much longer than the client's cycle).
There is no provided API for this.
As per the comment from #Fogmeister you can tag objects as deleted and update like that. Alternatively you can maintain a specific list of deleted ids (potentially using Parse.Cloud.beforeDelete) and then make a specific request to get only the deletions.
In either case you will need to explicitly manage the scheme you choose and also decide how and when to clean up the deleted objects / deletion records.

iOS: Design pattern for populating asynchronously fetched data

I am developing an app that fetches data from the web and displays it to the user. Assume that the data is reviews of a restaurant and one review is displayed on one view. The user can swipe left or right to go to the prev/next review. The data is fetched asynchronously (one thread for each review).
Here is the problem statement - Assume that 5 reviews have been fetched and the user is looking at the 3rd one currently. Now, the 6th review is fetched and I want to display it as the 4th review to the user (because the publish date of the 6th review is more recent than the 5th review). How should my model class inform the view controller?
I have considered some options -
Provide an array to the view controller and then send NSNotifications about new items to be inserted in-between the array at a specific index
Use an NSFetchedResultsController (this is a bit tricky because I am not using it with a table view controller)
View controller always asks for the next review to be displayed (from the model) and does not have a array of reviews with it
Are there any established design patterns that are employed in such a scenario? Other suggestions apart from the 3 above are welcome!
Just use an NSFetchedResultsController. When using NSIndexPaths just ignore the section. It's basically a glorified NSArray with free notifications.
Here's how I think I'd do it:
Make sure that the NSFetchRequest for your NSFetchedResultsController is sorted by publish date.
Handle NSFetchedResultsControllerDelegate methods.
When the NSFetchedResultsController updates, save the current object, reload the collection view, and then scroll to the saved object without any animation. This will appear to the user as if nothing happened to the current page.
While there is no perfect design pattern for every programming problem, the closest I can think of that relates to your problem is a combination of the Command and Observer patterns.
https://en.wikipedia.org/wiki/Command_pattern
The observer pattern is used in the NSNotification center.
While it's unclear as to why you'd want to skip a review, you could have two arrays to store them when fetched. The first holds all reviews that you have fetched. The second holds all reviews that are displayed.
Then you can get the last review in the fetched array, as if it were a stack. This way you always have the last one loaded displayed to the user.
I am confused why the order of display is different than the true order, ie why the 6th review comes before the 5th, but you asked about patterns to help.
Apart from MVC and observer, which are in the other answers and comments, I'd suggest using lazy loading with a virtual proxy. When reviews have been fetched, you can just display their proxy (eg with a "loading..." Message until they're fully in memory).
See more here: http://en.wikipedia.org/wiki/Proxy_pattern
I would recommend using the observing pattern to inform your controller than new data as been fetched. When receiving the signal, your view controller could update its array of "restaurant review" (either by adding the old one and reordering it according to some sort descriptors of your flavor or by querying the DAO directly).
Let's say you are fetching your data from internet and populating a CoreData entity with the results. Once you got your downloaded data you can populate your core data "Review" entity.
In order to "listen" at the change happening in core data, your controller should, in the viewDidLoad body, register itself as an observer for the NSManagedObjectContextDidSaveNotification.
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(updateInfo:) name:NSManagedObjectContextDidSaveNotification object:nil];
Then in your updateInfo, you can get the changes
- (void) updateInfo:(NSNotification *)notification
{
self.reviews = [self.managedObjectContext performRequest:myFetchRequest error:nil];
}

CoreData + iCloud + Cascade Delete - how to handle?

CoreData Entity "A" has a one-to-many relationship to a collection of CoreData Entries "B", using a Cascade delete rule.
In an iCloud environment, while device 1 shows a detail view of one of the "B" entries, device 2 deletes the "A" entry.
When the NSPersistentStoreDidImportUbiquitousContentChangesNotification notification is received in device 1, its App Delegate calls mergeChangesFromContextDidSaveNotification and then broadcasts an internal notification which is captured by the view controller showing the details of entry "B" (the code uses performBlock where it should).
However, although entry "A" is indeed nullified when the detail view controller receives the internal notification, entry "B" still exists as a valid CoreData object. It seems that the Cascade rule hasn't completed its operation yet. Therefore the view controller in device 1 isn't aware of the delete, which may lead to unexpected results.
mergeChangesFromContextDidSaveNotification appears to return prematurely, when the base data has been merged but the Cascade rule hasn't completed yet.
I tried to refresh entry "B" when the notification arrives while temporarily setting the stalenessInterval of the managed object context to zero so a cached object won't be used, but I still get a valid entry "B" from the store.
Checking for a null entry "A" at this point is not an option, because the situation is somewhat more complex than what I described here and a null entry "A" can be valid in some cases.
I tried to introduce a delay after merging the changes and before sending the internal notification to the view controllers. I found out that a 2-second delay doesn't help, but a 10-second delay works.
But I don't want to rely on this delay. This is a test environment without much data, and I don't know what will happen in a production environment. Relying on an experimental delay doesn't seem the right thing to do.
Is there a right thing? Or am I doing something wrong to begin with?
From experience, listening to notifications other than NSManagedObjectContextDidSaveNotification is a big mess and can lead to relying on properties not yet updated. The detail view controller should listen to NSManagedObjectContextDidSaveNotification notifications, which are thrown after cascade is applied. You can then check by several means if the current object is valid or not (you can check to see if the current object's managed object context is nil or you can perform a fetch and see if the object exists in the store).

Resources