I want to create a custom view that is just like contacts in iPhone. Is there any tutorial available to make a custom view just like ABPeoplePickerNavigationController?
Please note that I do not want to open the default people picker controller provided by AddressBookUI framework. Also, I do want to bind this navigation controller into my main view.
For reference that what I exactly want, You can refer contacts tab of Whatsapp on iOS device.
EDIT: I already got a contact list and displayed the first and last name of person in a table view. Now, I want to create an index for alphabets from A-Z and on tapping of that index, the table view should scroll to that contacts. Also, How can I implement search functionality to find user by his / her first or last name?
I have the exact same thing in the app i'm currently building and we also got our inspiration from apps like Whatsapp when it came to contacts.
I used a simple tableview with custom cells for visuals, with simply name & picture.
My process was the following :
Create a contact object in coredata (or another persistent way of keeping your data)
Through the ABAddressbook framework you can browse all your contacts and transform them in your new Contact objets. Keep a reference of your ABPerson in your Contact object, this will allow you to find-and-update your Contacts later just using references. If you don't do that you will have to browse to all your ABPersons every time you want to update your Contacts.
You could use the ABPersons directly but it would just be really painful to code.
Once you've extracted all your contacts, make sure to save your context if you use core data, or store them in .sqlite.
You can then simply extract them in an array of Contacts and display those in a custom cell of your choosing.
This appcoda tutorial is a decent custom cell for tableview tutorial. You can find a thousand more just by googling " tableview custom cell ios" and finding different things that you might like. In the end, you'll just have a cell with a label and a picture, you COULD use the simple UITableViewCell which I used for another tableview of "contact" type.
Keeping that contact list up to date (getting the right numbers, pictures, names, etc) and making sure they exist before updating, checking if a contact has been deleted, added, etc. All that has to be done in order for your list to be accurate, and it's a pretty long/annoying process.
I could share my Contact class but it includes a lot of irrelevant code for you which might confuse you because :
- I'm also checking if those contacts are already users of my app to move them in specific sections of my tableview
- I split my tableview in 27 sections (users, then alphabet letters).
Also, and I'll stop it with that last piece of general programming advice : It would be a good idea to write down first exactly what you need and what you'll need, get all the possibilities on paper, etc. I bumped into a lot of simple problems that it took me a while to resolve, either because I didn't plan properly or because it was hidden.
For example :
Some of your contacts don't have names, at all.
Some of your contacts have a name in the "Title" field (where you write Doctor, or Mister)
Some of your contacts don't have phone numbers (if you're using phone numbers as identifiers)
Some of your contacts have international formatting and some not (+32 46556555 or 0032 46556555)
Some of your contacts have pictures taken with camera, some others from Gmail, which have different formats
You might have duplicate contacts (same name, same everything) due to poor sync from the user
You need to make sure the firstname/lastname is in the right section, case sensitive coding can cause trouble
You need to find a solution for a contact that start with a smiley or non alphanumeric characters
Your users will want an index-list on the side
Of course you'll need to add a search bar because, some people have way more than 1000 contacts.
Many more to come, I guarantee that.
Because this is very app-specific i'm not gonna go over every problem that I had and what I did for it, but you get the idea :) Feel free to ask any very specific questions though and I might already have a very specific solution, since I pretty much had to copy whatsapp's contacts from scratch and, hell, I made it. (I actually got the exact same as Anonymess and iOS)
EDIT : Here are some methods of my ABPerson extracting methods ; the ContactDAO mostly interact with my persistent model (CoreData) and I believe their names are clear enough for you to understand what's happening. I'm kind of happy with the comments and variablenames so you should be to read that without too much trouble.
Here comes a massive block of code.
- (NSMutableArray *)getAllRecordsWithPeople:(CFArrayRef)allPeople andAddressBook:(ABAddressBookRef)addressbook{
NSMutableArray *newRecords = [[NSMutableArray alloc]init];
CFIndex nPeople = ABAddressBookGetPersonCount(addressbook);
for (int i=0;i < nPeople;i++){
ABRecordRef ref = CFArrayGetValueAtIndex(allPeople,i);
ABRecordID refId = ABRecordGetRecordID(ref);
NSNumber *recId = [NSNumber numberWithInt:refId];
[newRecords addObject:recId];
}
return newRecords;
}
- (void)getValidContactsFromAddressBookWithCompletionBlock:(void (^)(NSError *error))completion{
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, nil);
__block BOOL accessGranted = NO;
if (&ABAddressBookRequestAccessWithCompletion != NULL) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
accessGranted = granted;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
if (accessGranted) {
NSMutableArray *newRecords = [[NSMutableArray alloc]init];
NSMutableArray *updatedRecords = [[NSMutableArray alloc]init];
NSMutableArray *unchangedRecords = [[NSMutableArray alloc]init];
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);
//Checking the last time we updated
NSTimeInterval lastSyncTime;
if ([[NSUserDefaults standardUserDefaults]objectForKey:#"LastSyncTime"] == nil){
//This is the first time we update.
lastSyncTime = 0;
}else{
//Setting the last update in variable
lastSyncTime = [[[NSUserDefaults standardUserDefaults]objectForKey:#"LastSyncTime"]doubleValue];
}
if (lastSyncTime == 0){
//We have to insert everyone, this is the first time we do this.
newRecords = [self getAllRecordsWithPeople:allPeople andAddressBook:addressBook];
}else{
//We have to manually compare everyone to see if something has changed.
for (int i=0;i < nPeople;i++) {
ABRecordRef ref = CFArrayGetValueAtIndex(allPeople,i);
ABRecordID refId = ABRecordGetRecordID(ref);
NSNumber *recId = #(refId);
CFDateRef recordCreation = ABRecordCopyValue(ref, kABPersonCreationDateProperty);
NSDate *recCreDate = (__bridge NSDate *)(recordCreation);
NSTimeInterval creDateInterval = [recCreDate timeIntervalSince1970];
if(creDateInterval > lastSyncTime){
//This object was created after my lastSync, this must be a new record
[newRecords addObject:recId];
}else{
//Checking the last update of the given record
CFDateRef recordUpdate = ABRecordCopyValue(ref, kABPersonModificationDateProperty);
NSDate *recUpDate = (__bridge NSDate*)(recordUpdate);
if ([recUpDate timeIntervalSince1970] > lastSyncTime){
//The record was somehow updated since last time, we'll update it
[updatedRecords addObject:recId];
}else{
//The record wasn't updated nor created, it is therefore unchanged.
//We still need to keep it in a separate array to compare deleted contacts
[unchangedRecords addObject:recId];
}
}
}
if(allPeople)
CFRelease(allPeople);
}
[self manageNewContacts:newRecords updatedContacts:updatedRecords andUnchangedContacts:unchangedRecords inAddressBook:addressBook andBlock:^(NSError *error) {
completion(error);
}];
}else{
NSError *error = [NSError errorWithDomain:#"ABAccess access forbidden" code:403 userInfo:nil];
completion(error);
}
}
- (void)manageNewContacts:(NSMutableArray*)newRecords updatedContacts:(NSMutableArray*)updatedRecords andUnchangedContacts:(NSMutableArray*)unchangedRecords inAddressBook:(ABAddressBookRef)addressbook andBlock:(void (^)(NSError *error))completion{
AppDelegate *app = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = app.managedObjectContext;
//Getting all the CoreData contacts IDs to have something to compare
NSArray *coreDataContactsIds = [ContactDAO getAllContactIdsInManagedObjectContext:context];
for (NSDictionary *rec in coreDataContactsIds){
NSNumber *recId = rec[#"record_id"];
if (![unchangedRecords containsObject:recId]){
//The unchanged record doesn't exist locally
if (![updatedRecords containsObject:recId]){
//The updated record doesn't exist locally
if (![newRecords containsObject:recId]){
//The new record doesn't exist locally
//That means the ongoing record has been deleted from the addressbook,
//we also have to delete it locally
[ContactDAO deleteContactWithID:recId inManagedObjectContext:context];
}
}
}
}
for (NSNumber *recId in updatedRecords){
ABRecordID recordID = (ABRecordID)recId.intValue;
ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressbook, recordID);
NSDictionary *personDict = [self getPersonDictionaryFromABRecordRef:person];
if (personDict){
[ContactDAO updateContactWithFirstName:personDict[#"firstName"] lastName:personDict[#"lastName"] compositeName:personDict[#"compositeName"] picture:personDict[#"picture"] phoneNumbers:personDict[#"phoneNumbers"] recordID:recId inManagedObjectContext:context];
}
}
for (NSNumber *recId in newRecords){
ABRecordID recordID = (ABRecordID)recId.intValue;
ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressbook, recordID);
NSDictionary *personDict = [self getPersonDictionaryFromABRecordRef:person];
if (personDict){
[ContactDAO createContactWithFirstName:personDict[#"firstName"] lastName:personDict[#"lastName"] compositeName:personDict[#"compositeName"] picture:personDict[#"picture"] phoneNumbers:personDict[#"phoneNumbers"] recordID:recId inManagedObjectContext:context];
}
}
NSError *dbError;
[context save:&dbError];
NSTimeInterval lastSyncTime = [[NSDate date]timeIntervalSince1970];
[[NSUserDefaults standardUserDefaults]setObject:#(lastSyncTime) forKey:#"LastSyncTime"];
completion(dbError);
}
- (NSDictionary*)getPersonDictionaryFromABRecordRef:(ABRecordRef)person{
//Get name
NSString * firstName, *lastName;
firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
firstName = (firstName == nil) ? #"" : firstName;
lastName = (lastName == nil) ? #"" : lastName;
NSString *compositeName;
if ([firstName isEqualToString:#""] && [lastName isEqualToString:#""]){
return nil;
}
if ([lastName isEqualToString:#""]){
compositeName = [NSString stringWithFormat:#"%#", firstName];
}
if ([firstName isEqualToString:#""]){
compositeName = [NSString stringWithFormat:#"%#", lastName];
}
if (![lastName isEqualToString:#""] && ![firstName isEqualToString:#""]){
compositeName = [NSString stringWithFormat:#"%# %#", firstName, lastName];
}
//Get picture
CFDataRef imageData = ABPersonCopyImageData(person);
NSData *data = CFBridgingRelease(imageData);
//Get phone numbers
NSMutableSet *phoneNumbers = [[NSMutableSet alloc]init];
ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
for(CFIndex i = 0; i < ABMultiValueGetCount(phones); i++) {
CFStringRef str = ABMultiValueCopyValueAtIndex(phones, i);
NSString *num = CFBridgingRelease(str);
[phoneNumbers addObject:num];
/*if(str)
CFRelease(str);*/
}
//Save it in dictionary
NSDictionary *personDict = [[NSDictionary alloc]initWithObjectsAndKeys:firstName, #"firstName",lastName , #"lastName",phoneNumbers,#"phoneNumbers", compositeName, #"compositeName", data, #"picture", nil];
//Release everything.
if(phones)
CFRelease(phones);
return personDict;
}
When it comes to indexes, this tutorial should do fine.
Have a look to this : http://www.appcoda.com/ios-programming-index-list-uitableview/
Methods of table view help out to give your desired result :
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
and
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title
Related
Here's what I'm doing:
- (void)doCreateGroup {
[[self contentView] endEditing:true];
NSString * newString = [[[[self contentView] groupNameField] text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString * firstError = nil;
if ([newString length] == 0) {
firstError = #"Missing group name";
}
NSError * groupsError = nil;
NSArray * groups = [self.contactStore groupsMatchingPredicate:nil error:&groupsError];
for (CNGroup * group in groups) {
if ([group.name isEqualToString:newString]) {
firstError = #"Group already exists";
}
}
if (firstError) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:firstError] animated:YES completion:nil];
return;
}
CNMutableGroup * newGroup = [CNMutableGroup new];
[newGroup setName:newString];
CNSaveRequest *saveRequest = [CNSaveRequest new];
[saveRequest addGroup:newGroup toContainerWithIdentifier:nil];
NSError * error = nil;
[self.contactStore executeSaveRequest:saveRequest error:&error];
if (error) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:[error localizedDescription]] animated:YES completion:nil];
} else {
CNSaveRequest *saveRequest2 = [CNSaveRequest new];
NSArray * groupsAgain = [self.contactStore groupsMatchingPredicate:nil error:&groupsError];
CNGroup * gotGroup;
for (CNGroup * group in groupsAgain) {
if ([group.name isEqualToString:newString]) {
gotGroup = group;
}
}
for (CNContact * contact in self.selectedContactsArray) {
[saveRequest2 addMember:contact toGroup:gotGroup];
}
NSError * error1 = nil;
[self.contactStore executeSaveRequest:saveRequest2 error:&error1];
if (error) {
[self presentViewController:[WLGCommonUtilities doProcessErrorWithOkay:#"Error" errorMessage:[error1 localizedDescription]] animated:YES completion:nil];
} else {
[[self navigationController] dismissViewControllerAnimated:true completion:nil];
}
}
}
this works to create a CNGroup and then add contacts to said CNGroup. Works for all contacts EXCEPT for unified contacts. I've tried everything possible to make this work and it just doesn't. It likely has something to do with the unified CNContact's identifier since that identifier is only stored in temp memory so it can't be added to a CNGroup since it doesn't really haver a REAL CNContact identifier. Contacts framework is a mess! Any help would be appreciated. I've also filed a tech support request with Apple.
EDIT:
One way to get around this is to use Address Framework that is now deprecated. I can add as many unified contacts to Address groups by doing this.
ABRecordRef group = ABGroupCreate();
ABAddressBookAddRecord(addressBook, group, nil);
ABRecordSetValue(group, kABGroupNameProperty,#"My Groups", nil);
for (int i=0;i < nPeople;i++) {
ABRecordRef ref = CFArrayGetValueAtIndex(allPeople,i);
ABGroupAddMember(group, ref, nil);
ABAddressBookSave(addressBook, nil);
}
this does save everything in the contact book to a group, all visible contacts that is. so it does store the Unified contact into the group. if you unlink the contacts while they are in a group, both contacts stay within the group. so the old framework works to solve this. just seems ridiculous that it can't be solved with new Contacts framework. Again, I may be missing something with the new Contacts framework, so if this is possible with the current Contacts framework in iOS please let me know.
i figured it out. this is a mess
step one:
NSMutableArray * finalArray = [NSMutableArray array];
NSMutableArray * unifiedContacts = [NSMutableArray array];
NSMutableArray * fullContacts = [NSMutableArray array];
CNContactFetchRequest * request = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];
[request setSortOrder:CNContactSortOrderGivenName];
[self.contactStore enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
[unifiedContacts addObject:contact];
}];
CNContactFetchRequest * request2 = [[CNContactFetchRequest alloc] initWithKeysToFetch:keys];
[request2 setUnifyResults:false];
[request2 setSortOrder:CNContactSortOrderGivenName];
[self.contactStore enumerateContactsWithFetchRequest:request2 error:nil usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
[fullContacts addObject:contact];
}];
for (CNContact * contctUn in unifiedContacts) {
NSMutableArray * nestedContacts = [NSMutableArray array];
for (CNContact * contct in fullContacts) {
if ([contctUn isUnifiedWithContactWithIdentifier:contct.identifier]) {
[nestedContacts addObject:contct];
}
}
if (nestedContacts.count) {
[finalArray addObject:#{#"contact" : contctUn, #"linked" : nestedContacts}];
} else {
[finalArray addObject:#{#"contact" : contctUn}];
}
}
self.mainArray = [finalArray mutableCopy];
this pulls in all contacts from unified contacts and then pulls in all un-unified contacts, splices the groups together and saves them as dictionaries with "linked" being an array of linked contacts if the contact is indeed linked to the contact in question.
step 2: create a group ... this is pretty simple, no need to show the code since this is pretty easy
step 3:
for (id obj in self.filteredSearchArray) {
if ([obj valueForKey:#"linked"]) {
for (id obj2 in [obj valueForKey:#"linked"]) {
[self.selectedContactsArray addObject:obj2];
}
}
}
CNSaveRequest *saveRequest2 = [CNSaveRequest new];
for (CNContact * contact in self.selectedContactsArray) {
[saveRequest2 addMember:contact toGroup:[newGroup copy]];
}
NSError * error1 = nil;
[self.contactStore executeSaveRequest:saveRequest2 error:&error1];
self.selectedContactsArray is the array that contains the contacts you want in the group. it contains all contacts you want in the group in addition it contains the sublinked contacts if a contact you want in the group is linked to a user.
when this save request executes the group now contains the unified contact.
this is a mess. Contacts Framework in iOS is a mess, but this works. No app that creates groups for contacts has solve this, so here's the million dollar solution.
That seems odd indeed. As at least a workaround, have you tried to fetch the selected contacts with a CNContactFetchRequest that has its unifyResults set to false?
I mean, I don't know where you get the selectedContactsArray from, I assume either you can modify an existing request that gave you that data accordingly or you have to somehow refetch the contacts again. That's probably really, really ugly, as you would have to construct a fetch request with a predicate or key set that is guaranteed to match the same contacts (and only those contacts) plus said unifyResults member set to false.
I'd imagine something like this (sorry for using swift, it's a little compacter for me right now, I hope that's okay):
let allMyIds: [String] = self.selectedContactsArray.map { $0.identifier }
let predicate: NSPredicate = CNContact.predicateForContacts(withIdentifiers: allMyIds)
let fetchRequest = CNContactFetchRequest(keysToFetch: someKeys)
// not sure what you'd need here for someKeys...
// I assume it would have to be a key definitely present in all contacts you
// are interested in, e.g. name? I might be wrong though...
fetchRequest.unifyResults = false
_ = self.contactStore.enumerateContacts(with: fetchRequest, usingBlock: { contact, errorPointer in
// your group adding/save preparation code here
})
I admit I am not that familiar with the Contacts framework, so I can't say whether that is really feasible. Especially the set of keys you'd have to provide to the enumerate... method might be tricky if you don't have a key that's guaranteed to be part of all contacts you want.
I apologize for such a half-baked answer, but maybe it can at least give you a new impulse.
I've been using Parse.com for two months, but this is the first time I get a stupid error which I can't figure out.
I've been working in an app for iOS, but in the beginning I've been working with my own user. Now I registered other users and I want to fetch them from the server.
Well, I add one object in the table Changes, where I have a user property which is a Pointer<User> (it's not _User, but User, it's custom, just an object). Well, when I try to fetch all the rows I have, the one that I have with my user are ok:
so in my debug console is like :
but when I fetch other users:
my debug console is:
so there's not any variable!!!! :(
This is my code:
PFQuery *closestChanges = [PFQuery queryWithClassName:#"Changes"];
[closestChanges whereKey:#"coordinates" nearGeoPoint:geoPoint withinKilometers:0.5f];
[closestChanges findObjectsInBackgroundWithBlock:^(NSArray *changes, NSError *error) {
arrayChanges0 = [[NSMutableArray alloc] init];
arrayChanges1 = [[NSMutableArray alloc] init];
if (changes == nil && changes.count == 0) {
[_tableView reloadData];
return;
}
for (int i = 0; i < changes.count; i++) {
PFObject *currentChange = [changes objectAtIndex:i];
PFObject *user = [currentChange valueForKey:#"user"]; // here my user is null when it's other users.
PFObject *changeToStore = [PFObject objectWithClassName:#"Changes"];
[changeToStore setValue:currentChange[#"changetype"] forKey:#"type"];
[changeToStore setValue:currentChange[#"quantity"] forKey:#"quantity"];
[changeToStore setValue:currentChange[#"date"] forKey:#"enddata"];
[changeToStore setValue:user forKey:#"user"];
if ([currentChange[#"changetype"] intValue] == 0)
[arrayChanges0 addObject:changeToStore];
else
[arrayChanges1 addObject:changeToStore];
}
[_tableView reloadData];
}];
Is there anything wrong I'm doing by adding new users???
Thank you very much in advance.
When you fetch a pointer type from a table, you will only get back the metadata which is objectId. You need to call - (instancetype)includeKey:(NSString *)key in order to get all the data back from Changes table. If you query directly from User table, you will get all the data. But in this case, you User object is a subdocument from Changes objects.
Add this line before performing querying:
[closestChanges includeKey:#"user"]
I'm writing a player software and need to add tags to the songs. For this I've created a core data record.
How do I keep my library in sync with the iOS music library? My approach until now is to get the list of songs in the music library, and the list of songs in CoreData. Then use a dictionary and search for the uuid. If it exists I remove it from my list of CoreData items. After this loop I simply remove all that is left in my CoreData items list.
The problem with this approach, for some reason it crashes with NSFastEnumerationMutationHandler which basicly means I'm trying to access an object in an array in a loop just after I removed it.
- (void)updateMediaLibrary {
NSArray *mediaItems = [self getMediaItems];
NSMutableDictionary *coreDataItems = [self getCoreDataItems];
for (MPMediaItem *item in mediaItems) {
NSNumber *uuid = [item valueForProperty:MPMediaItemPropertyPersistentID];
Song *song = [coreDataItems valueForKey:[uuid stringValue]];
if (!song) {
[Song createSongFromMediaItem:item inContext:self.context];
}
else {
[coreDataItems removeObjectForKey:[uuid stringValue]];
}
}
for (Song *item in coreDataItems) {
[Song deleteSongWithUID:item.libKey inContext:self.threadContext];
}
}
I might just be blind but for some reason I can't see my error in this code.
Do you have alternative/better suggestions, how to keep the songs in sync?
Thanks for your help!
Rather than making a list of the actual coreDataItems, make a list of the persistent keys and do your work with that, something like:
- (void)updateMediaLibrary {
NSArray *mediaItems = [self getMediaItems];
NSMutableSet *coreDataItems = [NSMutableSet setWithArray:[[self getCoreDataItems] allKeys]];
for (MPMediaItem *item in mediaItems) {
NSNumber *uuid = [item valueForProperty:MPMediaItemPropertyPersistentID];
if (![coreDataItems containsObject:[uuid stringValue]]) {
[Song createSongFromMediaItem:item inContext:self.context];
}
else {
[coreDataItems removeObject:[uuid stringValue]];
}
}
for (NSString* key in coreDataItems) {
[Song deleteSongWithUUID:key];
}
}
To me, the problem is probably coming from that method:
[Song createSongFromMediaItem:item inContext:self.context]
Or from that method:
[Song deleteSongWithUID:item.libKey inContext:self.threadContext].
They probably modify the NSArray and/or the NSMutableDictionary during enumeration.
One thing that you could do is:
NSArray *mediaItems = [self getMediaItems].copy;
NSMutableDictionary *coreDataItems = [self getCoreDataItems].mutablecopy;
I want my users to fill an email field by selecting a contact's email from their address books. I don't want them to scroll all the contacts whose emails are not set, so I want to filter the ones that have email addresses.
This is the code I've written so far. I can figure out who has an email address and who has not but I couldn't tell the ABPeoplePickerNavigationController to list only the right contacts. Is it impossible to achieve this, I mean do I have to implement my own contact picker class by using a table view or is there something wrong with this piece of code?
ABAddressBookRef addressBook = ABAddressBookCreate();
NSArray *peopleList = (NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook);
NSLog(#"%ld people exist in the addressBook", ABAddressBookGetPersonCount(addressBook));
for (id peopleRecord in peopleList) {
ABMultiValueRef mv = ABRecordCopyValue((ABRecordRef)peopleRecord, kABPersonEmailProperty);
CFIndex numberOfAddresses = ABMultiValueGetCount(mv);
if(numberOfAddresses == 0) {
CFErrorRef err;
ABAddressBookRemoveRecord( addressBook, (ABRecordRef)peopleRecord, &err);
}
}
[peopleList release];
NSLog(#"%ld people have an email", ABAddressBookGetPersonCount(addressBook));
ABPeoplePickerNavigationController *peoplePicker = [[ABPeoplePickerNavigationController alloc] init];
NSNumber* emailProp = [NSNumber numberWithInt:kABPersonEmailProperty];
[peoplePicker setAddressBook:addressBook];
peoplePicker.displayedProperties = [NSArray arrayWithObject:emailProp];
[peoplePicker setPeoplePickerDelegate:self];
[self presentModalViewController:peoplePicker animated:YES];
I know this is old, but I stumbled across this while researching a related topic, so I thought I'd update this with my findings.
While it doesn't filter the results, it's worth noting that iOS 8 has a feature for disabling contacts that don't have an email address:
peoplePickerController.predicateForEnablingPerson = [NSPredicate predicateWithFormat:#"emailAddresses.#count > 0"];
You still see all of the contacts, but at least those without an email address are disabled. Obviously, if your minimum target OS predates iOS 8, you'd do the above conditionally:
if ([peoplePickerController respondsToSelector:#selector(predicateForEnablingPerson)]) {
peoplePickerController.predicateForEnablingPerson = [NSPredicate predicateWithFormat:#"emailAddresses.#count > 0"];
}
If you want to filter out those entries without addresses, you'll have to present your own UI (e.g. create your own tableview) for this. So, first, build your own array of contacts with email addresses like so:
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
if (granted) {
NSArray *allPeople = CFBridgingRelease(ABAddressBookCopyArrayOfAllPeople(addressBook));
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id person, NSDictionary *bindings) {
ABMultiValueRef emails = ABRecordCopyValue((__bridge ABRecordRef)person, kABPersonEmailProperty);
NSInteger count = ABMultiValueGetCount(emails);
CFRelease(emails);
return count > 0;
}];
NSArray *peopleWithEmails = [allPeople filteredArrayUsingPredicate:predicate];
// You now have an array of `ABRecordRef` associated with
// those contacts with email addresses. You can use this as
// the model backing your own table view, or populate your
// own model.
} else {
NSLog(#"Access not granted");
if (error) {
CFRelease(error);
}
}
});
Having done that, you can then build your own tableview listing the relevant details from that peopleWithEmails.
I do not believe there is a way to get iOS to do this filtering. I do it in code too. Note that you need to look for all kinds of email addresses - you have to iterate through the dictionary that you can get. Working with this is a PITA for sure - I've done it before - and you have to be careful to not have memory leaks.
What I do is as you suggest - iterate through all contact myself, then I pop a view with a table and let then select the names of the people they want. I keep an association around so I know what address is associated with what name, then use the system email framework and then populate the send-to addresses.
I'm in the midst of writing a data import routine for an iPad application. It seems to be working fine. However, because I'm an IOS noob and coming from an application development background in which such tasks are handled via calls to an SQL server, I'm feeling the need to double-check if I'm using the correct approach.
Here is the basic approach I'm using to import data into one of my master data entities:
// SET UP SOME VARIABLES/VALUES APPLICABLE TO ALL IMPORTS
NSStringEncoding encoding = 1;
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
NSString* file = nil;
NSError* error = nil;
NSArray* records = nil;
NSArray* fields = nil;
NSPredicate* predicate = nil;
NSArray* filteredArray = nil;
// IMPORT GEO DATA
// ProvinceState
file = [[NSBundle bundleForClass:[self class]] pathForResource:#"ProvinceState" ofType:#"txt"];
error = nil;
records = [NSArray arrayWithContentsOfCSVFile:file usedEncoding:&encoding error:&error];
NSManagedObject* provinceState = nil;
NSMutableArray* provinceStateArray = [NSMutableArray arrayWithCapacity:[records count]];
for (int i = 0; i < [records count]; ++i)
{
fields = [records objectAtIndex:i];
if ([fields count] == 2)
{
provinceState = [NSEntityDescription insertNewObjectForEntityForName:#"ProvinceState" inManagedObjectContext:self.managedObjectContext];
[provinceState setValue: [numberFormatter numberFromString:[fields objectAtIndex:0]] forKey:#"id"];
[provinceState setValue:[fields objectAtIndex:1] forKey:#"name"];
[provinceStateArray addObject:provinceState];
}
}
A couple of key points. This particular entity is very simple - just an integer id and a string name. Its source table is a simple csv file. The method arrayWithContentsOfCSVFile:usedEncoding:error: is from Dave Delong's excellent CHCSV parser, which makes parsing a breeze. I have not yet applied any error handling because I'm trying to work out the basic techniques first.
Okay, so unless you spot anything wrong with this simple code, I'll now get on with my main questions. The provinceStateArray referenced in the last meaningful line of the above code is to save myself the hassle of having to query the master data entities later on, when I need to populate them into the relationships of my transactional data. I realize this a) increases my memory requirements and b) prevents me, by implication, from draining the associated autoreleasepool, but since the half-dozen or so master data entities contain only a few hundred small objects in total, I didn't think this was a concern. Anyone think differently?
Now the part that does concern me. In the following code, which populates a Locations entity, I'm using these same master data entity arrays as follows:
file = [[NSBundle bundleForClass:[self class]] pathForResource:#"Location" ofType:#"txt"];
error = nil;
records = [NSArray arrayWithContentsOfCSVFile:file usedEncoding:&encoding error:&error];
NSManagedObject* location = nil;
NSMutableArray* locationArray = [NSMutableArray arrayWithCapacity:[records count]];
for (int i = 0; i < [records count]; ++i)
{
NSArray* fields = [records objectAtIndex:i];
if ([fields count] == 5)
{
location = [NSEntityDescription insertNewObjectForEntityForName:#"Location" inManagedObjectContext:self.managedObjectContext];
[location setValue: [numberFormatter numberFromString:[fields objectAtIndex:0]] forKey:#"id"];
predicate = [NSPredicate predicateWithFormat:#"id = %#", [numberFormatter numberFromString:[fields objectAtIndex:1]]];
filteredArray = [provinceStateArray filteredArrayUsingPredicate:predicate];
if ([filteredArray count] > 0)
{
[location setValue:[filteredArray objectAtIndex:0] forKey:#"provinceState"];
}
...and so on down through the other relationship entities that must be set.
This is the part - having to construct large quantities of objects simply to set the relationships - that feels foreign to me, even though I don't see any alternative.
Also, because the source data is from an RDBMS, I'm populating the one side of one-to-many relationships instead of iterating through sets.
Anything smell bad in all of this, or can I carry on with some comfort that I understand?
I think it is better to create SQLite database, import it to your project and import data from a file to that database(it is hard drive memory), not to arrays in RAM. Then, when your database is ready for use, make queries to it to populate only needed data for your user to iPhone RAM.