Apple把通讯录存放在SQLite 3中,但是应用之间不能直接访问,也就是说我们自己编写的应用不能采用数据持久化技术直接访问通讯录数据库。为了实现通讯录数据库的访问,Apple开放了一些专门的API。出于安全考虑,iOS 6之后的应用访问通讯录,需要获取用户授权。与其他应用不同的是,通讯录对一个应用只授权一次,即便是这个应用删除后重装,也不必再次授权。


Apple为开发者提供了两个框架(AddressBook和AddressBookUI)来开发通讯录应用。


AddressBook框架提供了直接访问通讯录中记录和属性等的API,使用这些API需要自己构建界面。

下面看一下AddressBook框架中常用的类:

1、ABAddressBook,封装访问通讯录接口。Core Foundation框架中对应的类是ABAddressBookRef。

2、ABPerson,封装通讯录个人信息数据,是数据库的一条记录。Core Foundation框架中对应的类是ABPersonRef。

3、ABGroup,封装通讯录组信息数据,一个组包含了多个人的信息,一个人也可以隶属于多个组。Core Foundation框架中对应的类型是ABGroupRef。

4、ABRecord,封装了数据库的一条记录,记录由属性组成。Core Foundation框架中对应的类型是ABRecordRef。



AddressBookUI框架提供了4个视图控制器和4个对应的委托协议,它们已经提供了UI界面,不需要我们自己构建界面。

下面是AddressBookUI框架中的视图控制器:

1、ABPeoplePickerNavigationController,它是从数据库中选取联系人的导航控制器,对应的委托协议为ABPeoplePickerNavigationControllerDelegate。

2、ABPersonViewController,查看并编辑单个联系人信息,对应的委托协议为ABPersonViewControllerDelegate。

3、ABNewPersonViewController,创建新联系人信息,对应的委托协议为ABNewPersonViewControllerDelegate。

4、ABUnknownPersonViewController,呈现记录部分信息,这些信息可以创建新联系人信息,或添加到已经存在的联系人,对应的委托协议为ABUnknownPersonViewControllerDelegate。


接下来用AddressBook写一个简单的联系人操作的应用。主界面是联系人列表,选择一个联系人后,跳转到联系人详细信息界面,在详细信息界面可以修改联系人信息,也可以删除当前联系人。在主界面右上角有个添加联系人按钮,通过此入口可以新加联系人。


Storyboard截图:

iOS 通讯录授权失败可以获取联系人信息吗_AddressBookUI


ViewController.h

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>

@interface ViewController : UITableViewController<UISearchBarDelegate,UISearchDisplayDelegate>

@property(strong,nonatomic) NSMutableArray *contactList;
@property (weak, nonatomic) IBOutlet UISearchBar *searchBar;


-(void)queryContactsWithText:(NSString*)searchText;

@end






ViewController.m

#import "ViewController.h"
#import "DetailViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.title = @"Contacts";
    self.searchBar.delegate = self;
    
    CFErrorRef error;
	ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted,CFErrorRef error){
        if(granted){
            //query all
            [self queryContactsWithText:@""];
        }
    });
    CFRelease(addressBook);
}

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self queryContactsWithText:@""];
}

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.contactList.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString* cellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if(!cell){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    ABRecordRef person = CFBridgingRetain([self.contactList objectAtIndex:[indexPath row]]);
    NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
    firstName = firstName == nil ? @"" : firstName;
    NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
    lastName = lastName == nil ? @"" : lastName;
    
    cell.textLabel.text = [NSString stringWithFormat:@"%@ %@",firstName,lastName];
    
//    NSString *compositeName = CFBridgingRelease(ABRecordCopyCompositeName(person));
//    cell.textLabel.text = compositeName;
    
    CFRelease(person);
    
    return cell;
}

-(void)queryContactsWithText:(NSString *)searchText
{
    if(ABAddressBookGetAuthorizationStatus()!= kABAuthorizationStatusAuthorized){
        return ;
    }
    CFErrorRef error ;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    if(searchText == nil || [searchText isEqualToString:@""]){
        self.contactList = CFBridgingRelease(ABAddressBookCopyArrayOfAllPeople(addressBook));
    }else{
        CFStringRef queryText = CFBridgingRetain(searchText);
        self.contactList = CFBridgingRelease(ABAddressBookCopyPeopleWithName(addressBook, queryText));
        CFRelease(queryText);
    }
    CFRelease(addressBook);
    [self.tableView reloadData];
}

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if([[segue identifier] isEqualToString:@"showDetail"]){
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        ABRecordRef recordRef = CFBridgingRetain([self.contactList objectAtIndex:indexPath.row]);
        DetailViewController *detailViewController = segue.destinationViewController;
        
        ABRecordID recordID = ABRecordGetRecordID(recordRef);
        detailViewController.recordID = [NSNumber numberWithInt:recordID];
        CFRelease(recordRef);
    }
}

-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
    [searchBar resignFirstResponder];
    [self queryContactsWithText:@""];
}

-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self queryContactsWithText:searchString];
    return YES;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end








DetailViewController.h

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>

@interface DetailViewController : UITableViewController<UIAlertViewDelegate,UITextFieldDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *contactImg;
@property (weak, nonatomic) IBOutlet UILabel *contactName;
@property (weak, nonatomic) IBOutlet UITextField *mobilePhone;
@property (weak, nonatomic) IBOutlet UITextField *iPhone;
@property (weak, nonatomic) IBOutlet UITextField *homeEmail;
@property (weak, nonatomic) IBOutlet UITextField *workEmail;
@property (strong,nonatomic) NSNumber *recordID;

- (IBAction)save:(id)sender;
- (IBAction)deleteContact:(id)sender;

-(void)confirmDeleteContact;

@end





DetailViewController.m

#import "DetailViewController.h"

@interface DetailViewController ()

@end

@implementation DetailViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.mobilePhone.delegate = self;
    self.iPhone.delegate = self;
    self.homeEmail.delegate = self;
    self.workEmail.delegate = self;
	
    [self displayRecord];
        
}

-(void)displayRecord
{
    CFErrorRef error = NULL;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    
    ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressBook, [self.recordID intValue]);
    
    //    NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
    //    firstName = firstName != nil ? firstName : @"";
    //    NSString *lastName =  CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
    //    lastName = lastName != nil ? lastName : @"";
    //    [self.contactName setText: [NSString stringWithFormat:@"%@ %@",firstName,lastName]];
    NSString* name = CFBridgingRelease(ABRecordCopyCompositeName(person));
    [self.contactName setText: name];
    
    ABMultiValueRef emailsProperty = ABRecordCopyValue(person, kABPersonEmailProperty);
    NSArray* emailsArray = CFBridgingRelease(ABMultiValueCopyArrayOfAllValues(emailsProperty));
    for(int index = 0; index< [emailsArray count]; index++){
        NSString *email = [emailsArray objectAtIndex:index];
        NSString *emailLabel = CFBridgingRelease(ABMultiValueCopyLabelAtIndex(emailsProperty, index));
        if ([emailLabel isEqualToString:(NSString*)kABWorkLabel]) {
            [self.workEmail setText:email];
        } else if ([emailLabel isEqualToString:(NSString*)kABHomeLabel]) {
            [self.homeEmail setText:email];
        }
    }
    CFRelease(emailsProperty);
    
    ABMultiValueRef phoneNumberProperty = ABRecordCopyValue(person, kABPersonPhoneProperty);
    NSArray* phoneNumberArray = CFBridgingRelease(ABMultiValueCopyArrayOfAllValues(phoneNumberProperty));
    for(int index = 0; index< [phoneNumberArray count]; index++){
        NSString *phoneNumber = [phoneNumberArray objectAtIndex:index];
        NSString *phoneNumberLabel = CFBridgingRelease(ABMultiValueCopyLabelAtIndex(phoneNumberProperty, index));
        if ([phoneNumberLabel isEqualToString:(NSString*)kABPersonPhoneMobileLabel]) {
            [self.mobilePhone setText:phoneNumber];
        } else if ([phoneNumberLabel isEqualToString:(NSString*)kABPersonPhoneIPhoneLabel]) {
            [self.iPhone setText:phoneNumber];
        }
    }
    CFRelease(phoneNumberProperty);
    
    if (ABPersonHasImageData(person)) {
        NSData *photoData = CFBridgingRelease(ABPersonCopyImageData(person));
        if(photoData){
            [self.contactImg setImage:[UIImage imageWithData:photoData]];
        }
    }
    
    CFRelease(addressBook);

}

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    switch (buttonIndex) {
        case 0://cancel
            break;
            
        case 1://confirm to delete
            [self confirmDeleteContact];
            [self.navigationController popToRootViewControllerAnimated:YES];
            break;
    }
    
}

-(void)confirmDeleteContact
{
    CFErrorRef error = NULL;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressBook, [self.recordID intValue]);
    ABAddressBookRemoveRecord(addressBook, person, &error);
    ABAddressBookSave(addressBook, &error);
    CFRelease(addressBook);
}

- (IBAction)save:(id)sender {
    CFErrorRef error = NULL;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    
    ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressBook, self.recordID.intValue);
    
    // PHONE
    ABMutableMultiValueRef multi = ABMultiValueCreateMutable(kABMultiStringPropertyType);
    ABMultiValueAddValueAndLabel(multi,  (__bridge CFTypeRef)self.mobilePhone.text,kABPersonPhoneMobileLabel, NULL);
    ABMultiValueAddValueAndLabel(multi,  (__bridge CFTypeRef)self.iPhone.text,kABPersonPhoneIPhoneLabel, NULL);
    
    // SAVE PHONE TO RECORD
    ABRecordSetValue(person, kABPersonPhoneProperty, multi, &error);
    CFRelease(multi);
    
    // EMAIL
    multi = ABMultiValueCreateMutable(kABMultiStringPropertyType);
    ABMultiValueAddValueAndLabel(multi,  (__bridge CFTypeRef)self.homeEmail.text,kABHomeLabel, NULL);
    ABMultiValueAddValueAndLabel(multi,  (__bridge CFTypeRef)self.workEmail.text,kABWorkLabel, NULL);
    
    // SAVE EMAIL TO RECORD
    ABRecordSetValue(person, kABPersonEmailProperty, multi, &error);
    CFRelease(multi);
    
    // SAVE
    ABAddressBookSave(addressBook, &error);
    CFRelease(addressBook);
    
    [self.navigationController popToRootViewControllerAnimated:YES];
}

- (IBAction)deleteContact:(id)sender {
    UIAlertView *alertView =[[UIAlertView alloc] initWithTitle:@"Delete Contact" message:@"Are you sure to delete ?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Confirm", nil];
    [alertView show];
}

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end





AddViewController.h

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>

@interface AddViewController : UITableViewController<UITextFieldDelegate>
@property (weak, nonatomic) IBOutlet UITextField *firstName;
@property (weak, nonatomic) IBOutlet UITextField *lastName;
@property (weak, nonatomic) IBOutlet UITextField *mobile;
@property (weak, nonatomic) IBOutlet UITextField *iPhone;
@property (weak, nonatomic) IBOutlet UITextField *workEmail;
@property (weak, nonatomic) IBOutlet UITextField *homeEmail;

- (IBAction)add:(id)sender;

- (IBAction)cancel:(id)sender;

@end





AddViewController.m

#import "AddViewController.h"

@interface AddViewController ()

@end

@implementation AddViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.firstName.delegate = self;
    self.lastName.delegate = self;
    self.mobile.delegate = self;
    self.iPhone.delegate = self;
    self.workEmail.delegate = self;
    self.homeEmail.delegate = self;
}

- (IBAction)add:(id)sender
{
    if(![self checkName]){
        return;
    }
    CFErrorRef error;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &error);
    
    ABRecordRef person = ABPersonCreate();
    
    //NAME
    ABRecordSetValue(person, kABPersonFirstNameProperty, (__bridge CFTypeRef)self.firstName.text, &error);
    ABRecordSetValue(person, kABPersonLastNameProperty, (__bridge CFTypeRef)self.lastName.text, &error);
    
    //PHONE
    ABMutableMultiValueRef multiValue = ABMultiValueCreateMutable(kABMultiStringPropertyType);
    ABMultiValueAddValueAndLabel(multiValue, (__bridge CFTypeRef)self.mobile.text,kABPersonPhoneMobileLabel, NULL);
    ABMultiValueAddValueAndLabel(multiValue, (__bridge CFTypeRef)self.iPhone.text,kABPersonPhoneIPhoneLabel, NULL);
    //SET PHONE
    ABRecordSetValue(person, kABPersonPhoneProperty, multiValue, &error);
    
    CFRelease(multiValue);
    
    //EMAIL
    multiValue = ABMultiValueCreateMutable(kABMultiStringPropertyType);
    ABMultiValueAddValueAndLabel(multiValue, (__bridge CFTypeRef)self.homeEmail.text,kABHomeLabel, NULL);
    ABMultiValueAddValueAndLabel(multiValue, (__bridge CFTypeRef)self.workEmail.text,kABWorkLabel, NULL);
    //SET EMAIL
    ABRecordSetValue(person, kABPersonEmailProperty, multiValue, &error);
    
    CFRelease(multiValue);

    //INSERT RECORD TO DB
    ABAddressBookAddRecord(addressBook, person, &error);
    
    //SAVE
    ABAddressBookSave(addressBook, &error);
    
    CFRelease(person);
    CFRelease(addressBook);
    
    [self dismissViewControllerAnimated:YES completion:nil];
}

-(BOOL)checkName
{
    if([self.firstName.text isEqualToString:@""] && [self.firstName.text isEqualToString:@""]){
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"输入错误" message:@"姓名必须非空!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
        [alertView show];
        return NO;
    }
    return YES;
}

- (IBAction)cancel:(id)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end





这里需要注意,Core Foundation框架的类型与Foundation框架的类型之间需要经过转化。我们有两种方式进行转化:简单强制转化、对象所有权转化。前者是指用__bridge进行转化,这只是简单的转化,对象的所有权并没有发生改变;后者是指通过函数CFBridgingRetain(Foundation-->Core Foundation)和CFBridgingRelease(Core Foundation-->Foundation)进行转化,这种方式转化后对象所有权发生会发生改变。


对于Core Foundation的类型对象,我们需要用CFRelease将其释放。


下面简单总结下联系人操作:


1、查询联系人

从通讯录数据库中查询联系人,只能通过ABAddressBookCopyArrayOfAllPeople和ABAddressBookCopyPeopleWithName函数获得:


CFArrayRef  ABAddressBookCopyArrayOfAllPeople(ABAddressBookRef  addressBook);

CFArrayRef  ABAddressBookCopyPeopleWithName(ABAddressBookRef  addressBook, CFStringRef  name);


前者用于查询所有联系人记录,后者通过人名查询通讯录中的联系人。两个函数都需要addressBook参数,这是我们要查询的通讯录对象,我们可以用ABAddressBookCreateWithOptions函数(iOS 6之前是ABAddressBookCreate)创建:

ABAddressBookRef  ABAddressBookCreateWithOptions(CFDictionaryRef  options, CFErrorRef*  error);

options是保留参数,目前没有使用,使用时直接传NULL;error是错误对象,包含错误信息。


2、读取记录属性

在一条联系人记录中,有很多属性。其中又有单值属性和多值属性之分,它们由一些常量定义。

(1)单值属性

单值属性的常量定义主要由以下这些:

kABPersonFirstNameProperty    名字

kABPersonLastNameProperty    姓氏

kABPersonMiddleNameProperty    中间名

kABPersonPrefixProperty    前缀

kABPersonSuffixProperty    后缀

kABPersonNicknameProperty    昵称

kABPersonFirstNamePhoneticProperty    名字汉语拼音或音标

kABPersonLastNamePhoneticProperty    姓氏汉语拼音或音标

kABPersonMiddleNamePhoneticProperty    中间名汉语拼音或音标

kABPersonOrganizationProperty    组织名

kABPersonJobTitleProperty    头衔

kABPersonDepartmentProperty    部门

kABPersonNoteProperty    备注

读取单值属性的函数是ABRecordCopyValue:CFTypeRef    ABRecordCopyValue(ABRecordRef  record, ABPropertyID  property)。其中record是记录对象,property是属性ID,就是上面列出的常量,返回值是CFTypeRef,它是Core Foundation类型的“范型”,可以代表任何的CoreFoundation类型。


(2)多值属性

多值属性是包含多个值的集合类型,如电话号码、Email等。多值属性的常量定义主要由以下这些:

kABPersonPhoneProperty    电话号码属性,kABMultiStringPropertyType类型的多值属性。

kABPersonEmailProperty    Email属性,kABMultiStringPropertyType类型的多值属性。

kABPersonURLProperty    URL属性,kABMultiStringPropertyType类型的多值属性。

kABPersonRelatedNamesProperty    亲属关系人属性,kABMultiStringPropertyType类型的多值属性。

kABPersonInstantMessageProperty    即时聊天属性,kABMultiDictionaryPropertyType类型的多值属性。

kABPersonSocialProfileProperty    社交账号属性,kABMultiDictionaryPropertyType类型的多值属性。


在多值属性中,包含了label、value和ID等部分,其中标签和值都是可以重复的,而ID是不能重复的。

多值属性访问方式与单值属性访问方式类似,都使用ABRecordCopyValue函数。不同的是,多值属性访问的返回值是ABMultiValueRef,然后使用ABMultiValueCopyArrayOfAllValues函数从ABMultiValueRef中获取CFArrayRef数组:

CFArrayRef  ABMultiValueCopyArrayOfAllValues(ABMultiValueRef  multiValue);


ABMultiValueCopyLabelAtIndex函数可以从ABMultiValueRef对象中返回标签:

CFStrigRef  ABMultiValueCopyLabelAtIndex(ABMultiValueRef  multiValue, CFIndex  index);其中index是查找标签的索引。


3、读取图片属性

通讯录中的联系人可以有一张照片,读取联系人照片的相关函数有ABPersonCopyImageData和ABPersonHasImageData等。前者可以获取联系人照片:CFDataRef  ABPersonCopyImageData(ABRecordRef  person);返回值是CFDataRef,与之对应的Foundation框架类型是NSData*。后者用于判断联系人是否有照片:

bool  ABPersonHasImageData(ABRecordRef  person)。


4、创建联系人

上述AddViewController是添加联系人相关的实现,只要涉及到下面几个函数:

ABPersonCreate,创建联系人。

ABRecordSetValue,设置联系人的属性(包括单值和多值属性)。

ABMultiValueCreateMutable,创建可变多值类型(ABMutableMultiValueRef)。

ABMultiValueAddValueAndLabel,设置多值属性的值。

ABAddressBookAddRecord,增加记录到联系人数据库。

ABAddressBookSave,保存未保存的变更到联系人数据库(同样适用于删除和修改联系人)。


5、修改联系人

首先通过ID获取到联系人(ABAddressBookGetPersonWithRecordID),然后修改属性值,最后保存修改。


6、删除联系人

删除过程比较简单,首先通过ABAddressBookGetPersonWithRecordID函数获得联系人,然后调用ABAddressBookRemoveRecord函数删除联系人,最后保存修改。



上面说到的联系人操作都是用AddressBook框架的API,这个框架的API比较偏底层,用这个框架需要我们自己构建UI。一开始我们也提到了另一个高级框架AddressBookUI,这个框架为我们提供了UI,使用起来比AddressBook更方便。下一篇博客(iOS 通讯录访问(二))将举例介绍AddressBookUI的使用。