iOS Address Book指南

时间:2021-01-11 21:35:02

尽管OC是一门面向对象的语言,但是在你做开发的时候你会发现,并不是所有你用的frameworks都是面向对象的。有些是用C写的,例如Address Book的API,接下来让我们去学习一下Address Book。

我们在我们的APP中可以Address Book API来读取或者修改用户联系人的信息(这和我们在手机通讯录上的效果是一样的)。

因为Address Book API是基于C语言的,它不是使用的对象,而且它也利用了一些其他的类型,在这里,你将会熟悉一下几个API:

  • ABRecordRef:它是一个联系记录,包括了所有的属性,例如手机,电话,电子邮件,姓,名等等。
  • ABAddresBookRef:它是所有用户联系人的集合,你可以对记录进行增加、修改和删除。
  • ABMutableMultiValueRef:它是ABMultiValueRef的可变类型(类似于NSDictionary的NSMutableDictionary),虽然它是方便的,但是它要求你设置ABRecordeRef属性的时候有多个实体,例如电话号码或者email.

既然读这个文章,那么就意味着你对iOS开发有一个基础的了解,而且熟悉C的基础语法。如果你没有满足刚才说的两个条件,可以先对iOS进行复习或者先了解C语言。

好了,让我们开始学习吧!

开始

首先,你可以先到这里下载这个界面程序,然后在这个基础上进行开发学习Address Book。(这个程序很简单,就是放了4个button,然后来了一个输出,用不同的tag标记不同按钮)

使用Address Book API,你需要导入头文件,导入方式如下:

@import AddressBook;

或者直接:

#import <AddressBook/AddressBook.h> 

在这个小Demo中,用户将可以点击任何一个图片,然后这个宠物联系人的信息就会存储到address Book里面。使用Address Book API,你可以联系到你的存储的朋友。

请求权限

在2012年,有一个争论:app是否可以复制用户的通讯录,然后将数据发送到自己的服务器。大众的响应肯定是不允许,就算是发送也要经过用户同意。所有Apple就诞生了一个新的特性:请求权限。防止用户在不知情的情况下自己的通讯录被APP盗取。

因此,现在如果你想使用Address Book,你首先要得到用户的允许。

让我们来尝试做一下,在ViewController.m中,添加如下代码到按钮点击事件:

- (IBAction)tapAction:(id)sender {
//iOS 8 and before
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted) {
//1
NSLog(@"Denied");
}else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
//2
NSLog(@"Authorized");
}else {
//3
NSLog(@"Not determined");
} // //iOS 9 and later
// CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
// if (status == CNAuthorizationStatusDenied || status == CNAuthorizationStatusRestricted) {
// NSLog(@"Denied");
// }else if (status == CNAuthorizationStatusAuthorized ) {
// NSLog(@"Authorized");
// }else {
// NSLog(@"Not determined");
// }
}

让我们来分析一下:

  1. 这个检查是用来检测用户是否拒绝了你的app访问手机通讯录,或者是它是受限制的(比如家长控制).如果用户拒绝了或者限制了,那么你只能告诉用户没有权限对通讯录进行操作,其他的什么也无法做。
  2. 这个检查是看看用户是否已经允许你的APP访问用户的通讯录,如果允许了,你可以随意地修改或者对通讯录进行其他操作。
  3. 这个检查是看用户是否还没有确定你的APP具有访问通讯录权限。

输出结果如下:

2016-09-13 16:46:35.513 ABContractDemo[14369:395357] Not determined

和现实生活一样:你需要什么东西的时候,你需要询问。

因此,你需要请求用户获取访问权限,在3的地方写如下代码:(这里就不在介绍iOS9)

        ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
if (granted) {
NSLog(@"Just authorized");
}else {
NSLog(@"Just deieny");
}
});

这里面第一个参数是ABAddressBookRef,你使用ABAddressBookCreateWithOptions(NULL,nil)。第二个参数是一个block:一旦用户点击了授权按钮,便会调用里面的东西。

这次运行的结果就是:

iOS Address Book指南

当你点击了Don't Allow,就表明iDenied了,如果ok就表示你允许了APP访问通讯录。

创建记录

现在,让我们开始去创建通讯录记录。我们现在清空按钮点击事件,然后重写它。在重写的这个方法里面,你需要创建一个ABRecordRef,他包括了宠物的属性,检查一下通讯录确保不存在你添加的联系人,如果宠物不在通讯录,就把他加入到通讯录。

在tapAction:(id)sender方法里面写入:

  NSString *petFirstName;
NSString *petLastName;
NSString *petphoneNumber;
NSData *petImageData;
if (sender.tag == ) {
petFirstName = @"Cheesy";
petLastName = @"Cat";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Cheesy.jpg"], 0.7f);
}else if (sender.tag == ) {
petFirstName = @"Freckles";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Freckles.jpg"], 0.7f);
}else if (sender.tag == ) {
petFirstName = @"Maxi";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Maxi.jpg"], 0.7f);
}else if(sender.tag == ) {
petFirstName = @"Shippo";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Shippo.jpg"], 0.7f);
}

通过点击不同的按钮,可以确定点击的是哪个宠物。接下来,写如下代码

    ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, nil);
ABRecordRef pet = ABPersonCreate();

第一行是创建一个ABAddressBookRef,它稍后用户将pet加到用户的通讯录中。第二行是为你的创建了一个空的记录,用来填充宠物的信息。

接下来,设置宠物的姓和名,代码如下:

 ABRecordSetValue(pet, kABPersonFirstNameProperty, (__bridge CFStringRef)(petFirstName), nil);
ABRecordSetValue(pet, kABPersonLastNameProperty, (__bridge CFStringRef)(petLastName), nil);

简单的介绍:

  • ABRecordSetValue()把ABREcordRef作为第一个参数,它的记录是pet
  • 第二个参数是ABPropertyID,这个是API定义的,因为你想设置姓,所以传入kABPersonFirstNameProperty
  • 对于名,类似地传入kABPersonLastNameProperty

第三个参数看起来困惑吗?它是一个CFTypeRef,该类型包括了CFStringRef和ABMultiValueRef,你需要传递CFStringRef,但是你之后NSString。为了将NSString 转换成CFTypeRef,使用(__bridge CFStringRef) myString。

手机号的稍微复杂一点,因为一个联系人可以有多个手机号(家庭,手机,等等),因此这个必须使用ABMutableMultiValueRef。这些可以通过下面的代码完成,(在上面代码后面继续添加):

    ABMutableMultiValueRef phoneNumbers = ABMultiValueCreateMutable(kABMultiStringPropertyType);
ABMultiValueAddValueAndLabel(phoneNumbers, (__bridge CFTypeRef)(petphoneNumber), kABPersonPhoneMainLabel, NULL);

当你声明ABMutableMultiValueRef,你必须说明是什么属性。在这里面,你想它是kABPersonPhoneProperty。第二行是添加pet's Phone number,这里注意你必须给这个号码一个label.这个label kABPersonPhoneMainLabel 说明这个号码是用户最主要的号码。然后是添加照片:

ABPersonSetImageData(pet, (__bridge CFDataRef)petImageData, nil);

最后是将联系人的信息保存到通讯录里面:

ABAddressBookAddRecord(addressBookRef, pet, nil);
ABAddressBookSave(addressBookRef, nil);

接下来运行,然后点击每个按钮,就可以将内容存储到自己本机的通讯录里面了。

但是你会发现一个问题,如果一致点击某个按钮,那么这个宠物的信息就会一直往通讯录里面添加。为了避免复制,你应该循环访问所有的通讯录信息确保新的通讯录记录名字不在通讯录里面。

插入以下代码到ABAddressBookAddRecord() 。首先,添加这一行:

NSArray *allContracts = (__bridge NSArray *)(ABAddressBookCopyArrayOfAllPeople(addressBookRef));

这里可以注意到:你可以使用__bridge将对象在Core Foundation对象转换成Foundation,也可以将Foundation转成Core Foundation。

然后,添加以下代码:

    for (id record in allContracts) {
ABRecordRef thisContract = (__bridge ABRecordRef)(record);
if (CFStringCompare(ABRecordCopyCompositeName(thisContract), ABRecordCopyCompositeName(pet), ) == kCFCompareEqualTo) {
//用户已经存在
NSLog(@"用户已经存在");
break; }
}

你必须使用id,因为从技术上来讲,Core Foundation类型是不能被转换成NSArray的,因为他们不是对象。ABRecordRefs被伪装成id来避免出错。所以为了得到ABRecordRef,还需要使用再次使用__bridge。

使用CFStringCompare的方式类似于NSString的isEqualToString。ABRecordCopyCompositeName得到了全名,它是联系人姓和名的组合。这样就可以用来防止重复记录了。

多线程

截止到这里上面的整体代码如下:

- (IBAction)tapAction:(UIButton *)sender {
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted) {
//
NSLog(@"Denied");
}else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
//
NSLog(@"Authorized");
}else {
//
ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
if (granted) {
NSLog(@"Just authorized");
}else {
NSLog(@"Just deieny");
}
});
} NSString *petFirstName;
NSString *petLastName;
NSString *petphoneNumber;
NSData *petImageData;
if (sender.tag == ) {
petFirstName = @"Cheesy";
petLastName = @"Cat";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Cheesy.jpg"], 0.7f);
}else if (sender.tag == ) {
petFirstName = @"Freckles";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Freckles.jpg"], 0.7f);
}else if (sender.tag == ) {
petFirstName = @"Maxi";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Maxi.jpg"], 0.7f);
}else if(sender.tag == ) {
petFirstName = @"Shippo";
petLastName = @"Dog";
petphoneNumber = @"";
petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Shippo.jpg"], 0.7f);
}
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, nil); //通讯录
ABRecordRef pet = ABPersonCreate(); //一条记录 //设置姓名
ABRecordSetValue(pet, kABPersonFirstNameProperty, (__bridge CFStringRef)(petFirstName), nil);
ABRecordSetValue(pet, kABPersonLastNameProperty, (__bridge CFStringRef)(petLastName), nil); //设置手机号
ABMutableMultiValueRef phoneNumbers = ABMultiValueCreateMutable(kABMultiStringPropertyType);
ABMultiValueAddValueAndLabel(phoneNumbers, (__bridge CFTypeRef)(petphoneNumber), kABPersonPhoneMainLabel, NULL);
ABRecordSetValue(pet, kABPersonPhoneProperty, phoneNumbers, nil); //设置照片
CFErrorRef *error;
ABPersonSetImageData(pet, (__bridge CFDataRef)petImageData, error); //获取所有联系人
NSArray *allContracts = (__bridge NSArray *)(ABAddressBookCopyArrayOfAllPeople(addressBookRef));
for (id record in allContracts) {
ABRecordRef thisContract = (__bridge ABRecordRef)(record);
if (CFStringCompare(ABRecordCopyCompositeName(thisContract), ABRecordCopyCompositeName(pet), ) == kCFCompareEqualTo) {
//用户已经存在
NSLog(@"用户已经存在");
break; }
} ABAddressBookAddRecord(addressBookRef, pet, nil);
ABAddressBookSave(addressBookRef, nil); // //iOS 8 and before
// if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted) {
// NSLog(@"Denied");
// }else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
// NSLog(@"Authorized");
// }else {
// ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
// if (granted) {
// NSLog(@"Just authorized");
// }else {
// NSLog(@"Just deieny");
// }
// });
// }
//
//// //iOS 9 and later
//// CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
//// if (status == CNAuthorizationStatusDenied || status == CNAuthorizationStatusRestricted) {
//// NSLog(@"Denied");
//// }else if (status == CNAuthorizationStatusAuthorized ) {
//// NSLog(@"Authorized");
//// }else {
//// NSLog(@"Not determined");
//// }
}

这里还有个隐藏的问题,如果你看了ABAddressBookRequestAccessWithCompletion的官方文档,刚才的点击事件是在任意的队列上调用的。换句话说,也就是它执行可能在其他的线程上,不一定在主线程。

这里面你必须要知道:用户图形界面展示只能在主线程上。你必须确保任何影响用户图像化界面显示的代码都要在主线程上调用。

使用下面的代码可以很容易的完成。在ABAddressBookRequestWithCompletion之前使用:

    dispatch_async(dispatch_get_main_queue(), ^{
<#code#>
});

这个是在主线程上执行,可以使用用户图形化展示。如果想学习更多,可以阅读这里

然后使用上述block块进行如下操作:

ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!granted){
//
UIAlertView *cantAddContactAlert = [[UIAlertView alloc] initWithTitle: @"Cannot Add Contact" message: @"You must give the app permission to add the contact first." delegate:nil cancelButtonTitle: @"OK" otherButtonTitles: nil];
[cantAddContactAlert show];
return;
}
//5
//添加通讯录操作
});
});

这是最好的方法去请求用户获取通讯录权限,最好的实践就是在你真正用到的时候才去请求权限。如果你在启动的时候就请求用户权限,用户就会怀疑,因为用户不知道你为什么要用到通讯录。

还有一个问题就是关于ABAddressBookRequestAccessWithCompletion,如果用户给了APP权限,有的时候需要有5-10s的延迟,直到回调被调用。这看起来好像当我们在添加通讯录记录的饿时候程序是卡的状态。在大多数情况下,这种问题并不常见。