序
在簡書寫了一個月的blog,開始只是簡單的將自己之前的筆記進(jìn)行CCVV模式(command+c/v),漸漸地已經(jīng)摸索出更多的套路,比如多寫些demo,多加些配圖,較多的知識點用思維導(dǎo)圖做索引,markdown的語法也更加嫻熟,每次整理都重新復(fù)習(xí)了下之前的知識點,也盡量讓自己的思路讓別人理解,所以覺得寫blog還是一個比較好的習(xí)慣的,也感謝簡書上這么多朋友關(guān)注,也是我繼續(xù)寫下去的動力,大家一起努力成為大神!
一、介紹
1.在iOS中,有2個框架可以訪問用戶的通訊錄:
- AddressBookUI.framework 提供了聯(lián)系人列表界面、聯(lián)系人詳情界面、添加聯(lián)系人界面等
一般用于選擇聯(lián)系人 - AddressBook.framework 純C語言的API,僅僅是獲得聯(lián)系人數(shù)據(jù),沒有提供UI界面展示,需要自己搭建聯(lián)系人展示界面,里面的數(shù)據(jù)類型大部分基于Core Foundation框架,使用起來極其蛋疼
2.邏輯結(jié)構(gòu)

3.授權(quán)相關(guān):
(1)從iOS6開始,必須得到用戶授權(quán)訪問通訊錄才能在AppStore上架(即使不授權(quán)也有時候可以訪問通訊錄)
(2)申請通訊錄訪問授權(quán)的代碼,通常放在AppDelegate中~!
(3)獲得通訊錄的授權(quán)狀態(tài)函數(shù):
ABAddressBookGetAuthorizationStatus()例子:獲取授權(quán)狀態(tài)
ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
(4)用戶授權(quán)狀態(tài)有4種:
kABAuthorizationStatusNotDetermined 用戶未選擇,用戶還沒有決定是否授權(quán)你的程序進(jìn)行訪問
kABAuthorizationStatusRestricted iOS設(shè)備上一些許可配置阻止程序與通訊錄數(shù)據(jù)庫進(jìn)行交互
kABAuthorizationStatusDenied 用戶明確的拒絕了你的程序?qū)νㄓ嶄浀脑L問
kABAuthorizationStatusAuthorized 用戶已經(jīng)授權(quán)給你的程序?qū)νㄓ嶄涍M(jìn)行訪問
例子:在 AppDelegate的 didFinishLaunchingWithOptions方法中進(jìn)行授權(quán)
#import <AddressBook/AddressBook.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//1. 獲取授權(quán)狀態(tài)
ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
//2. 創(chuàng)建 AddrssBook
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
//3. 沒有授權(quán)時就授權(quán)
if (status == kABAuthorizationStatusNotDetermined) {
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
//3.1 判斷是否出錯
if (error) {
return;
}
//3.2 判斷是否授權(quán)
if (granted) {
NSLog(@"已經(jīng)授權(quán)");
CFRelease(addressBook);
} else {
NSLog(@"沒有授權(quán)");
}
});
}
CFRelease(addressBook);
return YES;
}
二、彈出系統(tǒng)通訊錄程序
比如充話費時彈出的聯(lián)系人選擇界面(iOS 8 之前的方法,注意版本適配)
頭文件:#import <AddressBookUI/AddressBookUI.h>
例子:點擊彈出聯(lián)系人控制器界面

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//1. 創(chuàng)建聯(lián)系人選擇控制器
ABPeoplePickerNavigationController *picker = [ABPeoplePickerNavigationController new];
//2. 設(shè)置代理,注意不是 Delegate
picker.peoplePickerDelegate = self;
//3. 模態(tài)視圖彈出
[self presentViewController:picker animated:YES completion:nil];
}
三、代理方法
有UI交互,即彈出聯(lián)系人控制器才能獲取聯(lián)系人信息的方法
iOS7和iOS8適配
- iOS 8之后如不想自動dismiss可以在彈出聯(lián)系人控制器方法中加入如下代碼:
if([[UIDevice currentDevice].systemVersion floatValue] >= 8.0){
picker.predicateForSelectionOfPerson = [NSPredicate predicateWithValue:false];
}
- 設(shè)置代理可以獲取點擊后通訊錄里的值,注意代理名不是deleagte
@property(nonatomic,assign,nullable) id<ABPeoplePickerNavigationControllerDelegate> peoplePickerDelegate;
以下1、2兩個代理方法,如果同時實現(xiàn), 只會運行第一個方法!
1.第一個代理方法:選中某個聯(lián)系人時調(diào)用(iOS8之后實現(xiàn)后再無法調(diào)用第2個的方法)
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker didSelectPerson:(ABRecordRef)person NS_AVAILABLE_IOS(8_0);
參數(shù) (ABRecordRef)person 介紹:
- 一個聯(lián)系人就是一個ABRecordRef對象,就相當(dāng)于一條記錄,每個聯(lián)系人都有自己的屬性,比如名字、電話、郵件等,使用ABRecordCopyValue函數(shù)可以從ABRecordRef中獲得聯(lián)系人的簡單屬性(參見下面簡單屬性的介紹)
- ABRecordCopyValue 從記錄中取值函數(shù):
CFTypeRef ABRecordCopyValue(ABRecordRef record, ABPropertyID property)
有2個參數(shù):
- 第1個參數(shù)是ABRecordRef實例
- 第2個參數(shù)ABPropertyID是屬性關(guān)鍵字,定義在ABPerson.h中,下面有介紹
- 注意:使用ABRecordCopyValue可以從一條Person記錄中獲取到對應(yīng)的值,但是后續(xù)還需要根據(jù)值的具體類型再加以處理(比如簡單屬性取的值不需要再處理,多重屬性的值還需要通過其它函數(shù)再取值)
ABPropertyID聯(lián)系人屬性介紹
ABPropertyID 就是聯(lián)系人的屬性,所有的屬性常量值都定義在了ABPerson.h頭文件中
聯(lián)系人屬性包括以下類型:
(1)簡單屬性:姓 kABPersonLastNameProperty,名 kABPersonFirstNameProperty 等
(2)組合屬性:地址等 kABPersonAddressProperty
(3)多重屬性:電話號碼 kABPersonPhoneProperty 、電子郵件 kABPersonEmailProperty 等
聯(lián)系人的有些屬性值就沒這么簡單,一個屬性可能會包含多個值,比如郵箱,分為工作郵箱、住宅郵箱、其他郵箱等,比如電話,分為工作電話、住宅電話、其他電話等
如果是多重屬性,那么ABRecordCopyValue函數(shù)返回的就是ABMultiValueRef類型的數(shù)據(jù),例如郵箱或者電話
// 取電話號碼
ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
// 取記錄數(shù)量
NSInteger phoneCount = ABMultiValueGetCount(phones);
// 遍歷所有的電話號碼
for (NSInteger i = 0; i < phoneCount; i++) {...}
獲取多重屬性的方法
// 電話標(biāo)簽
CFStringRef phoneLabel = ABMultiValueCopyLabelAtIndex(phones, i);
// 本地化電話標(biāo)簽
CFStringRef phoneLocalLabel = ABAddressBookCopyLocalizedLabel(phoneLabel);
// 電話號碼
CFStringRef phoneNumber = ABMultiValueCopyValueAtIndex(phones, i);
4.ABPersonCopyLocalizedPropertyName(ABPropertyID property) 函數(shù)可以根據(jù)指定的關(guān)鍵字獲取對應(yīng)的標(biāo)簽文本(關(guān)于標(biāo)簽,下面案例2有介紹)
案例:
下面案例都是在該代理方法中使用,如果打印不出,嘗試在AppDelegate中進(jìn)行授權(quán)~!
- 例子1:從記錄中取姓,并轉(zhuǎn)換為NSString類型
CFStringRef lastName = ABRecordCopyValue(person, kABPersonLastNameProperty);
NSString *lastNameStr = (__bridge NSString *)(lastName);
CFRelease(lastName); // 使用__bridge type 方法記得釋放!
-
例子2:獲取電話號碼, 電話返回的是多數(shù)據(jù)類型(可以獲取到標(biāo)簽和電話號等信息)
如:住宅就是標(biāo)簽,下面是電話號
標(biāo)簽和值的概念
如:打印該方式獲取到的聯(lián)系人電話的標(biāo)簽和電話號碼
打印查看標(biāo)簽和值
// 獲取電話,電話是多數(shù)據(jù)類型
ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
// 獲取電話的個數(shù)
CFIndex count = ABMultiValueGetCount(phones);
// 遍歷聯(lián)系人,取出每個電話標(biāo)簽和電話號碼,CF框架必須用for i循環(huán)
for (CFIndex i = 0 ; i < count; i++) {
// 獲取聯(lián)系電話的標(biāo)簽,使用__bridge_transfer方法不用釋放
NSString *label = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phones, i);
NSLog(@"label: %@",label);
// 獲取聯(lián)系電話,使用CFBridgingRelease方法和上面功能一樣也不需要釋放
NSString *value = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phones, i));
NSLog(@"value: %@",value);
}
//phones 對象需要被釋放
CFRelease(phones);
- 例子3:獲取通訊錄中所有聯(lián)系人信息
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person {
// 1. 獲取系統(tǒng)通訊錄應(yīng)用
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, nil);
// 2. 獲取所有聯(lián)系人記錄
NSArray *array = (__bridge_transfer NSArray *)(ABAddressBookCopyArrayOfAllPeople(addressBook));
for (NSInteger i = 0; i < array.count; i++) {
// 取出一條記錄
ABRecordRef person = (__bridge ABRecordRef)(array[i]);
// 取出個人記錄中的詳細(xì)信息
NSString *firstNameLabel = (__bridge_transfer NSString *)(ABPersonCopyLocalizedPropertyName(kABPersonFirstNameProperty));
NSString *firstName = (__bridge_transfer NSString *)(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSString *lastNameLabel =(__bridge_transfer NSString *)(ABPersonCopyLocalizedPropertyName(kABPersonLastNameProperty));
NSString *lastName =(__bridge_transfer NSString *)(ABRecordCopyValue(person, kABPersonLastNameProperty));
NSLog(@"%@ %@ - %@ %@", lastNameLabel, lastName, firstNameLabel, firstName);
}
CFRelease(addressBook);
}
關(guān)于Core Foundation 和 NSFundation 橋接的問題:
注意:無論CF對象是否被引用,只要使用CF函數(shù),就會產(chǎn)生CF對象留在內(nèi)存中,如:ABMultiValueCopyValueAtIndex(phones, i)這一句,建議點擊靜態(tài)內(nèi)存分析工具,可發(fā)現(xiàn)沒有被釋放的CF對象。
橋接有三種方式:
- (__bridge type)(expression) : 只是讓NSFoundation框架暫時使用CF框架對象,注意需要手動釋放 Core Foundation 對象,用CFRelease( )函數(shù)。
- (__bridge_transfer type)(expression) / CFBridgingRelease(expression) : CF框架移交對象的管理權(quán)給NSFoundation框架,不需要手動釋放對象
- 前兩種是將CF對象轉(zhuǎn)NSFoundation,最后一個是NSFoundation轉(zhuǎn) CF對象,不常用
(__bridge_retained <#CF type#>)(<#expression#>)
其它知識點
1、添加聯(lián)系人的步驟
通過ABPersonCreate函數(shù)創(chuàng)建一個新的聯(lián)系人(返回ABRecordRef)
通過ABRecordSetValue函數(shù)設(shè)置聯(lián)系人的屬性
通過ABAddressBookAddRecord函數(shù)將聯(lián)系人添加到通訊錄數(shù)據(jù)庫中
通過ABAddressBookSave函數(shù)保存剛才所作的修改
可以通過ABAddressBookHasUnsavedChanges函數(shù)判斷是否有未保存的修改
當(dāng)決定是否更改通訊錄數(shù)據(jù)庫后,你可以分別使用 AbAddressBookSave 或 ABAddressBookRevert 方式來保存或放棄更改
2、 添加群組的步驟大體和添加聯(lián)系人一致
通過ABPersonCreate函數(shù)創(chuàng)建一個新的組(返回ABRecordRef)
通過ABRecordSetValue函數(shù)設(shè)置組名
通過ABAddressBookAddRecord函數(shù)將組添加到通訊錄數(shù)據(jù)庫中
通過ABAddressBookSave函數(shù)保存剛才所作的修改
3、 想操作聯(lián)系人的頭像,有以下函數(shù)
BPersonHasImageData
判斷通訊錄中的聯(lián)系人是否有圖片
ABPersonCopyImageData
取得圖片數(shù)據(jù)(假如有的話)
ABPersonSetImageData
設(shè)置聯(lián)系人的圖片數(shù)據(jù)
2.第二個代理方法,選中聯(lián)系人某個屬性(詳細(xì)的信息,如電話號碼)的時候調(diào)用,注意和上面的方法只能實現(xiàn)一個,如同時實現(xiàn)無法跳轉(zhuǎn)到聯(lián)系人詳情頁面,優(yōu)先上面的方法
該方法可以獲取具體的哪個電話號碼,例如使用充值話費時不能使用上面方法,因為無法確定具體充值哪個號碼
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController*)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier NS_AVAILABLE_IOS(8_0);

例子:獲取用戶點擊的,確定的某個電話號碼或聯(lián)系人信息
#pragma mark 選中聯(lián)系人的某個屬性的時候調(diào)用
- (void)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker didSelectPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier {
// 獲取該聯(lián)系人多重屬性--電話號
ABMutableMultiValueRef phoneMulti = ABRecordCopyValue(person, kABPersonPhoneProperty);
// 獲取該聯(lián)系人的名字,簡單屬性,只需ABRecordCopyValue取一次值
ABMutableMultiValueRef firstName = ABRecordCopyValue(person, kABPersonFirstNameProperty);
NSString *name = (__bridge NSString *)(firstName);
// 獲取點擊的聯(lián)系人的電話
NSLog(@"聯(lián)系人名字 : %@",name);
// 點擊某個聯(lián)系人電話后dismiss聯(lián)系人控制器,并回調(diào)點擊的數(shù)據(jù)
[self dismissViewControllerAnimated:YES completion:^{
// 從多重屬性——電話號中取值,參數(shù)2是取點擊的索引
NSString *aPhone = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneMulti, ABMultiValueGetIndexForIdentifier(phoneMulti,identifier)) ;
// 獲取點擊的聯(lián)系人的電話,也可以取標(biāo)簽等
NSLog(@"聯(lián)系人電話 : %@",aPhone);
// 去掉電話號中的 "-"
aPhone = [aPhone stringByReplacingOccurrencesOfString:@"-" withString:@"" ];
NSLog(@"去掉-號 : %@",aPhone);
}];
}
3.第三個代理方法:取消選中聯(lián)系人的時候調(diào)用
注意:在iOS 7 下必須實現(xiàn)此方法,否則會崩潰!
- (void)peoplePickerNavigationControllerDidCancel:(ABPeoplePickerNavigationController *)peoplePicker;
4.下面兩個是iOS7選擇聯(lián)系人代理方法,iOS 8之前才會調(diào)用,適配iOS 7時實現(xiàn),適配iOS 8之后使用上面兩個方法
1.返回YES,則會跳轉(zhuǎn)到聯(lián)系人詳情頁面,如果返回NO必須手動實現(xiàn)控制器dismiss方法
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person NS_DEPRECATED_IOS(2_0, 8_0);
2.返回NO不會執(zhí)行默認(rèn)的操作,如:打電話,必須手動實現(xiàn)控制器dismiss方法
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier NS_DEPRECATED_IOS(2_0, 8_0);
四、不需要彈出聯(lián)系人控制器就可以獲取聯(lián)系人信息的方法
#pragma mark - 點擊屏幕獲取所有聯(lián)系人信息,記得授權(quán)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//1. 判斷是否授權(quán)成功, 授權(quán)成功才能獲取數(shù)據(jù)
if ( ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
//2. 創(chuàng)建通訊錄
ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
//3. 獲取所有聯(lián)系人
CFArrayRef peosons = ABAddressBookCopyArrayOfAllPeople(addressBook);
//4. 遍歷所有聯(lián)系人來獲取數(shù)據(jù)(姓名和電話)
CFIndex count = CFArrayGetCount(peosons);
for (CFIndex i = 0 ; i < count; i++) {
//5. 獲取單個聯(lián)系人
ABRecordRef person = CFArrayGetValueAtIndex(peosons, i);
//6. 獲取姓名
NSString *lastName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonLastNameProperty));
NSString *firstName = CFBridgingRelease(ABRecordCopyValue(person, kABPersonFirstNameProperty));
NSLog(@"lastName: %@, firstName: %@", lastName, firstName);
//7. 獲取電話
ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
//7.1 獲取電話的count數(shù)
CFIndex phoneCount = ABMultiValueGetCount(phones);
//7.2 遍歷所有電話號碼
for (CFIndex i = 0; i < phoneCount; i++) {
NSString *label = CFBridgingRelease(ABMultiValueCopyLabelAtIndex(phones, i));
NSString *value = CFBridgingRelease(ABMultiValueCopyValueAtIndex(phones, i));
// 打印標(biāo)簽和電話號
NSLog(@"label: %@, value: %@",label, value);
}
NSLog(@"\\n\\n");
//8.1 釋放 CF 對象
CFRelease(phones);
}
//8.1 釋放 CF 對象
CFRelease(peosons);
CFRelease(addressBook);
}
}
五、iOS 9 新出的點擊通訊錄的獲取信息的辦法
有UI交互,即需要點擊聯(lián)系人控制器,代理屬性為delegate
頭文件:#import <ContactsUI/ContactsUI.h>
#pragma mark - 先彈出聯(lián)系人控制器
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 1. 創(chuàng)建控制器
CNContactPickerViewController * picker = [CNContactPickerViewController new];
// 2. 設(shè)置代理
picker.delegate = self;
// 3. 設(shè)置相關(guān)屬性,謂詞篩選email地址是@mac.com的聯(lián)系人
picker.predicateForSelectionOfProperty = [NSPredicate predicateWithFormat:@"(key == 'emailAddresses') AND (value LIKE '*@mac.com')"];
/ / 謂詞篩選email地址數(shù)等于1的聯(lián)系人
picker.predicateForSelectionOfContact = [NSPredicate predicateWithFormat:@"emailAddresses.@count == 1"];
// 4. 彈出
[self presentViewController: picker animated:YES completion:nil];
}
#pragma mark - 取消選中聯(lián)系人的時候調(diào)用,點擊右上角的cancel時候觸發(fā),而不是picker的所有dismiss動作中都會觸發(fā)。在多選模式下,cancel在done的左側(cè)。
- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker {
}
其他四個代理方法只要實現(xiàn)其中一個就行了。分別為單選和多選兩組,都實現(xiàn)的時候,多選優(yōu)先執(zhí)行,單選不執(zhí)行。特別要注意的是predicateForEnablingContact,predicateForSelectionOfContact,predicateForSelectionOfProperty這三組屬性會影響它們的動作。predicateForEnablingContact返回YES的聯(lián)系人才是可交互的,默認(rèn)聯(lián)系人都是可交互的。

#pragma mark - 選擇聯(lián)系人的時候調(diào)用 (如果predicateForSelectionOfContact屬性沒被設(shè)置或符合篩選條件,如不符合則不會觸發(fā)該方法并進(jìn)入聯(lián)系人詳情頁)
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact {
//1. 獲取姓名 ,givenName == firstName
NSLog(@"givenName: %@, familyName: %@", contact.givenName, contact.familyName);
//2. 獲取電話,泛型,會在數(shù)組遍歷時幫很大的忙
for (CNLabeledValue *labeledValue in contact.phoneNumbers) {
NSLog(@"label: %@",labeledValue.label);
CNPhoneNumber *phoneNumber = labeledValue.value;
NSLog(@"phoneNumber: %@",phoneNumber.stringValue);
}
}
#pragma mark - 實現(xiàn)了此方法, 就可以選擇多個聯(lián)系人,該方法在點擊done按鈕時觸發(fā),注意:該方法不受predicateForSelectionOfContact屬性影響!
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContacts:(NSArray<CNContact *> *)contacts {
for (CNContact *contact in contacts) {
NSLog(@"givenName: %@, familyName: %@", contact.givenName, contact.familyName);
//2. 獲取電話,泛型,會在數(shù)組遍歷是幫很大的忙
for (CNLabeledValue *labeledValue in contact.phoneNumbers) {
NSLog(@"label: %@",labeledValue.label);
CNPhoneNumber *phoneNumber = labeledValue.value;
NSLog(@"phoneNumber: %@",phoneNumber.stringValue);
}
}
}
#pragma mark - 點擊某個聯(lián)系人的某個屬性(property)時觸發(fā)并返回該聯(lián)系人屬性(contactProperty)。
只實現(xiàn)該方法時,可以進(jìn)入到聯(lián)系人詳情頁面(如果predicateForSelectionOfProperty屬性沒被設(shè)置或符合篩選條件,如不符合會觸發(fā)默認(rèn)操作,即打電話,發(fā)郵件等)。
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty {
}
#pragma mark - 只實現(xiàn)該方法時,停留在多選模式下的聯(lián)系人列表頁面(如果predicateForSelectionOfProperty屬性沒被設(shè)置或符合篩選條件,該聯(lián)系人才能被選中),在點擊done按鈕的時候觸發(fā),返回的contactProperties中只包含選中的contactProperties,沒選中的話返回空。
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperties:(NSArray<CNContactProperty *> *)contactProperties {
// 循環(huán)打印出所有選中的聯(lián)系人名字
for (CNContactProperty *contactProperty in contactProperties) {
NSLog(@"%@",contactProperty.contact.givenName);
}


