Contacts 프레임워크

Contacts 프레임워크를 이용한 주소록 액세스

iOS8 까지는 AddressBook 프레임워크를 이용해서 시스템의 주소록 데이터베이스에 액세스할 수 있었는데, iOS9에서는 이 프레임워크 자체가 deprecated 되었다. (헐…) 대신에 연락처를 액세스하는 별도의 프레임워크인 Contacts가 신설되었다.

!주의! 현재 Contacts 프레임워크에는 버그가 많이 있는 듯… 저장할 때 툭하면 에러가 나는 경우가 많다.

액세스 방법은 다음과 같다.

  1. CNContactStore 인스턴스를 만든다. 이는 연락처 저장소에 액세스할 수 있는 추상화된 인터페이스를 제공한다.
  2. addressbook.people()과 같은 인터페이스는 더 이상 제공되지 않는다. 대신 코어데이터와 유사하게 fetchRequest를 이용해서 필요한 연락처들을 페칭할 수 있다.
  3. 변경사항 전체를 한 번제 저장할 수 없다. 개별 연락처 혹은 그룹을 대상으로 부분적으로 업데이트한다. 저장시에는 CNSaveRequest 객체를 만들고, 어떤 그룹이나 연락처를 업데이터/추가/삭제할 것인지를 명시해서 이를 실행하는 형태로 저장한다.

저장소 만들기

저장소(CNContactStore)는 연락처 저장소를 액세스하기 위한 인터페이스를 제공하는 객체이다. 생성시에는 별도의 파라미터가 필요하지 않다,

let store = CNContactStore()

iOS9에서 주소록 액세스 방식이 변경된 것은 페이스북 연락처와 같은 외부 연락처 데이터가 iOS의 연락처 데이터와 통합된 것처럼 동작하기 때문이다. 2개 이상의 개별 연락처는 하나의 통합된 연락처 항목으로 링크되어 통합될 수 있고, 이를 처리하기 위한 새로운 인터페이스를 지원하기 위해 새로운 프레임워크가 도입되지 않았나 본다. (게다가 iOS는 OSX와 달리, AddressBook 프레임워크가 코코아 프레임워크가 아닌 코어파운데이션으로 되어 있었다.)

개별 연락처 항목의 ID를 알고 있다면 unifiedConatactWithIdentifier:keysToFetch:error:1를 이용해서 바로 해당 연락처 정보 중 원하는 키-값 쌍 정보를 얻을 수 있다. 만약 전체 연락처 정보를 알고 싶다면, enumerateContactWithFetchRequest:error:usingBlock:을 이용하면 된다.

참고로 predicate는 CNContact에서 제공하는 4가지 종류의 predicate 생성함수를 이용해야 하며, 이름, ID, 그룹ID, 컨테이너ID를 가지고 얻을 수 있다.

불러오기

연락처를 불러오기 위한 가장 좋은 방법은 enumerateContactsWithFetchRequest:error:usingBlock:을 쓰는 것이다. 이를 위해서는 NSContactFetchRequest 객체가 필요한데, 별다른 옵션이 없으면 전체 연락처를 가져온다. 대신, 모든 연락처의 모든 키를 가져오는 것은 아니며, 사용을 원하는 모든 키를 명시해야 한다.

let fetchRequest = CNContactFetchRequest(keysToFetch:[
    CNContactGivenNameKey, CNContactNicknameKey
])

만약 특정 ID, 특정 그룹을 대상으로 하고 싶다면, 여기에도 predicate 옵션을 줄 수 있다. 역시 CNContact에서 제공하는 predicate만 적용이 가능하다.

전체 연락처를 순회하면서 어떤 동작을 하는 코드는 다음과 같다.

try! store.enumerateContactsWithFetchRequest(fetchRequest){
    contact, stop in
    print(contact.givenName) // givenName은 `firstname`에 해당한다.
}

변경, 편집

fetching된 결과인 CNContact 인스턴스의 각 키는 희한하게도 immuatble하다. 결국 mutable한 사본을 만들어서 변경해야 한다.

let mutableContact = contact.mutableCopy() as! CNMuatbleContact
mutableContact.givenName = "아무개"

저장

저장을 위해서는 CNSaveRequest 객체를 만들어서 어떤 항목을 추가/삭제/업데이트하는지 그 문맥정보를 기입한 후 스토어 객체를 통해서 처리한다.

let saveReqeust = CNSaveRequest()
saveRequest.updateContact(mutableContact) // 이름을 바꿨음
try! store.executeSaveRequest(saveReqeust)

예제

다음은 한글 이름으로부터 초성자음으로 이루어진 닉네임을 생성하고, 이를 이용해서 닉네임을 업데이트하는 함수를 작성해보도록 하겠다.2 먼저 한글로 된 문자열을 주었을 때 초성을 추려내는 변환함수를 생각해보자.

func makeNewNickname(name:String) -> String {
    let transform: UnicodeScalar -> String = { u in
        let iu = Int(u.value)
        guard iu >= 0xac00 && iu <= 0xd7a3 else { return String(u) }
        let index = (iu - 0xac00) / 28 / 21
        return String(UnicodeScalar(index + 0x1100))
    }
    return name.UnicodeScalars.map(transform).joinWithSeparator("")
}

그러면 모든 연락처를 순회하면서 새로운 닉네임을 만들고, 업데이트가 필요한 닉네임만 업데이트하는 코드를 작성해 보겠다.

func updateAllNicknames() {
    let store = CNContactStore()
    let fetchRequest = CNContactFetchRequest(keysToFetch:[
        CNContactGivenNameKey, CNContactNicknameKey
    ])
    let saveRequest = CNSaveRequest()
    try! store.enumerateContactsWithFetchRequest(fetchRequest){
        contact, stop in
        let mutableContact = contact.mutableCopy() as! CNMuatbleContact
        let newNickname = makeNewNickname(mutableContact.givenName)
        if newNickname != mutableContact.nickname {
            mutableContact.nickname = newNickname
            saveRequest.updateContact(mutableContact)
        }
    }
    try! store.executeSaveRequest(saveRequest)
}

  1. 신규 API에서는 Objective-C에서 에러에 대한 포인터를 넘기는 대신 throws 함수로 구현되어 있다. 
  2. 이렇게하면 연락처 앱이나 전화앱에서 연락처 이름의 초성만으로 검색이 가능해진다. 참고로 내 경우에는 성과 이름을 구분하지 않고 모두 ‘이름’란에 입력한다.