Reputation: 399
In my iOS application I have a ViewController which displays a UITableView
. Everything (delegate, datasource...) has been set correctly. I want to do a common thing: populate the TableView with the contacts in the phone.
However for some reason the TableView is always empty, while I successfully get the contacts in viewDidLoad. I noticed that the method tableView:numberOfRowsInSection: is called before I get my contact list but I don't know what's the issue. Here is my Controller.m:
@interface Controller () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSMutableArray *contacts;
@property(nonatomic, strong) IBOutlet UITableView *contactsTable;
@end
@implementation Controller
- (void)viewDidLoad {
[super viewDidLoad];
self.contacts = [[NSMutableArray alloc] init];
ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
if (status == kABAuthorizationStatusDenied || status == kABAuthorizationStatusRestricted) {
// if you got here, user had previously denied/revoked permission for your
// app to access the contacts, and all you can do is handle this gracefully,
// perhaps telling the user that they have to go to settings to grant access
// to contacts
[[[UIAlertView alloc] initWithTitle:nil message:@"This app requires access to your contacts to function properly. Please visit to the \"Privacy\" section in the iPhone Settings app." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
return;
}
CFErrorRef error = NULL;
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
if (!addressBook) {
NSLog(@"ABAddressBookCreateWithOptions error: %@", CFBridgingRelease(error));
return;
}
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
if (error) {
NSLog(@"ABAddressBookRequestAccessWithCompletion error: %@", CFBridgingRelease(error));
}
if (granted) {
// if they gave you permission, then just carry on
[self listPeopleInAddressBook:addressBook];
} else {
// however, if they didn't give you permission, handle it gracefully, for example...
dispatch_async(dispatch_get_main_queue(), ^{
// BTW, this is not on the main thread, so dispatch UI updates back to the main queue
[[[UIAlertView alloc] initWithTitle:nil message:@"This app requires access to your contacts to function properly. Please visit to the \"Privacy\" section in the iPhone Settings app." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
});
}
CFRelease(addressBook);
});
}
- (void)listPeopleInAddressBook:(ABAddressBookRef)addressBook
{
NSArray *allPeople = CFBridgingRelease(ABAddressBookCopyArrayOfAllPeople(addressBook));
NSInteger numberOfPeople = [allPeople count];
for (NSInteger i = 0; i < numberOfPeople; i++) {
ABRecordRef person = (__bridge ABRecordRef)allPeople[i];
NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
NSString *fullname = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
CFIndex numberOfPhoneNumbers = ABMultiValueGetCount(phoneNumbers);
for (CFIndex i = 0; i < numberOfPhoneNumbers; i++) {
NSString *phoneNumber = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phoneNumbers, i));
NSLog(@" phone:%@", phoneNumber);
GIContact *contact = [[GIContact alloc] initWithFullName:fullname telephone:phoneNumber];
[self.contacts addObject:contact];
}
CFRelease(phoneNumbers);
}
}
//Called before I got the contacts
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [self.contacts count]; //This works when I replace it with a number like 2
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Create an instance of UITableViewCell, with default appearance
UITableViewCell *cell =
[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"ContactTableCell"];
// Set the text on the cell with the description of the item
// that is at the nth index of items, where n = row this cell
// will appear in on the tableview
GIContact *contact = self.contacts[indexPath.row];
cell.textLabel.text = contact.fullname;
return cell;
}
@end
Contact is a simple model class with two NSString (fullname and telephone)
Upvotes: 2
Views: 137
Reputation: 2994
The method ABAddressBookRequestAccessWithCompletion
does its work off the main thread. A hint for that, is the trailing Closure
which is invoked as callback, as soon as the method finished doing what it does.
So when (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection: (NSInteger)section
is called, its still working off the main thread or the data hasn't 'reached' the main thread yet.
If you want to populate the data as soon as you've finished fetching the address book request you should reload the UITableView
on the main thread:
- (void)listPeopleInAddressBook:(ABAddressBookRef)addressBook
{
NSArray *allPeople = CFBridgingRelease(ABAddressBookCopyArrayOfAllPeople(addressBook));
NSInteger numberOfPeople = [allPeople count];
for (NSInteger i = 0; i < numberOfPeople; i++) {
ABRecordRef person = (__bridge ABRecordRef)allPeople[i];
NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
NSString *fullname = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
CFIndex numberOfPhoneNumbers = ABMultiValueGetCount(phoneNumbers);
for (CFIndex i = 0; i < numberOfPhoneNumbers; i++) {
NSString *phoneNumber = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phoneNumbers, i));
NSLog(@" phone:%@", phoneNumber);
GIContact *contact = [[GIContact alloc] initWithFullName:fullname telephone:phoneNumber];
[self.contacts addObject:contact];
}
CFRelease(phoneNumbers);
}
// reload the tableview on the main thread here
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
Upvotes: 1
Reputation: 3984
You get contact after tableView reloadData
method call.
- (void)listPeopleInAddressBook:(ABAddressBookRef)addressBook
{
NSArray *allPeople = CFBridgingRelease(ABAddressBookCopyArrayOfAllPeople(addressBook));
NSInteger numberOfPeople = [allPeople count];
for (NSInteger i = 0; i < numberOfPeople; i++) {
ABRecordRef person = (__bridge ABRecordRef)allPeople[i];
NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
NSString *fullname = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
ABMultiValueRef phoneNumbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
CFIndex numberOfPhoneNumbers = ABMultiValueGetCount(phoneNumbers);
for (CFIndex i = 0; i < numberOfPhoneNumbers; i++) {
NSString *phoneNumber = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phoneNumbers, i));
NSLog(@" phone:%@", phoneNumber);
GIContact *contact = [[GIContact alloc] initWithFullName:fullname telephone:phoneNumber];
[self.contacts addObject:contact];
}
CFRelease(phoneNumbers);
}
[self.tableView reloadData];// refresh table Data
}
Upvotes: 2
Reputation: 5760
After a successful connection and retrieving the contact list, refresh the table view. numberOfRowsInSection
is called because the view is immediately trying to display the data, which is not there yet.
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Upvotes: 3