개요

Contacts 프레임워크 톺아보기

Contacts 프레임워크 문서를 참고해서 사용자의 연락처를 가져와서 테이블뷰를 통해 보여주자.

🔨 사용자의 연락처를 가져와서 보여주기

UI 구성하는 부분은 패스. 코드나 스토리보드로 테이블뷰와 셀 구성을 해주면 된다. 셀의 스타일은 Subtitle로 지정해줬다. 데이터를 많이 가져와서 보여줄 게 아니라서 간단하게 했다.

그다음으로 Fetching Contacts 문단에 있는 코드를 그대로 따라 하면 다음과 같은 코드를 작성할 수 있다.

override func viewDidLoad() {
    super.viewDidLoad()

    self.fetchContacts()
}

func fetchContacts() {
    let store = CNContactStore()
    do {
        let predicate = CNContact.predicateForContacts(matchingName: "Appleseed")
                let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey] as [CNKeyDescriptor]
        let contacts =
            try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
        print("Fetched contacts: \(contacts)")
    } catch {
        print("Failed to fetch contact, error: \(error)")
        // Handle the error
    }
}

Error Message

실행해보면 위와 같이 오류가 발생하는 걸 볼 수 있다. 왜냐하면 연락처는 개인정보에 해당하기 때문에 사용자에게 권한을 요청받아야 한다.

Contacts Topics

Contacts 문서 하단에는 관련된 토픽을 볼 수 있는데, 위 문제를 해결하려면 Requesting Authorization to Access Contacts 문서를 보면 된다.

Requesting Authorization to Access Contacts

영어만 봐도 눈이 핑 돌아간다. @_@…

앱은 사용자가 권한을 부여할 때까지 연락처 항목에 액세스 할 수 없다고 설명하면서 사용자에게 액세스 권한이 있는지 확인하려면 authorizationStatus(for:) 메서드를 사용하라고 쓰여있다.

Configure Your Information Property List File

조금 더 밑으로 내려서 읽어보면, Info.plist 파일에 NSContactsUsageDescription 키를 추가해야 한다고 쓰여있다. 이 키의 값은 앱이 사용자의 연락처로 수행하는 작업을 설명하는 문자열이라고 한다.

그리고 그 다음으로는 사용자에게 권한 승인 요청을 하는 방법이 쓰여있다. requestAccess(for:completionHandler:) 를 사용하면 된다고 한다. 필요한 정보를 전부 알아냈으니 문제를 문서에서 제시하는 대로 앱을 수정해본다.

먼저 Info.plist 파일에 NSContactsUsageDescription 키를 추가해야 하는데, 이것만 봐선 뭔지 모를 거 같지만 애플에선 친절하게 해당 키가 실제로 무슨 이름을 가졌는지 문서로 제공하고 있다.

https://developer.apple.com/documentation/bundleresources/information_property_list/nscontactsusagedescription

NSContactsUsageDescription

Info.plist에 다음과 같이 추가해준다. 사용자에게 이 권한이 왜 필요한지 설명을 기록해야 한다.

이렇게 한 다음에 바로 연락처에 접근하는 게 아니라 requestAccess(for:completionHandler:)를 이용해 사용자에게 승인 요청을 한다. 권한이 있으면 연락처에 접근하고, 없으면 사용자에게 권한 요청을 한다.

let store = CNContactStore()

override func viewDidLoad() {
    super.viewDidLoad()

    self.requestCNContactStoreAccess {
        // 승인 요청 성공시 할 작업
        fetchContacts()
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
}

func requestCNContactStoreAccess(completion: @escaping () -> Void) {
    self.store.requestAccess(for: .contacts) { (granted, error) in
        // 에러 발생
        if let error = error {
            print(error.localizedDescription)
            return
        }

        // 사용자에게 승인 요청 성공시 탈출
        if granted {
            completion()
        }
    }
}

이 과정에서 fetchContacts에 있던 store 객체를 외부로 보냈다.

requestCNContactStoreAccess 메서드를 만들면서 처음으로 탈출 클로저를 사용해봤다. 이렇게 작성하니 코드가 깔끔해져서 가독성이 좋아졌다.

원래는 fetchrequest를 같이 하고 있었는데 리팩토링을 해서 메서드를 분리했다.

"AddressBookApp" Would Like to Access Your Contacts

사용자 승인 요청과 연락처 정보까지 잘 가져오는 것을 확인할 수 있다.

그런데 위 코드는 이름이 "Appleseed"인 사람의 연락처만 갖고 온다. 모든 연락처 정보를 가져올 수 있게 fetchContacts() 메서드를 수정해보자. 그리고 가져온 정보를 CNContact 배열에 저장한다.

let store = CNContactStore()
var contatcs: Array<CNContact> = []

func fetchContacts() {
    let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey] as [CNKeyDescriptor]
    let request = CNContactFetchRequest(keysToFetch: keysToFetch)

    do {
        try self.store.enumerateContacts(with: request, usingBlock: { (contact, stopPointer) in
            self.contacts.append(contact)
        })
    } catch {
        print("Failed to fetch contact, error: \(error)")
    }
}

연락처 정보를 가져올 때 모든 정보를 가져오는 게 아니라 keysToFetch 에 있는 값만 갖고 오기 때문에 필요한 값을 적어줘야 한다.

그리고 진짜 마지막으로.. 가져온 데이터를 테이블뷰를 통해 뿌려준다.

// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return contacts.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withpermalink: "cellIdentifier", for: indexPath)

    let contact = contacts[indexPath.row]
    cell.textLabel?.text = contact.familyName
    cell.detailTextLabel?.text = contact.givenName

    return cell
}

Log

완성!

완성 화면

애플 개발자 문서는 진짜 잘 정리되어있다는 걸 또 한번 느꼈다.