盡管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"); // } }
讓我們來分析一下:
- 這個檢查是用來檢測用戶是否拒絕了你的app訪問手機通訊錄,或者是它是受限制的(比如家長控制).如果用戶拒絕了或者限制了,那么你只能告訴用戶沒有權限對通訊錄進行操作,其他的什么也無法做。
- 這個檢查是看看用戶是否已經允許你的APP訪問用戶的通訊錄,如果允許了,你可以隨意地修改或者對通訊錄進行其他操作。
- 這個檢查是看用戶是否還沒有確定你的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:一旦用戶點擊了授權按鈕,便會調用里面的東西。
這次運行的結果就是:

當你點擊了Don't Allow,就表明iDenied了,如果ok就表示你允許了APP訪問通訊錄。
創建記錄
現在,讓我們開始去創建通訊錄記錄。我們現在清空按鈕點擊事件,然后重寫它。在重寫的這個方法里面,你需要創建一個ABRecordRef,他包括了寵物的屬性,檢查一下通訊錄確保不存在你添加的聯系人,如果寵物不在通訊錄,就把他加入到通訊錄。
在tapAction:(id)sender方法里面寫入:
NSString *petFirstName; NSString *petLastName; NSString *petphoneNumber; NSData *petImageData; if (sender.tag == 1) { petFirstName = @"Cheesy"; petLastName = @"Cat"; petphoneNumber = @"201345345345"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Cheesy.jpg"], 0.7f); }else if (sender.tag == 2) { petFirstName = @"Freckles"; petLastName = @"Dog"; petphoneNumber = @"2015434345345"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Freckles.jpg"], 0.7f); }else if (sender.tag == 3) { petFirstName = @"Maxi"; petLastName = @"Dog"; petphoneNumber = @"1243504354"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Maxi.jpg"], 0.7f); }else if(sender.tag == 4) { petFirstName = @"Shippo"; petLastName = @"Dog"; petphoneNumber = @"5406957657"; 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), 0) == kCFCompareEqualTo) { //用戶已經存在 NSLog(@"用戶已經存在"); break; } }
你必須使用id,因為從技術上來講,Core Foundation類型是不能被轉換成NSArray的,因為他們不是對象。ABRecordRefs被偽裝成id來避免出錯。所以為了得到ABRecordRef,還需要使用再次使用__bridge。
使用CFStringCompare的方式類似於NSString的isEqualToString。ABRecordCopyCompositeName得到了全名,它是聯系人姓和名的組合。這樣就可以用來防止重復記錄了。
多線程
截止到這里上面的整體代碼如下:
- (IBAction)tapAction:(UIButton *)sender { if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted) { //1 NSLog(@"Denied"); }else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) { //2 NSLog(@"Authorized"); }else { //3 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 == 1) { petFirstName = @"Cheesy"; petLastName = @"Cat"; petphoneNumber = @"201345345345"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Cheesy.jpg"], 0.7f); }else if (sender.tag == 2) { petFirstName = @"Freckles"; petLastName = @"Dog"; petphoneNumber = @"2015434345345"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Freckles.jpg"], 0.7f); }else if (sender.tag == 3) { petFirstName = @"Maxi"; petLastName = @"Dog"; petphoneNumber = @"1243504354"; petImageData = UIImageJPEGRepresentation([UIImage imageNamed:@"contact_Maxi.jpg"], 0.7f); }else if(sender.tag == 4) { petFirstName = @"Shippo"; petLastName = @"Dog"; petphoneNumber = @"5406957657"; 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), 0) == 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){ //4 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的延遲,直到回調被調用。這看起來好像當我們在添加通訊錄記錄的餓時候程序是卡的狀態。在大多數情況下,這種問題並不常見。
