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

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.

Related

CNContact :migration from ABAddressBook : iOSLegacyIdentifier + lastModifcationDate

i have a DB that stores all local user contacts.
now i want to use the new framework (contact framework), my problem is that the CNContact have a new identifier now (no longer the auto-incretntal one) called "identifier" and i can't mach old entries in my DB with a potential update of a contact.
i have 2 questions:
in xcode debugger, i can see _iOSLegacyIdentifier(the old, auto-incremental one) as a property in CNContact, how can i get it without private API calls
i can't see "lastModifcationDate" for the CNContact (in ABAddressBook framework it is called kABPersonModificationDateProperty) how can i get it using the new framework.
thanks.
[EDIT]: i have open a ticket for Apple about this and here's the answer:
There are no plans to address this based on the following:
1) iOSLegacyIdentifier is private API for CNContact. 2) A modification
date is not offered on CNContact.
To migrate your DB you can to match contacts by name and disambiguate
by manually matching other properties like email addresses or phone
numbers.
We are now closing this report.
as you can see there's no real solution for this, we have to guess..
you can obtain ContactID (iOSLegacyIdentifier) with new Contact Framework. I use this in my app to find iOSLegacyIdentifier for specific contact, you can modify for your pleasure.
let predicate = CNContact.predicateForContacts(matchingName: "contactName")
let toFetch = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactIdentifierKey]
do{
let contacts = try self.contactStore.unifiedContacts(matching: predicate, keysToFetch: toFetch as [CNKeyDescriptor])
for contact in contacts{
var diccionario:[String : Any] = contact.dictionaryWithValues(forKeys: ["iOSLegacyIdentifier"])
//With this you can see/obtain iOSLegacyIdentifier
print(diccionario["iOSLegacyIdentifier"] as! Int)
return;
}
} catch let err{
print(err)
}
1.Doesn't exists. There is only private selector
[CNContact iOSLegacyIdentifier];
or you can get the same
[CNContainer CNContainerIOSLegacyIdentifierKey];
Mind that this is not compiled in framework. Use perform selector
2.There is no such property in the new framework. If you disassembly the Contact framework you can see that uniqueId is still used in predicates that touches underlaying core data. But that's a work for you and again dance with private selectors
(blame Apple, not me that there is no way). Take a look at internals of the framework.

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 all table records from core data database using relationship

I'm using core data for my application i have 4-5 tables one of which is userProfile table. i have implemented logout in the app. if user logs out of the app im deleting user profile and will be inserting new if logged in with other user account. i want to delete all the records from database on userprofile delete. im using relationship for this but it is not deleting other records from the db even if user profile record has been deleted.
one thing i would like to mention is all the data is coming from service. and i am using cascade delete rule for relationship created between userprofile table and other tables.
You have two different ways to achieve this.
The first is to delete the store and create it again. This means access the store in the file system and delete the sql file, for example. For example, you can find how to achieve it in the following discussion: Delete/Reset all entries in Core Data?.
The second is solution is to create a cascade relationship in UserProfile entity that will link the other ones. In the latter you must set up an inverse relationship (nullify would be the correct approach). For further info see my answer at Setting up a parent-child relationship in Core Data.
Said this, and based on my experience, I would discourage to save user info (e.g. passwords) in Core Data. Instead, adopt the Keychain for this. There are libraries that wrap the Keychain access in an easy manner (e.g. SSKeychain).
I tried using relationship using cascade rule but didn't worked for me so I used Delete/Reset all entries in Core Data method. Used following code for this.
NSError * error;
NSURL * storeURL = [[AppDelegate.managedObjectContext persistentStoreCoordinator] URLForPersistentStore:[[[AppDelegate.managedObjectContext persistentStoreCoordinator] persistentStores] lastObject]];
[AppDelegate.managedObjectContext lock];
[AppDelegate.managedObjectContext reset];//to drop pending changes
//delete the store from the current managedObjectContext
if ([[AppDelegate.managedObjectContext persistentStoreCoordinator] removePersistentStore: [[[AppDelegate.managedObjectContext persistentStoreCoordinator] persistentStores] lastObject] error:&error])
{
// remove the file containing the data
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//recreate the store like in the appDelegate method
[[[AppDelegate managedObjectContext] persistentStoreCoordinator] addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options: #{NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES} error:&error];
}
[AppDelegate.managedObjectContext unlock];
//that's it !
This worked for me.

One To Many With Convenience Backpointer

I'd like to model a one-to-many relationship between users and checkIns on Parse.com. Additionally, I'd like a user to have a convenience pointer to it's lastCheckin, to avoid having to query checkIns to get a user's most recent checkIn. Using the following code, the user's lastCheckin column is always empty in the Data Browser:
PFUser *currentUser = [PFUser currentUser];
PFObject *checkIn = [PFObject objectWithClassName:#"CheckIn"];
checkIn[#"forUser"] = currentUser;
checkIn[#"foursquareID"] = selectedVenue[#"id"];
checkIn[#"name"] = selectedVenue[#"name"];
checkIn[#"location"] = [PFGeoPoint geoPointWithLatitude:[selectedVenue[#"location"][#"lat"] doubleValue]
longitude:[selectedVenue[#"location"][#"lng"] doubleValue]];
[checkIn saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
currentUser[#"lastCheckin"] = checkIn;
[currentUser saveInBackground];
}];
Removing the following line causes the User.lastCheckin to correctly point to the desired CheckIn in the Data Browser, but obviously this breaks the one-to-many linkage:
checkIn[#"forUser"] = currentUser;
Is there some way for me to get both the one-to-may relationship between Users and CheckIns, and a User.lastCheckIn pointer?
If you are setting the lastCheckin on the User each time you create a new CheckIn you can access that checkin easily by fetching the current user and including lastCheckin with includeKey.
http://blog.parse.com/2011/12/06/queries-for-relational-data/
To get all checkins for a user you can simply query the CheckIn class where the current user is equal to forUser. That is your 1 to many relationship. You do not need to maintain a relation of checkins on the User class since you have a pointer from each CheckIn instance back to the user as a pointer. When you set lastCheckin on the user instance you are not changing the property on older checkins.
Perhaps you are thinking this is like SQL with primary and foreign keys. I get confused by NoSQL since I have a SQL background. I am always learning to think like Parse and NoSQL. What I see above is all you need to query all checkins for a user and the last checkin for a user. You can also simply query all checkins for a user, limit result to 1 and sort it so the newest checkin is the first result. It should be quite fast on Parse.
You may also want to look into using Cloud Code. You could call a Cloud Code function and simply pass the parameters you need to create a new checkin and have all of this work done in Cloud Code. An After Save function could be used to update lastCheckin on the user.
http://www.parse.com/docs/js/symbols/Parse.Cloud.AfterSaveRequest.html
If you user on Cloud Code you can update how the data is managed without updating the iOS app.

Core Data, duplicate and deleted RSS entries

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.