2012년 12월 31일 월요일

[iOS6] ABAddressBook 연락처 정보 읽어 오기


iOS6에서 사용자의 Address Book에서 연락처 리스트를 읽어와서, 각 연락처를 읽어오는 방법을 정리합니다.
Address Book은 Address Book 프레임워크를 사용하고 있어서, AB라는 접두어를 사용하고 있습니다.

1.  접근 권한 체크
처음 접근을 하면, 사용자에게 접근을 허락할 것인지 입력을 받습니다. 여기서 권한을 승인하거나 거부할 수 있는데, 프로그램에서는 이 권항이 있는지부터 체크를 해야 합니다.
소스
    //접근권한체크하기.
    ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
    if(status == kABAuthorizationStatusDenied ||
       status == kABAuthorizationStatusRestricted)
    {
        NSString *message;
        message = @"연락처에접근할수있는권한이없습니다. 
                   설정 > 개인정보보호 > 연락처에서권한을설정해주세요.";
        //NSLog(@"Location Services Disabled");
        UIAlertView* addrressBookAlert = [[UIAlertView alloc] initWithTitle:@"Error" 
                                                                    message:message 
                                                                   delegate:self 
                                                          cancelButtonTitle:@"OK"
                                                          otherButtonTitles: nil];
        [addrressBookAlert show];
    }
이 권한은 연락처 일기 등을 할 때, 체크해서 보여줘도 되고, 프로그램이 시작할 때, 체크해서 사용자에게 권한을 변경하도록 요청할 수 있습니다.
 카카오톡의 경우는 친구 리스트에 "친구이름표시문제 안내"를 표시하고, 도움말에 설정을 변경할 것을 표시합니다.
 Foursqure의 경우는 위치보기 설정을 해야 정상적으로 표시함을 정보창의 크기로 알려줍니다.


2. Address Book 열기
객체를 ABAddressBookCreateWithOption함수를 통해서 어드레스북 객체를 만든다. 객체가 NULL일 경우, 에러가 난 것으로 권한이 없는지, iOS가 지원하지 않는 버전인지 등등을 체크해야 한다.
 그리고, 접근하기 위해서 ABAddressBookRequestAccessWithCompletion을 통해서 접근 요청을 한다. 여기에서 Block이 사용되고, 접근이 grant되었는지, 에러는 없는지 Block내에서 체크해야 한다.
아래에 myAddressBook은 ABAddressBookRef 타입이다.

- (void) prepareAddressBook
{
    CFErrorRef error;
    myAddressBook = ABAddressBookCreateWithOptions(NULL, &error);
    if (myAddressBook == NULL) {
        //
        if(error == kABOperationNotPermittedByStoreError){
            NSLog(@" not permitted by store");
        }
        /*
         else if(error == kABOperationNotPermittedByUserError){
         NSLog(@" not permitted by User");
         }*/
    }
    
    TCContactManager * __weak weakSelf = self;  // avoid capturing self in the block
    ABAddressBookRequestAccessWithCompletion(myAddressBook,
                            ^(bool granted, CFErrorRef error) {
                                if (granted) {
                                    weakSelf.friendsList = [weakSelf fetchPeopleInAddressBook:myAddressBook];
                                    NSLog(@" load success: %d", [weakSelf.friendsList count]);
                                                     
                                } else {
                                    // Handle the error
                                    NSLog(@" no granted..");
                                }
                            });
}

위에서 접근이 granted된 경우 현재 생성된 ABAddressBookRef 객체를 가지고 아래와 같이 연락처를 가져온다.

3. 모든 연락처 가져오기.
address book에서 모든 사람들의 리스트를 copy해 올려면, ABAddressBookCopyArrayOfAllPeople함수를 사용하면, 내 iOS에 저장된 모든 사람들의 주소가 저장된 순서대로 가져오게 된다.
 모든 연락처가 아닌 Source에 따라서 읽어오려면 ABAddressBookCopyArrayOfAllPeopleInSource 함수를 사용하여 특정 Source에 있는 주소를 읽어 온다.
순서를 가지고 읽어 오게 하려면 ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering 를 사용해서 Source에서 Ordering을 두고 읽어오게 할 수 있다.
아래는 모든 사람들을 읽어오는 예제이다.

// Return a list of people in address book
- (NSMutableArray*) fetchPeopleInAddressBook:(ABAddressBookRef)addressBook
{
    NSMutableArray *list = nil;
    CFArrayRef personList = ABAddressBookCopyArrayOfAllPeople(addressBook);
    long personCount = CFArrayGetCount(personList);
    if( personCount > 0){
        NSLog(@" Person Count:%ld", personCount);
        
        if(list == nil)
        {
            list = [[NSMutableArray alloc] initWithCapacity:personCount];
        }
        for(int i = 0; i < personCount; i++)
        {
            ABRecordRef record = CFArrayGetValueAtIndex(personList, i);
            
            TCContactData* contact = [self getContactDataFrom:record];
            
            if(contact != nil && ![list containsObject:contact]){
                [list addObject:contact];
            }
            
            CFRelease(record);
        }
    }
    CFRelease(personList);
    
    return list;
}
CFArrayRef의 형태로 받아오게 되고, CFArrayGetCount로 읽어온 개수를 알 수 있다.
각 데이터는 ABRecordRef의 형태로 가져올 수 있게 되는데, 이것은 클래스의 형태가 아닌 CF 형태로 되어 있으므로 이것을 Class로 변환을 해야, 관리가 편리하게 된다.
아래에서 ABRecordRef에서 각 데이터를 읽어오는 것을 알아본다.


4. 하나의 record에서 연락처 읽어 오기.
ABRecordRef는 Address book에서 하나의 레코드를 나타내는 것으로 각 레코드의 정보를 읽어오는 함수를 제공한다. (Getting Record Information)
ABRecordGetRecordID
ABRecordID ABRecordGetRecordID (
   ABRecordRef record
);
레코드의 Unique ID를 읽어오는 함수로 현재 레코드의 ID를 읽어올 수 있습니다.

ABRecordGetRecordType
ABRecordType ABRecordGetRecordType (
   ABRecordRef record
);
위에서 각 People이 아닌, 그룹의 리스트를 가져올 수도 있고, Source의 리스트를 받아 올 수도 있어서, 현재 Record의 타입이 뭔지 알수 있어야 합니다.
kABPersonType for person records
kABGroupType for group records.
kABSourceType for source records.

그리고, 각 레코드의 값을 읽어오거나, 설정/제거 할 수 있는 함수를 제공합니다. (Managing Property Value)
ABRecordSetValue
bool ABRecordSetValue (
   ABRecordRef record,
   ABPropertyID property,
   CFTypeRef value,
   CFErrorRef *error
);

레코드에 있는 Property에 값을 설정할 수 있는 함수이다.

ABRecordCopyValue
CFTypeRef ABRecordCopyValue (
   ABRecordRef record,
   ABPropertyID property
);

레코드에 있는 값들을 읽어 오는 함수로, ABPropertyID에 따라서 값들을 읽어 온다. 이 ABPropertyID는 두가지 형태가 있는데, ABPerson과 ABGroup에 따라 다르다. 각각에 Reference에 따라서 값들이 달라지는데, ABPerson을 예를 들면, 전화번호를 보면 kABPersonPhoneProperty로 값을 읽어오면, 아래와 같이 여러가지 종류의 전화번호를 가지고 있을 수 있다.
const ABPropertyID kABPersonPhoneProperty;
const CFStringRef kABPersonPhoneMobileLabel;
const CFStringRef kABPersonPhoneIPhoneLabel;
const CFStringRef kABPersonPhoneMainLabel;
const CFStringRef kABPersonPhoneHomeFAXLabel;
const CFStringRef kABPersonPhoneWorkFAXLabel;
const CFStringRef kABPersonPhoneOtherFAXLabel;
const CFStringRef kABPersonPhonePagerLabel;
그래서, kABPersonPhoneProperty로 읽으면 ABMultiValueRef 형태로 받아오고, 거기에서 위에 값들이 있는지 ABMultiValueCopyLabelAtIndex를 통해서 읽어 올 수 있다. 좀 복잡한데 아래 함수 설명 이후 전화번호 읽는 것을 예제로 보이도록 하겠다.

ABRecordRemoveValue
bool ABRecordRemoveValue (
   ABRecordRef record,
   ABPropertyID property,
   CFErrorRef *error
);

레코드에서 해당 값을 삭제하는 함수로 해당 ABPropertyID를 가지는 모든 Value를 삭제하게 된다.

ABRecordCopyCompositeName
CFStringRef ABRecordCopyCompositeName (
   ABRecordRef record
);

레코드에서 이름을 읽어 오는데, First, middle, last name을 각각 붙여서 읽어 오는 함수이다. Apple Documentation에는 "Returns an appropriate, human-friendly name for the record."라고 되어 있다.

아래 예제는 현재 ABRecordRef에서 전화번호가 있는지 살펴보고, 전화번호가 있으면, 이름과 이미지를 읽어서 TCContactData라는 클래스에 넣고 리턴하는 함수이다.

- (TCContactData*) getContactDataFrom:(ABRecordRef)record
{
    
    ABMultiValueRef phoneRef = ABRecordCopyValue(record, kABPersonPhoneProperty);
    NSString* phoneNumber = nil;
    int phoneRefCount = ABMultiValueGetCount(phoneRef);
    for( int j = 0; j < phoneRefCount; j++)
    {
        CFStringRef labelRef = ABMultiValueCopyLabelAtIndex(phoneRef, j);
        if( CFStringCompare(labelRef, kABPersonPhoneMobileLabel, 0) == kCFCompareEqualTo){
            phoneNumber = (__bridge NSString*)ABMultiValueCopyValueAtIndex(phoneRef, j);
            break;
        }else if( CFStringCompare(labelRef, kABPersonPhoneIPhoneLabel, 0) == kCFCompareEqualTo){
            phoneNumber = (__bridge NSString*)ABMultiValueCopyValueAtIndex(phoneRef, j);
            break;
        }
        /*else if( CFStringCompare(labelRef, kABPersonPhoneMainLabel, 0) == kCFCompareEqualTo){
         contact.phoneNumber = (__bridge NSString*)ABMultiValueCopyValueAtIndex(phoneRef, j);
         break;
         }*/
        CFRelease(labelRef);
    }
    if(phoneNumber){
        TCContactData* contact = [[TCContactData alloc] init];
        
        contact.name      = (__bridge NSString*)ABRecordCopyCompositeName(record);
        contact.firstName = (__bridge NSString*)ABRecordCopyValue(record, kABPersonFirstNameProperty);
        contact.lastName  = (__bridge NSString*)ABRecordCopyValue(record, kABPersonLastNameProperty);
        contact.phoneNumber = phoneNumber;
        
        CFDataRef thumbnail = ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
        if(thumbnail != nil)
        {
            contact.image = [[UIImage alloc] initWithData:(__bridge NSData*)thumbnail];
            NSLog(@" Name:%@ has image.", contact.name);
            CFRelease(thumbnail);
        }
        return contact;
    }
    
    CFRelease(phoneRef);
    return nil;
} 

위 함수에서 처럼 현재 레코드에서  Person Info Property, Address Property, Date Property, Person Type Property, Phone Number Property, Instant Messaging Keys, Instant Messaging Services, Social Profile Keys, Social Profile Services, URL Property, Related Name Property, Generic Property Labels 등의 정보를 읽어 올 수 있다.

한국과 맞지 않는 체계도 있지만, 전세계적으로 맞출수 있도록 되어 있습니다.
해외 서비스를 위해서는 많은 데이터를 보고, 그 나라 사람들이 사용하는 데이터가 되도록 조합해서 사용해야 되겠습니다.

2012년 마지막 포스팅이 되겠네요.
새해 복 많이 받으세요~
.