iOS9新特性之Contacts Framework

iOS9中关于通讯录更新了新的框架,使用Contacts Framework替换掉了难用的AddressBook Framework,而且适用于Objective-C和Swift。Session有关于新框架的讲解。

在这里仅仅通过Contacts Framework进行通讯录的读取,没有进行添加或者删除操作,其实后两者进行起来还是比较简单的,因为新的框架中返回的联系人是统一的,这意味着,从不同的数据源获得的相同联系人数据会自动进行合并操作。

本文是在看了他的文章之后写的一个Objective-C版本demo,其实就是将Swift转化成Objective-C,但是这其中由于不懂Swift的语法,出现了一些问题,但是最后这些问题都不是问题了。

首先也是创建工程,平常的Single ViewController就行,然后在ViewController中import框架的头文件#import <Contacts/Contacts.h>,当然前提还是在Link Binary With Libraries中添加框架。

可以看到每一个联系人信息都是一个CNContact类,而所有的联系人都由一个CNContactStore类进行读取和保存,由于这个类管理着所有的联系人,每一个联系人又都有很多的字属性,比如姓名、电话、邮箱、社交账号、生日等等,所以,在读取联系人的时候我们要指定一个条件从联系人数据库中进行读取,这个条件就是CNContactFetchRequest

我们可以设置CNContactFetchRequest的keysToFetch数组进行条件选择

1
2
3
4
5
6
7
8
9
NSArray * fetch = @[CNContactNicknameKey,
CNContactImageDataKey,
CNContactGivenNameKey,
CNContactFamilyNameKey,
CNContactPhoneNumbersKey,
CNContactOrganizationNameKey,
CNContactEmailAddressesKey,
CNContactDatesKey];
CNContactFetchRequest * fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:fetch];

然后通过这个fetchRequest进行联系人的获取

1
2
3
4
5
6
7
   CNContactStore * store = [[CNContactStore alloc] init];

NSMutableArray * contacts = [NSMutableArray array];

[store enumerateContactsWithFetchRequest:fetchRequest error:NULL usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
[contacts addObject:@[contact]];
}];

需要说明的是这个数组contacts里面装的是一个数组,对,数组中又是一个数组,里面的数组中的第一项就是我们要的联系人

cellForRow方法

1
2
3
4
5
6
NSArray * contactArray = contacts[indexPath.row];
CNContact * contact = contactArray[0];

NSString * giveName = contact.givenName;
NSString * familyName = contact.familyName;
NSString * organizationName = contact.organizationName;

前面说了,每一个联系人有很多的子属性,在fetchRequest的时候进行了添加,在获得到的联系人中就会存在这个字属性,没有添加就不会又这个属性,硬是要获取的话,会崩溃的。举个例子,上面的fetch数组中没有联系人的生日keyCNContactBirthdayKey,所以在后面如果要是获取生日的话

1
NSDateComponents * birtyday = contact.birthday;

就会崩溃,报错内容

1
*** Terminating app due to uncaught exception 'CNPropertyNotFetchedException', reason: 'A property was not requested when contact was fetched.'

因此,在使用的时候要特别注意。

后面的基本上就不涉及Contacts Framework的使用了,仅仅是将某一位联系人的基本信息展示出来。

在获得到所有的联系人后,使用tableView进行展示,点击某一位联系人进入联系人详情界面,使用sb进行跳转,在prepareForSegue方法中进行值传递

viewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

if ([segue.identifier isEqualToString: @"detail"]) {
HLLDetailViewController * destinationViewController = segue.destinationViewController;
NSIndexPath * selectedIndexPath = self.tableView.indexPathForSelectedRow;
if (selectedIndexPath) {

NSArray * contactArray = [self.contacts objectAtIndex:selectedIndexPath.row];
CNContact * contact = [contactArray objectAtIndex:0];
destinationViewController.contact = contact;
destinationViewController.title = @"联系人详情";
}
}
}

在联系人详情界面有联系人的头像,姓名,联系方式。

头像如果有的话就是用contact的imageData进行获取并展示,而姓名由于有多种可能,比如nickName、giveName、familyName等,所以需要判断显示。联系方式就有得说道了,每一个联系方式都有一个备注,比如“联通号”、“学校”等等,获得电话的方法如下

cell.m

NSString * start = @"_$!<";
NSString * end = @">!$_";

NSString * predicateString = [NSString stringWithFormat:@"self beginswith '%@' and self endswith '%@'",start,end];
NSPredicate * labelPredicate = [NSPredicate predicateWithFormat:predicateString];

NSMutableString * newLabel = [NSMutableString stringWithString:label];

if ([labelPredicate evaluateWithObject:label]) {
    NSLog(@"yes ,its");
    NSRange startRange = [newLabel rangeOfString:start];
    [newLabel deleteCharactersInRange:startRange];
    NSRange endRange = [newLabel rangeOfString:end];
    [newLabel deleteCharactersInRange:endRange];   
}
self.labelLabel.text = newLabel;

看到了那些奇怪的符号_$!<>!$_了么,这是系统自带的电话标签,自定义的标签没有这些字符。

到这里就完整的进行了联系人的读取以及某一位联系人的具体信息展示部分。

10.22 update

下面说一下关于ContactUI FrameWork的东西,这个框架有两个经常会用到的类CNContactViewControllerCNContactPickerViewController

CNContactPickerViewController这个好理解,使用的是系统自带的通讯录界面,允许用户进行单选或者多选联系人,可以决定显示联系人的某些属性,可以进行谓词设置从而筛选要展示的联系人,至于单选或者多选是由代理方法的实现决定的,其他没有什么了。

而至于CNContactViewController就有一点麻烦了,这个类用于显示某一位给定CNContact实例联系人的详情,初始化的时候需要传递一个CNContact实例,而且必须是CNMutableContact实例,至于为什么,因为我一直使用的不可变contact进行的初始化,导致崩溃,崩溃原因如下,

1
2
3
*** Terminating app due to uncaught exception 'CNPropertyNotFetchedException', reason: 'Contact 0x7fda9c447000 is missing some of the required key descriptors: (
"<CNAggregateKeyDescriptor: 0x7fda99e234d0: kind=+[CNContactViewController descriptorForRequiredKeys]>"
)'

这是什么鬼,看到descriptorForRequiredKeys发现头文件里面有一个类方法

1
+ (id<CNKeyDescriptor>)descriptorForRequiredKeys;

看语义结合注释应该是必须设定contact的一些key,但是初始化的时候的contact是已经进行了key设置的,WTF!想着是不是要实现这个方法呢(想多了,骚年)?或者调用一下(然并卵)?所以当时搁置了一下,今天闲来看代码,觉得不能不解决啊,就去网上翻了一下,最后还是这篇文章解决了CNContactViewController的问题,需要使用一个CNMutableContact来初始化视图控制器,至于CNMutableContact的一些属性设置就比较操蛋了,需要一个一个进行设置,感受一下:

1
2
3
4
5
6
7
8
9
10
NSArray * contactArray = [self.contacts objectAtIndex:indexPath.row];
CNContact * contact = [contactArray objectAtIndex:0];
CNMutableContact * mutableContact = [[CNMutableContact alloc] init];
mutableContact.contactType = CNContactTypePerson;
mutableContact.givenName = contact.givenName;
mutableContact.familyName = contact.familyName;
mutableContact.phoneNumbers = contact.phoneNumbers;
mutableContact.emailAddresses = contact.emailAddresses;

CNContactViewController * contactViewController = [CNContactViewController viewControllerForContact:mutableContact];

奇怪为什么系统不提供一个可以根据contact来进行便利初始化的方法,像NSMutableString和其他Mutable系列的都有这样的便利初始化方法的。

至于其他的功能如果不是打算做一个通讯录App,我觉得是没必要了解的,大概看一遍头文件就能明白各个类对应的通讯录中的具体含义。这是demo