Contacts FrameWork是苹果推出的新联系人框架。应用于iOS 9.0之后,9.0之后将会全面取代Address Book 。用过Address Book的开发者都应该能理解苹果为何要弃用它了。一方面对于开发者实在不是很友好,语法怪怪的。另一方面,作者猜测可能是线程安全方面做得不够好,所以苹果打算有所改进。本文将阐述ContactsContactsUI的基本原理和基本用法。

Contacts

因为大部分的App都只是需要读取联系人信息而不需要做修改,因此Contacts只能读取,同时它的线程安全得到很好的保障。


联系人对象

这里写图片描述

Contact类是线程安全的,联系人的属性是不可以改变的,比如联系人的名称,图片,或者电话号码。这个类有点像NSDictionary,里面存了各种类型的东西。和MutableDictionary很类似,它有一个可变的子类CNMutableContact,可以修改联系人的属性。联系人的某些属性可以有多个值,如电话号码或电子邮件地址,它们都是包含了CNLabeledValue对象数组,CNLabeledValue是很有意思的一个类,我个人感觉里面主要还是使用了swift的新特性puple中文叫做元组。元组和数组类似,只不过里面数组的元素类型可以是不同的。一个CNLabeledValue对象其实就是包含了一个Label和一个Value的线程安全的不可变元组。每个Label描述了每个Value的使用者。比如电话号码,家庭号码和工作号码都是不同的Label。苹果已经提供了很多实用的标签,如果开发者觉得不够用也可以自己定义,我个人感觉够用了。


学习相关API

首先了解一下头文件中包含的相关类。头文件集中在<Contacts/Contacts.h>, 我们依次过一遍相关的API。

1
2
3
4
5
6
7
#import <Contacts/CNLabeledValue.h>
#import <Contacts/CNPhoneNumber.h>
#import <Contacts/CNPostalAddress.h>
#import <Contacts/CNMutablePostalAddress.h>
#import <Contacts/CNInstantMessageAddress.h>
#import <Contacts/CNSocialProfile.h>
#import <Contacts/CNContactRelation.h>

CNLabeledValue是一个泛型的类,可以使用如下方法直接返回一个CNLabeledValue类型

1
+ (instancetype)labeledValueWithLabel:(nullable NSString *)label value:(ValueType)value;

CNContact里面的属性都是以CNLabeledValue,或是CNLabeledValue数组的形式存在。如果实在难以理解CNLabeledValue到底是什么,可以把它当作一个字典,Label类似于key,Value类似于字典里面的Value。然后联系人属性都是存储字典或是字典数组。
CNPhoneNumber是电话号码类,主要包括了一个属性就是号码,至于到底是家庭号码还是工作号码 需要根据Label区分。
CNPostalAddressCNMutablePostalAddress是邮寄地址也就是联系人地址。
CNSocialProfile是社会化组件信息,比如FaceBook,微博这些
CNInstantMessageAddress是即时通讯信息,比如QQ这些。
CNContactRelation是联系人关系信息。
我们点其中任何一个类的头文件开会发现里面有很多类似如下的定义。

1
2
3
4
5
6
7
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberiPhone NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberMobile NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberMain NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberHomeFax NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberWorkFax NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberOtherFax NS_AVAILABLE(10_11, 9_0);
CONTACTS_EXTERN NSString * const CNLabelPhoneNumberPager

他们是用于操作这些信息的Label,或是Key,用到什么再去找具体的就好。

1
2
3
#import <Contacts/CNContact.h>
#import <Contacts/CNContact+Predicates.h>
#import <Contacts/CNMutableContact.h>

CNContactCNMutableContact不用多说,是系统用于存储联系人信息类。CNContact+Predicates是用于联系人过滤筛选的类。

1
2
3
#import <Contacts/CNContactStore.h>
#import <Contacts/CNContactFetchRequest.h>
#import <Contacts/CNSaveRequest.h>

看三个类是不是和CoreData里面操作数据库的类很像,这三个类主要就是用于联系人信息的增删改查等等相关操作。

1
2
3
4
5
6
#import <Contacts/CNGroup.h>
#import <Contacts/CNGroup+Predicates.h>
#import <Contacts/CNMutableGroup.h>
#import <Contacts/CNContainer.h>
#import <Contacts/CNContainer+Predicates.h>

分组和容器类。一个用户可能在本地或是服务器都有联系人,每个账户至少都有一个容器,每个联系人只可能在一个容器中。
这里写图片描述

组是容器里面的一个联系人集合,并不是所有的容器都支持组合子分组。比如iCloud账户只有一个容器不过有很多个组和分组。但是一个外汇账户不允许有很多个分组,不过会有很多容器。
这里写图片描述

1
2
#import <Contacts/CNContactFormatter.h>
#import <Contacts/CNPostalAddressFormatter.h>

以上两个类主要是用于格式化姓名电话号码和地址,比如可以格式化为全名、全部地址等。

1
2
3
4
#import <Contacts/CNContactVCardSerialization.h>
#import <Contacts/CNContactsUserDefaults.h>
#import <Contacts/CNContactProperty.h>
#import <Contacts/CNError.h>

CNContactVCardSerialization用于序列化电子名片,CNContactsUserDefaults用于用户默认的设置,CNContactProperty是获取单个属性。


实际运用

获取授权信息

在进行电话本相关操作的时候最好是先进行相关的权限判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CNContactStore * contactStore = [[CNContactStore alloc]init];
if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined) {
[contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * __nullable error) {
if (granted==YES) {
//执行相关操作
if ([self addContactInIOS9andLower]) {
NSLog(@"Error");
}
else{
NSLog(@"Error");
}
}
else{
NSLog(@"Error");
}
}];
}
else if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized){
//执行相关操作
}
else {
NSLog(@"Error");
}
}

Contact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 创建联系人对象
CNMutableContact *contact = [[CNMutableContact alloc] init];
// 设置联系人的头像
contact.imageData = UIImagePNGRepresentation([UIImage imageNamed:@"animal"]);
// 设置联系人姓名
contact.givenName = @"测试";
// 设置姓氏
contact.familyName = @"测试";
// 设置联系人邮箱
CNLabeledValue *homeEmail = [CNLabeledValue labeledValueWithLabel:CNLabelHome value:@"12345qq.com"];
CNLabeledValue *workEmail = [CNLabeledValue labeledValueWithLabel:CNLabelWork value:@"32467@sina.cn"];
CNLabeledValue *otherEmail = [CNLabeledValue labeledValueWithLabel:CNLabelOther value:@"l111.com"];
contact.emailAddresses = @[homeEmail,workEmail,otherEmail];
// 设置机构名
contact.organizationName = @"iOS";
// 设置部门
contact.departmentName = @"移动";
// 设置工作的名称
contact.jobTitle = @"ios";
// 设置社交组件
CNSocialProfile *profile = [[CNSocialProfile alloc] initWithUrlString:@"22222222.cn" username:@"Test" userIdentifier:nil service:@"IT行业"];
CNLabeledValue *socialProfile = [CNLabeledValue labeledValueWithLabel:CNSocialProfileServiceGameCenter value:profile];
contact.socialProfiles = @[socialProfile];
// 设置电话号码
CNPhoneNumber *mobileNumber = [[CNPhoneNumber alloc] initWithStringValue:@"15201596724"];
CNLabeledValue *mobilePhone = [[CNLabeledValue alloc] initWithLabel:CNLabelPhoneNumberMobile value:mobileNumber];
contact.phoneNumbers = @[mobilePhone];
// 设置与联系人的关系
CNContactRelation *friend = [[CNContactRelation alloc] initWithName:@"好朋友"];
CNLabeledValue *relation = [CNLabeledValue labeledValueWithLabel:CNLabelContactRelationFriend value:friend];
contact.contactRelations = @[relation];
// 设置生日
NSDateComponents *birthday = [[NSDateComponents alloc] init];
birthday.day = 6;
birthday.month = 5;
birthday.year = 2000;
contact.birthday = birthday;

联系人创建完毕,我们可以添加联系人

1
2
3
4
5
6
7
8
- (void)addContact:(CNMutableContact *)contact{
// 创建联系人请求
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest addContact:contact toContainerWithIdentifier:nil];
// 写入联系人
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

删除联系人

1
2
3
4
5
6
7
8
- (void)deleteContact:(CNMutableContact *)contact{
// 创建联系人请求
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest deleteContact:contact];
// 写入操作
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

更新联系人

1
2
3
4
5
6
7
8
- (void)updateContact:(CNMutableContact *)contact{
// 创建联系人请求
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest updateContact:contact];
// 重新写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

查询联系人

1
2
3
4
5
6
7
8
9
10
11
12
- (NSArray *)queryContactWithName:(NSString *)name{
CNContactStore *store = [[CNContactStore alloc] init];
//检索条件
NSPredicate *predicate = [CNContact predicateForContactsMatchingName:name];
//过滤的条件,也可以过滤时候格式化
NSArray *keysToFetch = @[CNContactEmailAddressesKey, [CNContactFormatter descriptorForRequiredKeysForStyle:CNContactFormatterStyleFullName]];
NSArray *contact = [store unifiedContactsMatchingPredicate:predicate keysToFetch:keysToFetch error:nil];
return contact;
}

keysToFetch是表明从这些属性里面过滤。[CNContactFormatter descriptorForRequiredKeysForStyle:CNContactFormatterStyleFullName]格式化姓名,输出全名作为过滤条件。

Group

添加一个Group

1
2
3
4
5
6
7
8
9
- (void)addGroupWithName:(NSString *)name{
CNMutableGroup *group = [[CNMutableGroup alloc] init];
group.name = name;
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest addGroup:group toContainerWithIdentifier:nil];
// 写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

更新Group

1
2
3
4
5
6
7
- (void)updateGroup:(CNMutableGroup *)group{
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest updateGroup:group];
// 写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

删除Group

1
2
3
4
5
6
7
- (void)deleteWithGroup:(CNMutableGroup *)group{
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest deleteGroup:group];
// 写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

group里面添加一个联系人

1
2
3
4
5
6
7
- (void)addMemberWithContact:(CNContact *)contact toGroup:(CNGroup *)group{
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest addMember:contact toGroup:group];
// 写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

从Group删除联系人

1
2
3
4
5
6
7
8
- (void)deleteMemberWithContact:(CNContact *)contact toGroup:(CNGroup *)group{
CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init];
[saveRequest removeMember:contact fromGroup:group];
// 写入
CNContactStore *store = [[CNContactStore alloc] init];
[store executeSaveRequest:saveRequest error:nil];
}

填加子分组和删除子分组

1
2
3
- (void)addSubgroup:(CNGroup *)subgroup toGroup:(CNGroup *)group NS_AVAILABLE(10_11, NA);
- (void)removeSubgroup:(CNGroup *)subgroup fromGroup:(CNGroup *)group NS_AVAILABLE(10_11, NA);

格式化

有的时候我们想要输出全名或是全部的地址,就可以使用格式化。

1
2
3
4
5
6
7
NSString *name = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStyleFullName];
NSLog(@"全名是---%@",name);
NSString *phoneName = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStylePhoneticFullName];
NSLog(@"号码全名是---%@",phoneName);
NSString *ad = [CNPostalAddressFormatter stringFromPostalAddress:address style:CNPostalAddressFormatterStyleMailingAddress];

本地化

有时候可能我不知道某个keyCNContactFamilyNameKey或是Label 在某个语言里面具体表达的是什么,就可以使用本地化方法来获取对应语言的字符串。

1
2
3
4
5
NSString *disPlayName = [CNContact localizedStringForKey:CNContactFamilyNameKey];
NSLog(@"CNContactFamilyNameKey---%@",disPlayName);
NSString *disPlay = [CNLabeledValue localizedStringForLabel:CNLabelURLAddressHomePage];
NSLog(@"CNLabelURLAddressHomePage是---%@",disPlay);

部分联系方式

不知我这么翻译准确不准确。苹果的原词是Partial Contacts。要表达的就是某个联系人可能只包含一部分属性,如果你要使用的属性不存在的话就会存在异常,所以在使用属性的时候最好先用isKeyAvailable或是areKeysAvailable去检查一下属性可用不可用。可用的话再使用,否则使用统一查询查询联系人的其他信息。

1
2
3
4
5
6
7
8
if ([contact isKeyAvailable:CNContactPhoneNumbersKey]) {
//使用contact
}else {
NSArray *keyFetch = @[CNContactGivenNameKey,CNContactFamilyNameKey];
CNContactStore *store = [[CNContactStore alloc] init];
CNContact *refetchedContact = [store unifiedContactWithIdentifier:contact.identifier keysToFetch:keyFetch error:nil];
}

这里写图片描述


ContactsUI

主要就是实现 CNContactPickerDelegate和数据的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker{
NSLog(@"取消");
}
/**
* 选中联系人时执行该方法
*
* @param picker 联系人控制器
* @param contact 联系人
*/
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact{
NSLog(@"联系人的资料:%@",contact);
NSString *fullName = contact.familyName;
NSArray<CNLabeledValue *> *phoneArray = contact.phoneNumbers;
for (CNLabeledValue *label in phoneArray) {
NSString *phone = label.label;
CNPhoneNumber *phoneNum = label.value;
NSString *code = phoneNum.stringValue;
if ([label.label isEqualToString:CNLabelPhoneNumberMobile]) {
NSLog(@"%@手机号码是phone:%@---%@",fullName,phone,code);
}
NSLog(@"%@号码是phone:%@---%@",fullName,phone,code);
}
NSString *name = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStyleFullName];
NSLog(@"全名是---%@",name);
NSString *phoneName = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStylePhoneticFullName];
NSLog(@"号码全名是---%@",phoneName);
NSArray<CNLabeledValue *> *postAddressArray = contact.postalAddresses;
for (CNLabeledValue *addressLabel in postAddressArray) {
CNPostalAddress *address = addressLabel.value;
NSString *ad = [CNPostalAddressFormatter stringFromPostalAddress:address style:CNPostalAddressFormatterStyleMailingAddress];
NSLog(@"地址是---%@",ad);
}
}
//选中属性时候执行
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty {
NSString *key = contactProperty.key;
NSString *value = contactProperty.value;
NSLog(@"%@号码是phone:%@---",key,value);
}

手机号码和邮箱的提取有点麻烦,需要多次判断,其他的没有什么难度。获取手机号

1
2
3
4
5
6
7
8
9
10
NSArray<CNLabeledValue *> *phoneArray = contact.phoneNumbers;
for (CNLabeledValue *label in phoneArray) {
NSString *phone = label.label;
CNPhoneNumber *phoneNum = label.value;
NSString *code = phoneNum.stringValue;
if ([label.label isEqualToString:CNLabelPhoneNumberMobile]) {
NSLog(@"%@手机号码是phone:%@---%@",fullName,phone,code);
}
NSLog(@"%@号码是phone:%@---%@",fullName,phone,code);
}

首先是取到CNLabeledValue数组,然后遍历数组取出CNPhoneNumber。需要注意的是CNPhoneNumber并不包含是手机号,还是工作号这些标签。所以判断这个还是要依据CNLabeledValue的Label进行判断[label.label isEqualToString:CNLabelPhoneNumberMobile]