Core Data, duplicate and deleted RSS entries - objective-c

I've been working on an RSS Reader, using core data for caching. Like a lot of people, I wanted to avoid duplicate entries, which led me to this question, and also this one.
But, there was another thing I wanted, I also wanted to give users the ability to delete articles, and avoid adding deleted articles again when refreshing a feed, that is if the deleted article still existed in the feed. So, my solution currently, is, to maintain another entity in my managed object context with unique identifiers (which how I identify each item in the feed) of deleted articles, I just add the identifier of the article that is being deleted to that entity, and check against it.
Now, here is a piece of code that I wrote to accomplish all of the above. This code is run every time a new item in the feed is parsed, during the parsing process.
dispatch_queue_t checkQueue = dispatch_queue_create("com.feedreader.backgroundchecking", NULL);
dispatch_async(checkQueue,^{
NSMutableArray *mutablesortedArticles = [NSMutableArray arrayWithArray:self.feeds.sortedArticles];
if (!mutablesortedArticles) {
// Handle the error.
}
if ([[mutablesortedArticles valueForKey:#"identifier"]
containsObject:article.identifier]) {
NSLog(#"This article already exists");
return;
}else {
NSMutableArray *mutabledeletedArticles = [NSArray arrayWithArray:self.alldeletedArticles];
if (!mutabledeletedArticles) {
// Handle the error.
}
if ([mutabledeletedArticles valueForKey:#"identifier"]
containsObject:article.identifier]) {
NSLog(#"This article has been deleted");
return;
}else {
Article *newArticle = [NSEntityDescription insertNewObjectForEntityForName:#"Article" inManagedObjectContext:self.managedObjectContext];
newArticle.title = article.title;
newArticle.date = article.date;
newArticle.link = article.link;
newArticle.summary = article.summary;
newArticle.image = article.image;
newArticle.identifier = article.identifier;
newArticle.updated = article.updated;
newArticle.content = article.content;
newArticle.feed = self.feed;
dispatch_async(dispatch_get_main_queue(),^{
NSError *error = nil;
[self.managedObjectContext save:&error];
if (error) {
NSLog(#"%#", error);
}
});
}
}
});
Both, self.feeds.sortedArticles and self.alldeletedArticles are fetched from managed object context before parsing starts.
My problem begins when this code is being run, the UI freezes, for 1-2 seconds (I tried it with a feed that had a little more than 500 articles in the managed object context). So, I guess my question is, is there a more efficient way to do what I'm trying to do here, one that hopefully doesn't freeze the UI?
Perhaps a better way of dealing with deleted articles?

My first suggestion would be to handle deleted articles by added a "itemDeleted" property to the Article entity. Then you have only one list of objects to check when inserting new items.
(Hint: Don't call that attribute "deleted". isDeleted is a built-in property of NSManagedObject, so that is likely to cause name collisions.)
The next suggestion is to save the managed object context only after all items have been imported, and not after each each item (EDIT: See also Caffeine's answer, which was posted while I was writing this.)
Finally, searching each new item in the list of all articles separately is a pattern that does not scale well. Implementing Find-or-Create Efficiently in the "Core Data Programming Guide" describes a pattern that might be better:
for a list of to-be-inserted items, perform a fetch request that fetches all items of this list which are already present in the database,
traverse both the list of new items and the fetched list in parallel, to find out which items are really new and have to be inserted.

The UI freeze is probably caused by [self.managedObjectContext save:&error] since writing out all the objects to disk takes a couple seconds. A great solution to this in iOS 5+ are nested contexts. See this blog post for more details http://www.cocoanetics.com/2012/07/multi-context-coredata/ in particular the Asynchronous Saving section at the end.

Related

How can i know if a relationship is fetched or not in neo4j?

For example I load an entity in the following way:
Movie m = session.load(Movie.class, id, 0); // load properties but not relationships
m.getActors(); // empty since depth was 0
A bit later in another method:
// Do I need to load it?
if (needsLoad(m) {
m = session.load(Movie.class, m.getId(), 1);
}
for (Actor a : m.getActors()) {
// ...
}
The only solution I've found is to load it every time.
Is there a better approach?
There is no API for accessing the session cache.
As a result it is not possible to get information about how deep the object graph got loaded.
I wonder how you get in the situation to need this.
The standard approach would be: load data from the database with the "right" depth, manipulate the data and save it back.
All within one transaction.

Change Store with Magical Record

In my application, I want to use 2 different stores. Each store would be selected based on user selection, load it and do stuff with it.
Does Magical Record support this?
When I set up the core data with [MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:], I create a store name based on the site:
- (void)setupDBForSite:(NSString *)site {
DLog(#"DBStore: %#", [self loadDBStoreForSite:site]);
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:[self loadDBStoreForSite:site]];
}
- (NSString *)loadDBStoreForSite:(NSString *)site {
NSString *bundleID = (NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey];
return [NSString stringWithFormat:#"%# - %#.sqlite", bundleID, site];
}
So I had hoped that if I pass in "Site01" it would create a unique store. The next launch, pass in "Site13". But when I test to see if each store has its own data, both stores contain the same information.
Or am I doing something wrong?
EDIT: Ok, so the issue was with my testing. It looks like it will load the correct store and save to the store fine.
The issue is removing a store and loading a new store. When I load up the store for "Site01" and then load up store for "Site13", I still only see "Site01" data. If I load up store for "Site13" first, it has the correct information.
So I need to remove a store and then load up the new store. But I don't see a method for Magical Record to remove a store.
Your code looks OK. Of course, if you do not do anything different based on the store name, the stores will have the same content.

Deleting multiple CKRecords at the same time

I have been programming in objective-C for about a year now, but i am new to cloud kit. I can do simple things such as fetch, save and delete Records but I have not been able to find a way of deleting multiple Records at a time.
I tried a for loop but, although there were no errors, nothing was deleted.
heres some of the code:
for (CKRecord* r in self.allRecords) {
[[[CKContainer defaultContainer] publicCloudDatabase] deleteRecordWithID:r.recordID completionHandler:^(CKRecordID *recordID, NSError *error) {
if (error) {
NSLog(#"error");
}else
NSLog(#"deleted");
}];
}
allRecords is an array containing the records which i need deleting but it does not delete any of the records.
Thanks
If you need to modify (that is, save or delete) multiple records in one CloudKit round-trip, you need to use a CKModifyRecordsOperation: https://developer.apple.com/library/prerelease/ios/documentation/CloudKit/Reference/CKModifyRecordsOperation_class/index.html
You mention "allRecords is an array containing the records which i need deleting but it does not delete any of the records".
It's not clear whether you mean that the records aren't being deleted from CloudKit, or you mean that the records aren't being deleted from your self.allRecords array.
In case you're expecting the records to be removed from self.allRecords: they won't. That's your job to manage after examining the response from either the CKModifyRecordsOperation or the deleteRecordWithID:completionHandler: call in your snippet above.

Dealing with duplicate contacts due to linked cards in iOS' Address Book API

Some beta-users of my upcoming app are reporting that the list of contacts contain a lot of duplicate records. I'm using the result from ABAddressBookCopyArrayOfAllPeople as the data source for my customized table view of contacts, and it baffles me that the results are different from the iPhone's 'Contacts' app.
When looking more closely at the Contacts app, it seems that the duplicates originate from entries with "Linked Cards". The screenshots below have been obfuscated a bit, but as you see in my app on the far right, "Celine" shows up twice, while in the Contacts app on the left there's only one "Celine". If you click the row of that single contact, you get a "Unified Info" card with two "Linked Cards" (as shown in the center, I didn't use Celine's contact details because they didn't fit on one screenshot):
The issues around "Linked Cards" have quite a few topics on Apple's forums for end users, but apart from the fact that many point to a 404 support page, I can't realistically go around fixing all of my app's users' address books. I would much rather like to deal with it elegantly and without bothering the user. To make matters worse, it seems I'm not the only one with this issue, since WhatsApp is showing the same list containing duplicate contacts.
Just to be clear about the origins of the duplicate contacts, I'm not storing, caching or otherwise trying to be smart about the array ABAddressBookCopyArrayOfAllPeople returns. So the duplicate records come directly from the API call.
Does anyone know how to deal with or detect these linked cards, preventing duplicate records from showing up? Apple's Contacts app does it, how can the rest of us do so too?
UPDATE: I wrote a library and put it on Cocoapods to solve the issue at hand. See my answer below
One method would be to only retrieve the contacts from the default address book source:
ABAddressBookRef addressBook = ABAddressBookCreate();
NSArray *people = (__bridge NSArray *)ABAddressBookCopyArrayOfAllPeopleInSource(addressBook, ABAddressBookCopyDefaultSource(addressBook));
But that is lame, right? It targets the on-device address book, but not extra contacts that might be in Exchange or other fancy syncing address books.
So here's the solution you're looking for:
Iterate through the ABRecord references
Grab each respective "linked references" (using ABPersonCopyArrayOfAllLinkedPeople)
Bundle them in an NSSet (so that the grouping can be uniquely identified)
Add that NSSet to another NSSet
Profit?
You now have an NSSet containing NSSets of linked ABRecord objects. The overarching NSSet will have the same count as the number of contacts in your "Contacts" app.
Example code:
NSMutableSet *unifiedRecordsSet = [NSMutableSet set];
ABAddressBookRef addressBook = ABAddressBookCreate();
CFArrayRef records = ABAddressBookCopyArrayOfAllPeople(addressBook);
for (CFIndex i = 0; i < CFArrayGetCount(records); i++)
{
NSMutableSet *contactSet = [NSMutableSet set];
ABRecordRef record = CFArrayGetValueAtIndex(records, i);
[contactSet addObject:(__bridge id)record];
NSArray *linkedRecordsArray = (__bridge NSArray *)ABPersonCopyArrayOfAllLinkedPeople(record);
[contactSet addObjectsFromArray:linkedRecordsArray];
// Your own custom "unified record" class (or just an NSSet!)
DAUnifiedRecord *unifiedRecord = [[DAUnifiedRecord alloc] initWithRecords:contactSet];
[unifiedRecordsSet addObject:unifiedRecord];
CFRelease(record);
}
CFRelease(records);
CFRelease(addressBook);
_unifiedRecords = [unifiedRecordsSet allObjects];
I've been using ABPersonCopyArrayOfAllLinkedPeople() in my app for some time now. Unfortunately, I've just discovered that it doesn't always do the right thing. For example, if you have two contacts that have the same name but one has the "isPerson" flag set and the other does not, the above function won't consider them "linked". Why is this an issue? Because Gmail(exchange) sources don't support this boolean flag. If you try to save it as false, it will fail, and the contact you saved in it will come back on the next run of your app as unlinked from the contact you saved in iCload (CardDAV).
Similar situation with social services: Gmail doesn't support them and the function above will see two contacts with the same names as different if one has a facebook account and one does not.
I'm switching over to my own name-and-source-recordID-only algorithm for determining whether two contact records should be displayed as a single contact. More work but there's a silver lining: ABPersonCopyArrayOfAllLinkedPeople() is butt-slow.
The approach that #Daniel Amitay provided contained nuggets of great value, but unfortunately the code is not ready for use. Having a good search on the contacts is crucial to my and many apps, so I spent quite a bit of time getting this right, while on the side also addressing the issue of iOS 5 and 6 compatible address book access (handling user access via blocks). It solves both the many linked cards due to incorrectly synched sources and the cards from the newly added Facebook integration.
The library I wrote uses an in-memory (optionally on-disk) Core Data store to cache the address book record ID's, providing an easy background-threaded search algorithm that returns unified address book cards.
The source is available on a github repository of mine, which is a CocoaPods pod:
pod 'EEEUnifiedAddressBook'
With the new iOS 9 Contacts Framework you can finally have your unified contacts.
I show you two examples:
1) Using fast enumeration
//Initializing the contact store:
CNContactStore* contactStore = [CNContactStore new];
if (!contactStore) {
NSLog(#"Contact store is nil. Maybe you don't have the permission?");
return;
}
//Which contact keys (properties) do you want? I want them all!
NSArray* contactKeys = #[
CNContactNamePrefixKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactFamilyNameKey, CNContactPreviousFamilyNameKey, CNContactNameSuffixKey, CNContactNicknameKey, CNContactPhoneticGivenNameKey, CNContactPhoneticMiddleNameKey, CNContactPhoneticFamilyNameKey, CNContactOrganizationNameKey, CNContactDepartmentNameKey, CNContactJobTitleKey, CNContactBirthdayKey, CNContactNonGregorianBirthdayKey, CNContactNoteKey, CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactImageDataAvailableKey, CNContactTypeKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactPostalAddressesKey, CNContactDatesKey, CNContactUrlAddressesKey, CNContactRelationsKey, CNContactSocialProfilesKey, CNContactInstantMessageAddressesKey
];
CNContactFetchRequest* fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:contactKeys];
[fetchRequest setUnifyResults:YES]; //It seems that YES is the default value
NSError* error = nil;
__block NSInteger counter = 0;
And here i loop through all unified contacts using fast enumeration:
BOOL success = [contactStore enumerateContactsWithFetchRequest:fetchRequest
error:&error
usingBlock:^(CNContact* __nonnull contact, BOOL* __nonnull stop) {
NSLog(#"Unified contact: %#", contact);
counter++;
}];
if (success) {
NSLog(#"Successfully fetched %ld contacts", counter);
}
else {
NSLog(#"Error while fetching contacts: %#", error);
}
2) Using unifiedContactsMatchingPredicate API:
// Contacts store initialized ...
NSArray * unifiedContacts = [contactStore unifiedContactsMatchingPredicate:nil keysToFetch:contactKeys error:&error]; // Replace the predicate with your filter.
P.S You maybe also be interested at this new API of CNContact.h:
/*! Returns YES if the receiver was fetched as a unified contact and includes the contact having contactIdentifier in its unification */
- (BOOL)isUnifiedWithContactWithIdentifier:(NSString*)contactIdentifier;
I'm getting all sources ABAddressBookCopyArrayOfAllSources, moving the default one ABAddressBookCopyDefaultSource to the first position, then iterate through them and getting all people from source ABAddressBookCopyArrayOfAllPeopleInSource skipping ones I've seen linked before, then getting linked people on each ABPersonCopyArrayOfAllLinkedPeople.

Filtering NSFetchedResultsController results at runtime

What's the best way to implement runtime filtering of Core Data objects using NSFetchedResultsController?
For example, I want to be able to display all Record-entities in a RecordStore-entity, but also filter all Records in a RecordStore for some predefined critera, eg (ANY recordStore.records.count > 0).
I read that changing an NSFetchedResultsController's predicate after it has been created is bad. So should I store the fetched results in an NSArray that I can filter and use that as the UITableView's datasource, or should I create multiple NSFetchedResultsControllers?
You can re-fetch the data when you need to update. If the data itself changed, then you can just call fetch again. If your criteria changed, then set the predicate on your NSFetchedResultsController, and call fetch.
Create the NSFetchRequest instance, update request each time and perform fetch. Delete cache if you are using:
let shortDescriptor = NSSortDescriptor(key: key, ascending: ascending)
fetchedResultViewController.fetchRequest.sortDescriptors = [shortDescriptor]
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: fetchedResultViewController.cacheName)
do {
try fetchedResultViewController?.performFetch()
} catch let error as NSError {
print("Error in fetch \(error)")
}
Read the apple document: https://developer.apple.com/reference/coredata/nsfetchedresultscontroller