初識 iOS 9 中新的聯絡人框架

weixin_34337265發表於2016-01-12

作者:gabriel theodoropoulos,原文連結,原文日期:2015-09-29
譯者:BridgeQ、星夜暮晨;校對:小鐵匠Linus;定稿:

同每一代 iOS 系統版本的更新一樣,最新發布的 iOS 9 為使用者和開發者帶來了許多新特性以及原有功能的改善。在這個版本中,我們不僅看到了很多首次推出的 API,還可以看到許多針對原有框架和類庫的更新。此外,一些舊版本的 API 被標記為 deprecated(校對注:意為新版本已被棄用),而使用了更好的 API 來替代。iOS 9 中,新的 Contacts framework (聯絡人框架)是最好的例子了,它是來代替原有 AddressBook framework 的。該框架更加符合技術潮流且簡單易用。

過去使用過 AddressBook API 的開發者經常會抱怨這個舊有的聯絡人框架非常難用,大家普遍認為它不易理解而且很難管理,對開發者菜鳥來說更是如此。然而,這些都已成為歷史,全新的聯絡人框架非常簡單易用,通過它你可以很容易地查詢、建立和更新聯絡人資訊,開發時間被極大地減少,擴充套件更新也可以很快地實現。

37776-72f9ec4dbfe87064.jpg

在接下來的部分中,我們將重點介紹 Contacts framework 中最主要的內容。如果需要更多的技術細節,你可以去蘋果的官方文件中查詢,或者觀看 WWDC 2015 session 223 video 來學習。

首先,我們來談論一件非常重要的事情,那就是使用者隱私。使用者總是在被詢問是否允許應用程式訪問他們的聯絡人資料,如果被允許,應用就可以自由地同聯絡人資料庫進行互動,而如果使用者禁止訪問,那麼應用必須尊重使用者的選擇,即無法同聯絡人資料進行任何互動。稍後,我們會談論使用者隱私的更多細節,我們將看到如何通過程式的手段來處理所有可能的情況。此外,要記住使用者總是有資格在手機設定的選項中更改應用的授權狀態,所以在你想要執行與聯絡人資料相關的任務前,總應該檢查你的應用是否允許訪問聯絡人資料。

聯絡人資料的主要來源是裝置內建的資料庫。然而,新的聯絡人框架不僅可以檢索這個資料庫,實際上,它還可以對別的來源進行資料的檢索,比如通過你的 iCloud 賬戶(當然是在你已經連線了 iCloud 賬戶的情況下),並且返回檢索到的聯絡人結果。這是非常有用的,因為你不需要單獨再進行某個來源的檢索,你一次就能夠檢索所有資料,並且可以隨意管理。

新的聯絡人框架包括了許多不同功能的類,所有類都非常重要,但其中使用最多的一個是 CNContactStore,它代表聯絡人資料庫,並且提供了大量的操作方法,比如查詢、儲存、更新聯絡人記錄、授權檢查、授權請求等。 CNContact 表示一條聯絡人記錄,並且它的內部屬性都是不可變的,如果你想要建立或者更新一條已經存在的聯絡人記錄,你應該使用它的可變版本 CNMutableContact。值得注意的是,當你使用聯絡人框架時,尤其是進行聯絡人查詢時,你應該總是在後臺執行。如果一條聯絡人記錄的查詢花費較長的時間並且在主執行緒執行的話,你的應用會無法響應,這會使應用的使用者體驗非常糟糕。

當匯入聯絡人資料到應用中時,幾乎不需要匯入所有的聯絡人屬性。在所有聯絡人框架允許的搜尋範圍中檢索所有已存在的聯絡人資料,是一個非常費資源的操作,你應該儘量避免這樣去做,除非你確定你真的需要使用所有的聯絡人資料。可喜的是,新聯絡人框架提供了僅檢索部分結果的方式,即檢索一個聯絡人的部分屬性。比如說,你可以只查詢聯絡人的姓、名、家庭郵件地址和家庭電話號碼,而撇開所有那些你不需要的資料。

除了通過程式設計的方式來使用聯絡人框架,它還提供了一些預設的使用者介面(UI),可以讓你的應用以直觀可視的方式訪問聯絡人資料。預設提供的使用者介面跟手機自帶聯絡人應用幾乎一樣,也就是說同樣有一個聯絡人選擇控制器(contact picker view controller)用來選擇聯絡人和聯絡人屬性,一個聯絡人檢視控制器用來展示聯絡人的詳細資訊並且處理某些操作(例如,撥打電話)。

上面所有這些方面我們都將在本教程的後續部分詳細介紹。再次宣告,你可以通過官方文件來學習所有這些方面的詳細內容。接下來,我們先來看一下示例程式是什麼樣子,然後我們開始學習使用新的聯絡人框架中的各種類,你會發現新的聯絡人框架非常易用而且有趣。

示例應用簡介

我試圖在本篇教程的示例應用中,儘可能給大家全面地展示這個新框架的功能。實際上,在以下部分我將會給大家展示:

  1. 檢查應用是否准許訪問聯絡人,並且如何請求授權。
  2. 使用三種不同的方式檢索聯絡人。其中一種方式將會涉及 Picker View Controller 的使用。
  3. 訪問檢索到的聯絡人屬性,並調整為適當的顯示格式。
  4. 使用預設的 Contacts UI 來實現選擇、檢視以及編輯聯絡人。
  5. 建立一個新的聯絡人。

我將這個示例應用命名為 Birthdays,因為其目的就是展示所有聯絡人生日資訊。同時,還會顯示聯絡人的全名、頭像(如果有的話)以及家庭 email 地址。雖然在理想情況下,這個應用的主要功能應該是進行生日提醒,不過我們並不會處理諸如通知、傳送簡訊之類的事情。

這個應用是基於導航欄設計的,包含了以下幾個部分:

ViewController 是應用啟動時的預設展示介面。它將會展示我在上面所提及的所有資訊,包括匯入的聯絡人,提供檢索聯絡人的選項(右邊的導航欄按鈕)、建立新的聯絡人(左邊的導航欄按鈕)以及通過單擊單元格來檢視聯絡人的具體資訊:

37776-1bca58b62e6ec464.png
預設展示介面

聯絡人詳情將會通過內建的聯絡人檢視控制器進行展示。你會在後面看到,這個控制器既可以展示所有的聯絡人資訊,也可以只顯示你感興趣的內容。

在接下來的內容中,檢索聯絡人將會是一個非常有意思的部分。我會為大家展示三種進行檢索的方法,我將使用三種不同的思路:

  1. 第一種方法,我們將通過填寫聯絡人姓名(或者姓名的一部分),點選鍵盤上的返回按鈕,然後應用就會檢索所有匹配該姓名的聯絡人。
  2. 在下面這個截圖中您可以看到,螢幕中央有一個選擇器檢視。我們將會用它來尋找所有生日滿足對應月份要求的聯絡人,月份可以在這個選擇器中進行選擇,通過點選右上角的 "Done" 導航欄按鈕,還會顯示檢索進度。
  3. 我們將使用框架所提供的預設選擇器檢視控制器,來直接檢視和檢索聯絡人。值得注意的是,這個控制器可以自定義可用的聯絡人,此外其顯示風格也可以自定義。大家會在後面部分看到如何操作。
37776-4198a619ed917fcd.png

這個就是選擇器檢視控制器,其中只顯示了有生日記錄的聯絡人:

37776-d1dfe226ad2e39d4.png

我們這個應用的最後一個部分就是建立新聯絡人了。這個任務相當簡單,為了簡單起見,我們使用下面的這個檢視控制器來輸入我們要建立的聯絡人姓名、家庭 email 地址以及生日(我們不處理頭像,這玩意兒對於我們的示例來說並不重要)。

37776-96b08e14fc175968.png

這個示例應用所使用的資料(作為例子的聯絡人資訊)都是 iPhone 模擬器預設資料庫中所包含的。這些聯絡人資訊對我們來說就已經足夠了。當然,您也可以使用自己裝置中的聯絡人資訊,或者給模擬器中新增新的聯絡人。預設情況下模擬器所提供的聯絡人是沒有頭像的,但是你可以從照片庫中簡單地為聯絡人新增頭像。

一如往常,您可以下載這個起始專案,因為我們接下來所做的工作將從它開始。一旦您下載完成,您可以開啟這個專案然後瀏覽一下其中我新增的那些程式碼。當您覺得準備好的時候,就可以繼續閱讀下一個部分了。

Contact Store 類

我們在處理聯絡人的時候,經常使用的一個基礎類就是 CNContactStore 類。這個類實際上代表了裝置中所擁有的聯絡人資料庫,它負責管理應用和實際資料庫之間的資料互動操作。具體而言,它負責處理諸如檢索、儲存、更新聯絡人以及組記錄(group records)之類的工作。簡而言之,在使用聯絡人資訊的時候,這個類是絕大多數我們所能做的任務的起始點,並且我們將會在下面要寫的程式碼中看到它。

此外,我在概述中也提及了,使用者隱私是 iOS 中重要的組成部分,因此在使用的時候千萬要小心。眾所周知,使用者可以准許或者禁止第三方應用訪問他們的聯絡人資訊,因此確保您的應用在任何時候都准許顯示與任務有關的聯絡人資訊就變得至關重要。使用 CNContactStore 類,您可以檢查您應用當前的認證狀態,然後根據實際情況進行相應的處理。要記住,每當使用者在檢視設定的時候,都很有可能禁止應用訪問他們的聯絡人資訊,即使他們在應用初次啟動的時候同意了這個請求,因此在執行任務前一定要確保您有許可權執行,然後根據實際情況進行相應的處理。如果不這樣做的話,往往會導致極差的使用者體驗,這也是您應當極力避免的。在本教程的這部分裡,我們會認真考慮示例應用的認證狀態。我們接下來將要做的,就是讓你能夠在專案中隨意使用它。

您將會發現,Contact Store 類很擅長處理下面的情形(和其他方式相比):

  • 檢索聯絡人
  • 建立(儲存)新聯絡人,以及更新聯絡人資訊
  • 使用 Contact Picker 檢視控制器來選擇聯絡人

要時刻記住,在整個類中我們只需要初始化一個 CNContactStore 物件,並使用它即可。另一方面,雖然我們可以在需要的時候建立一個新的 CNContactStore 物件,但是由於這個類代表了程式碼中的聯絡人資料庫,那麼為什麼還要建立多個資料庫的例項呢?因此,讓我們從開啟 AppDelegate.swift 檔案開始吧,宣告並初始化一個 CNContactStore 屬性。在類的頂部新增以下程式碼:

var contactStore = CNContactStore()

當然,在類的頂部匯入下面這個框架也是必要的:

import Contacts

好的!現在,在我們處理應用認證狀態以及所有相關操作之前,讓我們先寫兩個簡便的輔助方法。注意這兩個方法並不是必須的,沒有它們我們仍能夠很好地工作。不過,實現這些有特定功能的方法將會帶來極大的便利。

因此,第一個方法會讓其他類訪問應用委託 (AppDelegate) 變得更容易些。正常情況下,為了訪問應用委託我們需要使用下面這條語句:

UIApplication.sharedApplication().delegate as! AppDelegate

然而,我個人覺得,每次獲取應用委託的時候都要寫上面這段程式碼,實在是太煩人了。我們為什麼不寫一個類方法呢?

class func getAppDelegate() -> AppDelegate {
    return UIApplication.sharedApplication().delegate as! AppDelegate
}

通過這個方法,我們可以以一個非常簡單的方式來訪問應用委託中的所有屬性和方法。例如,我們可以從專案中的任意一個類中使用下面這行程式碼訪問 contectStore 屬性。

AppDelegate.getAppDelegate().contactStore

第二個加在 AppDelegate.swift 檔案中的輔助方法將會顯示一個帶有訊息的警告控制器(alert controller),我們每次使用它的時候只需要提供一個引數即可。實現起來並不複雜,但是我們在這裡做了一點小小的特殊動作;警告控制器必須通過檢視控制器來進行顯示,然而應用委託中並沒有檢視控制器的存在。要解決這個問題,我們首先必須要找到當前顯示在應用視窗上的頂層檢視控制器,然後在這個檢視控制器中顯示警告控制器。我們可以這麼做:

func showMessage(message: String) {
    let alertController = UIAlertController(title: "Birthdays", message: message, preferredStyle: UIAlertControllerStyle.Alert)
 
    let dismissAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) { (action) -> Void in
    }
 
    alertController.addAction(dismissAction)
 
    let pushedViewControllers = (self.window?.rootViewController as! UINavigationController).viewControllers
    let presentedViewController = pushedViewControllers[pushedViewControllers.count - 1]
 
    presentedViewController.presentViewController(alertController, animated: true, completion: nil)
}

現在,我們要做的就是重點了,我們來處理應用的認證狀態。該狀態是通過 CNAuthorizationStatus 列舉來表示的,這個列舉屬於 CNContactStore 類。它包含了下列四個列舉值:

  1. NotDetermined:這個狀態說明使用者暫未決定是否允許訪問聯絡人資料庫。當應用第一次安裝在裝置上時將處於此狀態。
  2. Restricted:這個狀態說明應用不僅不能夠訪問聯絡人資料,並且使用者也不能在設定中改變這個狀態。這個狀態是某些被啟用的限制所導致的(比如說家長控制)。
  3. Denied:這個狀態說明使用者不允許應用訪問聯絡人資料。這個狀態只能夠被使用者改變。
  4. Authorized:這個狀態是所有應用都希望擁有的,這表明應用能夠自由訪問聯絡人資料庫,然後根據聯絡人資料來處理某些任務。

有一點在這需要說明清楚:應用安裝之後,當且僅當使用者第一次嘗試執行涉及聯絡人資料(比如說檢索聯絡人)的操作時,iOS 才會顯示一個預定義的警告控制器,詢問使用者是否給應用授權:

37776-38b1e40c55261822.png

如果使用者准許授權,那麼萬事大吉。然而,如果使用者禁止授權的話,那麼應用就不能夠獲取聯絡人資料了,自然也沒法做任何操作了。在我們的示例應用中,對於這個特殊的情況,我們會展示一個自定義的警告訊息(使用我們此前定義的函式),告知使用者他必須在設定中准許我們的應用訪問聯絡人資料。我們在一個新的函式中處理這個狀況,接下來我們會對其進行實現。顯然,在這個函式中我們會盡可能考慮到所有的認證狀態。我們先來看看函式吧,然後對其進行簡短的分析:

func requestForAccess(completionHandler: (accessGranted: Bool) -> Void) {
    let authorizationStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts)
 
    switch authorizationStatus {
    case .Authorized:
        completionHandler(accessGranted: true)
 
    case .Denied, .NotDetermined:
        self.contactStore.requestAccessForEntityType(CNEntityType.Contacts, completionHandler: { (access, accessError) -> Void in
            if access {
                completionHandler(accessGranted: access)
            }
            else {
                if authorizationStatus == CNAuthorizationStatus.Denied {
                    dispatch_async(dispatch_get_main_queue(), { () -> Void in
                        let message = "\(accessError!.localizedDescription)\n\nPlease allow the app to access your contacts through the Settings."
                        self.showMessage(message)
                    })
                }
            }
        })
 
    default:
        completionHandler(accessGranted: false)
    }
}

觀察上面這個函式,你會發現它包含了一個 completionHandler 閉包,當應用准許訪問聯絡人的時候通過傳遞一個 true 值來呼叫,不可訪問的時候傳遞一個 false 值。某些狀況非常簡單,比如說 Authorized 或者 Restricted,通過 completionHandler 中傳遞的值可以很清楚的知道其操作。然而,有趣的是,這裡 DeniedNotDetermined 狀態的處理竟然是相同的,它們都會呼叫 requestAccessForEntityType:completionHandler,因此應用會請求授權。我之前提到的自定義訊息只會在 Denied 狀態下顯示。

值得注意的是, requestAccessForEntityType:completionHandler: 以及 authorizationStatusForEntityType: 這兩個方法都需要一個 CNEntityType 引數。這是一個列舉值,它其中只包含了一個名為 Contacts 的值。這個列舉實際上指定了我們需要請求訪問的實體。

從下一節開始,上面這個函式將會被多次使用。每次我們執行涉及到聯絡人資料的操作時,我們都會使用這個函式,我們要確定聯絡人資料是否准許訪問,當然還要處理每個可能的情況,以避免產生差的使用者體驗。我們暫時沒有發現問題,因為我們準備了一些可重用的程式碼,能夠讓我們接下來的工作更為便利。

使用斷言(Predicates)來檢索聯絡人

正如我在概覽一節闡述過的,我們打算實現三種不同的方式來檢索聯絡人資料。其中之一是通過在文字框中填寫我們想要檢索的聯絡人全名或部分名字(無論是姓還是名),然後向聯絡人框架請求結果。這就是我們即將開始的操作,實現此功能的核心函式是 unifiedContactsMatchingPredicate:keysToFetch:error:

這個函式作為 CNContactStore 類的一部分,接受兩個重要的引數:

  1. Predicate:為了得到返回結果而用以檢索的 NSPredicate 物件。需要特別注意的是,這裡只接受從 CNContact 類中得到的斷言,而不接受您自己建立的通用斷言(看這裡)。在 CNContact 類中所有支援的斷言函式中,有一個名為 predicateForContactsMatchingName: 的函式,我們將會使用它來生成斷言。
  2. keysToFetch:通過設定此引數,您可以指定您想要檢索的部分聯絡人資料。這是一個描述需要檢索的聯絡人(CNContact 物件)屬性的字串陣列。框架提供了預定義的常量字串值,可以用作關鍵詞來使用。

值得注意的是,這個方法可能會丟擲異常,因此它必須要在 do-catch 宣告中使用 try 關鍵字來進行修飾。然後在語句的 catch 模組中對錯誤情況進行處理。

unifiedContactsMatchingPredicate:keysToFetch:error: 函式的結果包含了匹配給定斷言的 CNContact 物件的一個陣列,或者當錯誤發生的時候返回 nil。

將上面的內容牢記在心,現在就可以開始實現程式碼了。現在開啟 AddContactViewController.swift 檔案,然後直接來到開啟的類上方。在這裡也要匯入聯絡人框架,如果沒有它,我們就沒法做事了:

import Contacts

我們現在前往 textFieldShouldReturn: 委託方法中。一開始我們會用上之前在應用委託中建立的最後一個函式,並且檢查應用是否有許可權讀取聯絡人,以便繼續:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
 
        }
    }
 
    return true
}

在准許訪問的情況下,為了匹配聯絡人,我們要準備好將進行檢索的斷言和關鍵詞。除此之外,我們還將宣告兩個變數:一個用於儲存結果的陣列(如果有結果的話),以及如果沒有檢索到匹配聯絡人或者檢索請求失敗的時候,用以儲存自定義訊息的字串變數。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!)
            let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey]
            var contacts = [CNContact]()
            var message: String!
 
        }
    }
 
    return true
}

仔細觀察我們是如何宣告斷言和關鍵片語的,隨後我們繼續。在下一步中,我們使用 try 關鍵字來檢索聯絡人資料,如果該操作成功的話,那麼查詢結果就會寫入到我們此前初始化的 contacts 陣列當中。如果沒有找到聯絡人或者檢索失敗的話,我們就會設定一個即將用來展示的自定義訊息;通過這幾個操作我們對這個函式的實現操作就即將完成了:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!)
            let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
            var contacts = [CNContact]()
            var message: String!
 
            let contactsStore = AppDelegate.getAppDelegate().contactStore
            do {
                contacts = try contactsStore.unifiedContactsMatchingPredicate(predicate, keysToFetch: keys)
 
                if contacts.count == 0 {
                    message = "No contacts were found matching the given name."
                }
            }
            catch {
                message = "Unable to fetch contacts."
            }
 
 
            if message != nil {
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    AppDelegate.getAppDelegate().showMessage(message)
                })
            }
            else {
 
            }
        }
    }
 
    return true
}

如你所見,我們現在遺留了一個 else 語句暫未處理,我們之後會回來補全這個遺漏的程式碼的。這裡最重要的是觀察我們是如何根據給定名字匹配聯絡人資料的,並且是如何處理非預期狀況的。

展示檢索到的聯絡人

最好的情況就是,我們的檢索請求成功地返回了匹配到的聯絡人資訊,接著將他們顯示在 ViewController 類的表檢視(tableview)中,這就很有必要了。然而,我們的第一步還是要讓 ViewController 類也得到檢索到的聯絡人資訊,因為我們的所有檢索操作都是在 AddContactViewController 中發生的。最好也是最簡單的方法就是,使用眾所周知的協議委託模式(Delegation pattern)。那麼,讓我們朝著這個方向進行吧,繼續給我們的示例應用添磚加瓦。

AddContactViewController.swift 檔案的類上方,建立如下所示的協議,這個協議只有一個委託方法:

protocol AddContactViewControllerDelegate {
    func didFetchContacts(contacts: [CNContact])
}

通過使用上面這個委託方法,我們不僅可以讓 ViewController 類知曉檢索到的聯絡人資訊,還可以把它傳遞給新檢索到的聯絡人。

接著,在 AddContactViewController 類中新增下面這個委託宣告:

var delegate: AddContactViewControllerDelegate!

還記得嗎,我們在上一節中的 textFieldShouldReturn: 方法中遺留了一個 else 沒有實現,現在是時候新增缺失的東西了。實際上,缺失的程式碼只有兩行而已:第一行是呼叫上面宣告的委託方法,第二行則是通過導航控制器棧來推出檢視控制器。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            ...
 
            if message != nil {
                ...
            }
            else {
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    self.delegate.didFetchContacts(contacts)
                    self.navigationController?.popViewControllerAnimated(true)
                })
            }
        }
    }
 
    return true
}

如您所見,當我們處理 UI 的時候一直都使用主執行緒。這是一個非常重要的細節,您應當牢記於心,否則的話 UI 就很有可能不會及時進行更新,應用也有可能出現一些無法預料的奇怪行為。

這時候我們就可以前往 ViewController.swift 檔案來處理檢索到的結果了。一開始,我們也需要在這個類中匯入 Contacts 框架:

import Contacts

接下來,我們需要實現我們新的自定義協議,因此我們需要在類的頭部新增這個協議名:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, AddContactViewControllerDelegate

現在,是時候來宣告一個 CNContact 物件的陣列了。這個陣列將會儲存所有從檢索請求返回的聯絡人資料,它甚至還是表檢視的資料來源。因此,在 ViewController 類的頂端新增以下程式碼:

var contacts = [CNContact]()

除此之外,我們還需要更新接下來將要進行展示的表檢視的行數:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return contacts.count
}

在我們實現我們先前宣告的委託方法之前,我們需要讓 ViewController 類成為 AddContactViewControllerDelegate 協議的委託。這會在 prepareForSegue: 函式中實現:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let identifier = segue.identifier {
        if identifier == "idSegueAddContact" {
            let addContactViewController = segue.destinationViewController as! AddContactViewController
            addContactViewController.delegate = self
        }
    }
}

最後,我們必須要實現我們自定義的委託方法。在委託方法中,我們將依次獲取所有返回的聯絡人資料,然後將它們新增到 contacts 陣列中即可。當然,我們會重新載入表檢視,以便讓其顯示新的聯絡人。

func didFetchContacts(contacts: [CNContact]) {
    for contact in contacts {
        self.contacts.append(contact)
    }
 
    tblContacts.reloadData()
}

現在讓我們來顯示這些聯絡人資訊吧!對於每個單元格(cell)來說,我們都要顯示聯絡人的姓和名,如果存在的話則還要顯示聯絡人的生日、頭像以及家庭 email。具體的實現你會在下面的程式碼中看到,我們將會修改很多東西,不過這足夠讓你理解聯絡人屬性是如何被訪問的了:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell
 
    let currentContact = contacts[indexPath.row]
 
    cell.lblFullname.text = "\(currentContact.givenName) \(currentContact.familyName)"
 
 
    // 設定生日資訊
    if let birthday = currentContact.birthday {
        cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)"
    }
    else {
        cell.lblBirthday.text = "Not available birthday data"
    }
 
 
    // 設定聯絡人頭像
    if let imageData = currentContact.imageData {
        cell.imgContactImage.image = UIImage(data: imageData)
    }
 
 
    // 設定聯絡人的家庭 email 地址
    var homeEmailAddress: String!
    for emailAddress in currentContact.emailAddresses {
        if emailAddress.label == CNLabelHome {
            homeEmailAddress = emailAddress.value as! String
            break
        }
    }
 
    if homeEmailAddress != nil {
        cell.lblEmail.text = homeEmailAddress
    }
    else {
        cell.lblEmail.text = "Not available home email"
    }
 
 
    return cell
}

讓我們來通覽一遍上面的實現。首先,我們將姓和名連線起來,將其賦給了 “lblFullname” 標籤。接下來,我還會為你展示另一種實現方式,不過現在我們就這麼做。接著,我們設定生日資訊。如果生日資料存在的話,我們就通過最簡單的方式將其展示出來。注意到這只是一個臨時方法 (temporary approach),之後我們會用正確的方式來處理這個出生日期。同樣,你必須知道生日資料並不是一個 NSDate 物件,其實,它是一個 NSDateComponents 物件,它可以轉換為 NSDate 後再轉換為 String

接下來我們要設定的是圖片資料。如果不存在的話,你唯一能在這看到的就只是 imgContactImage 圖片檢視的背景顏色了,這個顏色是我在自定義的單元格 xib 檔案中設定好的。

最後,我們要設定的就是家庭 email 地址了。你可以注意到的是,我們使用迴圈來遍歷了所有的 email 地址,直到我們找到所需要的那個為止。這是因為聯絡人所擁有的 emailAddresses 屬性包含了被標記為值 (CNLabeledValue) 物件所擁有的全部 email 地址。最後,如果家庭 email 地址找到的話,我們就將其分別賦值給對應的標籤,否則的話我們就將其設定為上面你所看到的訊息。

如果你現在執行這個應用的話,輸入您想要選擇的聯絡人名稱,上面的實現或許可用,也可能不起作用。再次嘗試的話應用會崩潰掉,但是你不必擔心。我們之後會修復這個問題。我故意沒有給你上面方法的完整實現,因為上面的方法更容易展示應用是如何工作的。

重新檢索聯絡人

這個應用可能會崩潰的原因在於,當你請求聯絡人資料的時候,它可能並沒有檢索到所有的值。為此,CNContact 類包含了一個名為 isKeyAvailable: 的方法,必須要在訪問任何聯絡人屬性之前使用。比如說,在我們檢視顯示生日、頭像以及 email 地址之前,我們應該新增如下檢查:

if currentContact.isKeyAvailable(CNContactBirthdayKey) {
    ...
}
 
if currentContact.isKeyAvailable(CNContactImageDataKey) {
    ...
}
 
if currentContact.isKeyAvailable(CNContactEmailAddressesKey) {
    ...
}

如果沒有對應的關鍵詞的話,那麼必須要採取合適的操作來重新檢索聯絡人資料,然後嘗試再次顯示。這就是我們在這所要做的,明確來說我們要在 ViewController 類中建立一個新的函式。然而,在此之前,我們需要通過新增 isKeyAvailable: 方法來修復聯絡人詳情的顯示問題。實際上,我們建立一個條件來檢查所有的不可用關鍵詞即可,而不是為上面所提到的屬性使用三個不同的條件語句,並且如果有關鍵詞缺失的話,我們就呼叫下面將要實現的這個函式,以便讓其重新檢索聯絡人資料。我故意沒有包含進聯絡人名字的關鍵詞,因此我們可以在下一個部分看到更多內容。

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell
 
    let currentContact = contacts[indexPath.row]
 
    cell.lblFullname.text = "\(currentContact.givenName) \(currentContact.familyName)"
 
    if !currentContact.isKeyAvailable(CNContactBirthdayKey) || !currentContact.isKeyAvailable(CNContactImageDataKey) ||  !currentContact.isKeyAvailable(CNContactEmailAddressesKey) {
        refetchContact(contact: currentContact, atIndexPath: indexPath)
    }
    else {
        // Set the birthday info.
        if let birthday = currentContact.birthday {
            cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)"
        }
        else {
            cell.lblBirthday.text = "Not available birthday data"
        }
 
        // Set the contact image.
        if let imageData = currentContact.imageData {
            cell.imgContactImage.image = UIImage(data: imageData)
        }
 
        // Set the contact's work email address.
        var homeEmailAddress: String!
        for emailAddress in currentContact.emailAddresses {
            if emailAddress.label == CNLabelHome {
                homeEmailAddress = emailAddress.value as! String
                break
            }
        }
 
        if homeEmailAddress != nil {
            cell.lblEmail.text = homeEmailAddress
        }
        else {
            cell.lblEmail.text = "Not available home email"
        }
    }
 
    return cell
}

上面呼叫的 refetchContact:atIndexPath: 函式是我們現在要實現的。此外,我覺得我們新增的那行條件語句非常明確,因此你能輕易理解其邏輯。注意到做完這個改動之後,應用就不再會發生崩潰了,即使返回的結果中出現了不可用的關鍵詞。

現在,讓我們看看這個新函式吧:

func refetchContact(contact contact: CNContact, atIndexPath indexPath: NSIndexPath) {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
 
            do {
                let contactRefetched = try AppDelegate.getAppDelegate().contactStore.unifiedContactWithIdentifier(contact.identifier, keysToFetch: keys)
                self.contacts[indexPath.row] = contactRefetched
 
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    self.tblContacts.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
                })
            }
            catch {
                print("Unable to refetch the contact: \(contact)", separator: "", terminator: "\n")
            }
        }
    }
}

首先,我們會檢查應用是否有許可權訪問聯絡人資料庫。接著,我們會指定想要檢索的特定結果關鍵詞,接著我們嘗試為給定的聯絡人重新進行資料檢索。注意到這個時候我們使用了一個新的方法來執行檢索操作,也就是 unifiedContactWithIdentifier:keysToFetch:。這個方法的功能是重新檢索一個通過識別符號引數值所指定的聯絡人資料。一旦結果得到返回,我們將會將位於 contacts 陣列中的舊聯絡人物件替換為新的。最後,我們就重新載入表檢視的特定行即可。

這時候你可以自己重新執行一遍應用。重新檢索聯絡人資料是一項您最好經常執行的任務,以防止某些資料發生丟失,這樣你就可以確保應用不會為使用者帶來出乎意料的“驚喜”。

輸出格式化

目前為止,在單元格上顯示每個聯絡人的生日資訊之前,我們並沒有對其進行正確的格式化操作。我們只是簡單的連線並展示這些生日屬性而已,但是現在我們已經完成了前面的事情,是時候來處理它了。

我們通過在 ViewController 類中建立新的自定義函式來解決這個問題。在其中,我們會使用 NSDateFormatter 物件將日期轉換為一個本地化的字串,但首先,我們需要將日期元件 (date components,日期的每個部分) 轉換為 NSDate 物件。讓我們來看看這個新函式:

func getDateStringFromComponents(dateComponents: NSDateComponents) -> String! {
    if let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents) {
        let dateFormatter = NSDateFormatter()
        dateFormatter.locale = NSLocale.currentLocale()
        dateFormatter.dateStyle = NSDateFormatterStyle.MediumStyle
        let dateString = dateFormatter.stringFromDate(date)
 
        return dateString
    }
 
    return nil
}

上面這個方法的引數是一個被 NSDateComponents 物件(在我們的例子中是出生日期)所表示的日期。返回值自然是一個字串。為了將 dateComponents 物件轉換為 NSDate 物件,只需要新增一行程式碼即可。我們使用 NSCalendar 來進行轉換,以及使用將會初始化的日期格式化器 (date formatter) 對日期物件進行處理。將這個日期格式化器的區域設定為當前裝置的區域,這是一個非常有必要的操作,只有這樣才能夠取得本地化的日期描述資訊。最後,我們要設定日期的樣式(不要太長,也不要太短),再執行最後的轉換即可。最終,轉換過的值將返回給呼叫者。

現在,讓我們來完善出生日期的顯示吧。其實,只需要呼叫上面這個方法即可:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    ...
 
    if !currentContact.isKeyAvailable(CNContactBirthdayKey) || !currentContact.isKeyAvailable(CNContactImageDataKey) ||  !currentContact.isKeyAvailable(CNContactEmailAddressesKey) {
        refetchContact(contact: currentContact, atIndexPath: indexPath)
    }
    else {
        // 設定生日資訊
        if let birthday = currentContact.birthday {
            cell.lblBirthday.text = getDateStringFromComponents(birthday)
        }
        ...
    }
 
    return cell
 
}

非常好,現在出生日期的顯示就更加高大上了。

現在讓我們來看看一些關於姓名顯示的有趣東西吧。CNContact 類提供了一個內建的格式化器,用以幫助我們輕鬆格式化兩類資料:聯絡人的全名 (CNContactFormatter) 以及地址 (CNPostalAddressFormatter)。這裡我們將使用第一種,因此,聯絡人的全名會被 Contacts 框架自動格式化。

首先,我們先回到最後一次修改聯絡人的方法,如下所示:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell
 
    let currentContact = contacts[indexPath.row]
 
    cell.lblFullname.text = CNContactFormatter.stringFromContact(currentContact, style: .FullName)
 
    ...
 
    return cell
 
}

如你所見,cell.lblFullname.text = “(currentContact.givenName) (currentContact.familyName)” 這行語句被下面這行替代了:

cell.lblFullname.text = CNContactFormatter.stringFromContact(currentContact, style: .FullName)

顯然,我們不再需要將聯絡人的姓與名連線起來而作為全名。CNContactFormatter 已經替我們完成了這項工作,同時它還提供了一個本地化字串(取決於裝置的本地化設定,通過合適的次序來設定名字部分)。

然而,上面這行程式碼還是會導致一些問題,因為聯絡人格式化器需要訪問所有與聯絡人名字相關聯的關鍵詞,即使這些關鍵詞我們並沒有在檢索的關鍵詞陣列中。不過,我們也沒有必要一個一個地將它們全部寫出來。所有相關的關鍵詞都可以通過關鍵詞描述符 (key descriptor) 所指定,這個描述符被用來替代關鍵詞陣列中的單一關鍵詞。

為了說明得更具體一些,前往 AddContactViewController 檔案的 textFieldShouldReturn: 方法。在那裡,將這行程式碼:

let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]

替換為下面這行使用關鍵詞描述符的程式碼:

let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]

正如上面所示,描述符格式化的方式是非常明確的。除此之外,其他的關鍵詞都保持不變。

上面的變化也必須在 refetchContact: 方法(在 ViewController 類中)進行。你所需要做的就是將 keys 陣列定義替換為上面的那行程式碼,所以放手向前吧:

func refetchContact(contact contact: CNContact, atIndexPath indexPath: NSIndexPath) {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
 
            ...
        }
    }
}

至此,我們已經給程式碼做完了所有與格式化相關的修改了。當然,你仍然可以使用單個關鍵詞來檢索單個名字,不過這得取決於你的具體需求了。

使用自定義過濾器檢索聯絡人

我在此教程中提到的首要事情之一就是,如何使用斷言來檢索聯絡人。我們使用 Contacts 框架中的斷言來匹配給定名字的聯絡人,但是你是否記得,通常情況下這個方法有一個缺點。我們必須使用框架內建的斷言,而我們無法對其進行自定義。那麼問題來了,我們如何實現自定義的過濾器來檢索聯絡人呢?

對我們的示例應用來說,問題可以變得更為具體一些,比如,如何才能基於聯絡人的生日來檢索呢?在 AddContactViewController 類中有一個用於展示所有月份的選擇器檢視,因此現在我們所想做的是,選擇一個月份,然後單擊“完成”按鈕,最後就可以獲得所有出生月份和所選月份相同的記錄了。

好吧,正如你所猜想的,的確是有一個辦法可以“應用”自定義的過濾器,但是會使整個過程比使用斷言還麻煩。通常情況下,我們所看到的方法是基於 CNContectStore 類中的 enumerateContactsWithFetchRequest(_:usingBlock) 方法,這也是蘋果針對這種情況而建議使用的。這個方法將會檢索所有的聯絡人,因此自定義的查詢標準 (criteria) 能夠在程式碼塊 (閉包) 中設定,比如說比較屬性值或者使用其他自定義的邏輯,並在最後獲得你所需要的聯絡人資訊。

在我們的例子中,我們將要檢查兩個東西:首先,我們必須要確保每個聯絡人的生日都已被設定,這樣可以避免任何可能出現的崩潰。其次,我們只要比較生日月份和在選擇器檢視中所選月份即可,如果有匹配的,就將這個聯絡人放到陣列當中。這個做法十分簡單,因為生日是 NSDateComponents 物件,因此我們能夠直接訪問其月份。此外,剩下的操作也十分簡單。我們將看到的所有操作已經在之前的部分展示過了,並且我也進行了介紹。接下來,我們會在 AddContactViewController 類的 performDoneItemTap 自定義方法中寫下這些新程式碼,這樣就可以在檢視控制器中的“完成”按鈕被按下的時候就基於所選月份來檢索聯絡人了。

程式碼在此:

func performDoneItemTap() {
    AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in
        if accessGranted {
            var contacts = [CNContact]()
 
            let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
 
            do {
                let contactStore = AppDelegate.getAppDelegate().contactStore
                try contactStore.enumerateContactsWithFetchRequest(CNContactFetchRequest(keysToFetch: keys)) { (contact, pointer) -> Void in
 
                    if contact.birthday != nil && contact.birthday!.month == self.currentlySelectedMonthIndex {
                        contacts.append(contact)
                    }
                }
 
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    self.delegate.didFetchContacts(contacts)
                    self.navigationController?.popViewControllerAnimated(true)
                })
            }
            catch let error as NSError {
                print(error.description, separator: "", terminator: "\n")
            }
        }
    }
}

如你所見,檢索完成後我們呼叫了委託,這樣 ViewController 類中的表檢視就會根據新的聯絡人資料進行更新了,接下來我們推出這個檢視控制器。上面這些程式碼對你來說在很多方面都十分有用,因為你所需要做的,就是隻改變一下位於上述程式碼塊中的過濾器規則條件即可。

聯絡人選擇器檢視控制器(Contact Picker View Controller)

目前,我們所完成的所有聯絡人管理操作都完全是基於程式碼的,然而我們的故事還沒有結束。Contacts 框架直接提供了檢視控制器 (UI),可以以視覺化的方式來訪問聯絡人,並立即與它們進行互動。所提供的檢視控制器和“通訊錄”應用中的控制器十分相像,因此你可以藉此得到用於選擇一個或多個聯絡人的選擇器控制器,一個用於檢視聯絡人詳情的檢視控制器,以及一個可以編輯資訊的表單。在選擇聯絡人的時候,重寫預設的控制器行為也是允許的,此外還有委託方法可以讓你處理結果。

在這一部分,我們將設定這個選擇器檢視控制器,並在應用的選擇器檢視控制器中選擇和匯入聯絡人。我們無需準備太多其他的東西,不過定製程度將取決於每個應用的需求。Contacts 框架允許設定三個可選的斷言,從而讓你自定義所顯示的聯絡人資訊:

  1. predicateForEnablingContact:這可能是你最常用的斷言了。通過它,你可以指定在選擇器控制器中可用的聯絡人。比如說,你可以通過它來完成聯絡人的過濾,因此只有那些擁有可用生日的聯絡人才能夠在選擇器中顯示出來。
  2. predicateForSelectionOfContact:通過它,你可以決定選擇器檢視控制器在被選擇的時候,應該在何種情況下返回所選的聯絡人;以及何時應該為顯示詳情檢視控制器而添額外的選擇。
  3. predicateForSelectionOfProperty:通過它,你可以指定某個屬性的預設行為是否可以被執行(比如說當點選電話號碼時會執行電話呼叫操作),或者所按下的屬性是否應該被返回。

這裡我們所打算使用的只是第一個斷言,開啟選擇器檢視控制器,只允許顯示有生日資訊的聯絡人資訊。另外兩個斷言的使用也不難,但是我們這裡暫時用不著它們;如果需要參考的話,我建議您分別檢視斷言的文件

再次回到我們的應用中,開啟 AddContactViewController.swift 檔案。首先,到檔案的頂端,匯入 ContactsUI 框架。

import ContactsUI

接著,實現 CNContactPickerDelegate 協議,因此我們可以處理返回的聯絡人:

class AddContactViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, CNContactPickerDelegate

從現在開始,我們的工作都將在 showContacts: 這個 IBAction 方法中進行。這個方法會啟用位於 AddContactViewController 底端的按鈕。讓我們來看看具體的實現:

@IBAction func showContacts(sender: AnyObject) {
    let contactPickerViewController = CNContactPickerViewController()
 
    contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "birthday != nil")
 
    contactPickerViewController.delegate = self
 
    presentViewController(contactPickerViewController, animated: true, completion: nil)
}

是不是非常簡單!在這個示例應用中我們不需要在單擊聯絡人時顯示詳情頁面。不過如果需要的話,很容易使用這些屬性來展示詳情。你所需要做的就是將一個包含所需關鍵詞的陣列賦值給一個名為 displayedPropertyKeys 屬性。比如說,如果我們打算在應用中展示詳情資訊的話,我們就會在顯示選擇器檢視控制器之前增加一行程式碼:

contactPickerViewController.displayedPropertyKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]

幾分鐘前,我們實現了 CNContactPickerDelegate 協議,現在是時候來實現一個必須實現(required)的委託方法了。在方法中,我們會獲取所選擇聯絡人,然後通過我們自己的代理方法將其發回給 ViewController 類當中。

func contactPicker(picker: CNContactPickerViewController, didSelectContact contact: CNContact) {
    delegate.didFetchContacts([contact])
    navigationController?.popViewControllerAnimated(true)
}

假設你顯示了聯絡人的詳情資訊,然後你想處理返回的屬性,你需要使用 contactPicker:didSelectContactProperty: 委託方法。我們在這裡不對其進行實現,因為我們不需要它。你可以在這裡找到所有委託方法的集合。

應用現在可以繼續測試了。這時候按下 “Open contacts to select” 按鈕來顯示選擇器檢視控制器。你會注意到沒有可用生日的聯絡人是不會顯示出來的。選擇其中一個聯絡人,然後你就會看到它出現在了 ViewController 的表檢視當中。

37776-d1dfe226ad2e39d4.png

聯絡人檢視控制器

到目前為止,我們已經實現了三種不同的方法以允許我們檢索聯絡人並將其新增到應用中來。然而,只在表檢視中顯示聯絡人資訊並不是一個很好的主意。我們想要更豐富的展現形式,那就是在一個新的檢視控制器中顯示所選聯絡人的詳情資訊。實際上,我們不需要建立一個自定義的控制器,我們會使用由 Contacts 框架所提供的聯絡人檢視控制器。通過它我們不僅能檢視聯絡人資料,還能夠對其進行編輯。當然,通過 CNContactViewController 類我們可以輕易獲得它。

讓我們回到 ViewController.swift 檔案中來,然後處理一下使用者單擊聯絡人時所發生的情況。然而,在我們顯示 CNContactViewController 例項之前,我們需要確保所選聯絡人的詳情資訊中所有關鍵詞都可用。即便我們在展示每個單元格的時候檢查了所有可用的關鍵詞,即便我們在需要的時候重新檢索了聯絡人,但是當使用者單擊此行單元格的速度比重新檢索操作的速度更快的時候,一切就都不好說了。因此,我們必須要處理點東西。

之前,我們使用 CNContact 類中的 isKeyAvailable: 方法來檢查某個檢索到的聯絡人中關鍵詞的可用性。除了這個方法外,CNContact 還提供了另一種名為 areKeysAvailable: 的方法,我們可以用其來確保聯絡人檢視控制器所需要的所有關鍵詞都已存在。這個方法只接收一個引數,也就是一個包含關鍵詞或者關鍵詞描述符的陣列 (和我們用來檢索聯絡人時多次使用的關鍵詞陣列類似)。就 CNContactViewController 而言,雖然我們必須要設定 CNContactViewController.descriptorForRequiredKeys()的特定值作為引數陣列的唯一元素。假設關鍵詞都可用的話,我們將會顯示聯絡人檢視控制器。如果不可用的話,我們就用之前的方法,使用 descriptorForRequiredKeys() 來重新檢索聯絡人,從而指定所需要檢索的關鍵詞。

此外,我們在整個示例應用中用來檢索聯絡人資料的 keys 陣列就會再次變得簡單易用。不僅可以如我剛剛所述的那樣檢查可用性,還可以指定在聯絡人檢視控制器中應該顯示何種屬性。你可以在下面的實現中看到它是如何使用的。注意,要記住如果你省略了這個屬性,那麼所有既有的聯絡人屬性 (並不只是我們想顯示的) 都將在聯絡人檢視控制器中顯示出來。

上面說了這麼多,我們現在還是來看看這些程式碼吧:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let selectedContact = contacts[indexPath.row]
 
    let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
 
    if selectedContact.areKeysAvailable([CNContactViewController.descriptorForRequiredKeys()]) {
        let contactViewController = CNContactViewController(forContact: selectedContact)
        contactViewController.contactStore = AppDelegate.getAppDelegate().contactStore
        contactViewController.displayedPropertyKeys = keys
        navigationController?.pushViewController(contactViewController, animated: true)
    }
    else {
        AppDelegate.getAppDelegate().requestForAccess({ (accessGranted) -> Void in
            if accessGranted {
                do {
                    let contactRefetched = try AppDelegate.getAppDelegate().contactStore.unifiedContactWithIdentifier(selectedContact.identifier, keysToFetch: [CNContactViewController.descriptorForRequiredKeys()])
 
                    dispatch_async(dispatch_get_main_queue(), { () -> Void in
                        let contactViewController = CNContactViewController(forContact: contactRefetched)
                        contactViewController.contactStore = AppDelegate.getAppDelegate().contactStore
                        contactViewController.displayedPropertyKeys = keys
                        self.navigationController?.pushViewController(contactViewController, animated: true)
                    })
                }
                catch {
                    print("Unable to refetch the selected contact.", separator: "", terminator: "\n")
                }
            }
        })
    }
}

在上面的程式碼片段中,你可以看到我們通過使用聯絡人檢視控制器例項的 displayedPropertyKeys 屬性,指定了我們想要展示的屬性。另一個值得提及的細節就是,我們通過 contactStore 屬性給聯絡人檢視控制器提供了我們的聯絡人儲存例項。如果應用中沒有 CNContactStore 例項的話這個設定就不是必要的,因為 CNContactsViewController 會自行建立一個新的儲存器。剩餘的部分我們之前已經討論過了。作為最後一步,不要忘記在檔案頭部匯入下面這個框架:

import ContactsUI

新建並儲存一個新聯絡人

到目前為止,我們已經見識了許多關於 Contacts 框架中的新東西了。然而,仍然有一個我們沒有討論的部分,那就是如何通過程式碼建立一個新的聯絡人並將其儲存到資料庫中。因此,正如你所理解的,本教程的最後一個部分我們將要談論這個話題。我不會詳細說明如何更新一個既有記錄,因為這個操作和我們在這裡將要看到的十分相似,因此我將這個操作完全留給你,你可以自己找一下這兩個操作之間的差異。

除了代表單個聯絡人及其所有屬性的 CNContact 類之外,Contacts 框架還提供了一個名為 CNMutableContact 的類。如它的名字所言,這個類和 CNContact 十分相似,它允許我們為聯絡人的屬性賦予新值,因此就可以通過它來建立一個新的聯絡人或者更新一個既有的聯絡人。實際的儲存 (以及更新) 操作將在我們所周知的聯絡人儲存類 (CNContactStore) 中處理,但是這是建立新聯絡人的最後一步。你可以在下面看到額外的具體資訊。

通常情況下,使用 CNMutableContact 類來設定某個聯絡人的屬性值包含了一系列與獲取它們時完全相反的操作。進一步來說,對於簡單的屬性而言,直接分配一個單獨的值即可 (比如說名),特殊的屬性需要特殊對待。例如:

  • 當設定某個聯絡人的出生日期的時候,必須建立一個 NSDateComponents 物件並將其賦給對應的屬性
  • 當設定聯絡人頭像的時候,必須要賦給一個 NSData 物件
  • 當設定 email 地址的時候,必須給每個單獨的 email 地址建立一個 CNLabeledValue 物件,然後所有的地址物件都應該放到一個陣列中賦值給 emailAddresses 屬性。

上面的僅僅只是一些例子。當然還有很多聯絡人屬性需要謹慎對待,不過無論如何,接下來你會看到這些操作並不是很困難。

回到我們的示例應用中來,這時候我們要切換到 CreateContactViewController.swift 檔案。在這個檔案中,你會找到一個空的名為 createContact() 的自定義函式,這是我們所有工作將要進行的地方。簡單而言,我們將建立一個 CNMutableContact 類的例項,然後設定我們感興趣的所有屬性值,最後我們將這個新紀錄儲存到資料庫中。讓我們來看一看實現:

func createContact() {
    let newContact = CNMutableContact()
 
    newContact.givenName = txtFirstname.text!
    newContact.familyName = txtLastname.text!
 
    let homeEmail = CNLabeledValue(label: CNLabelHome, value: txtHomeEmail.text!)
    newContact.emailAddresses = [homeEmail]
 
    let birthdayComponents = NSCalendar.currentCalendar().components([NSCalendarUnit.Year, NSCalendarUnit.Month, NSCalendarUnit.Day], fromDate: datePicker.date)
    newContact.birthday = birthdayComponents
 
    do {
        let saveRequest = CNSaveRequest()
        saveRequest.addContact(newContact, toContainerWithIdentifier: nil)
        try AppDelegate.getAppDelegate().contactStore.executeSaveRequest(saveRequest)
 
        navigationController?.popViewControllerAnimated(true)
    }
    catch {
        AppDelegate.getAppDelegate().showMessage("Unable to save the new contact.")
    }
}

我們從頭來看這些操作,第一步是初始化一個 CNMutableContact 物件,這個物件將在後面一直使用。很明顯設定姓、名屬性是一個非常簡單的操作。接下來的家庭 email 地址必須建立為一個 CNLabeledValue 物件,這也是上面程式碼所展示的。一旦新的 email 地址建立之後,就會作為 email 地址陣列的一部分新增到 emailAddresses 屬性當中。在我們的這個例子中,我們沒有其他的地址。最後,我們基於使用者所挑選的日期,為這個新聯絡人制定了出生日期。如上面的程式碼所示,使用 NSCalendar 類並從 NSDate 物件建立一個 NSDateComponents 物件是非常容易的。注意到日曆物件 (年、月、日) 是如何合併的,藉此它們產生了我們最終所想要的值。

這個程式碼片段中最有趣的部分就是儲存新聯絡人的方式了。你可以注意到,首先是建立一個 CNSaveRequest 物件,接著向其中新增新的聯絡人物件。到這裡並沒有任何實際的儲存操作被執行。這個操作而是發生在下一行程式碼中,也就是呼叫聯絡人儲存例項中的 executeSaveRequest: 方法的時候。

假設新聯絡人無法儲存的話,那麼就會給使用者彈出一個帶有訊息的警示框。

現在執行這個應用,使用 ViewController 左上角的按鈕來建立一個新的聯絡人。儲存你的記錄,然後前去使用我們在之前部分實現的檢索方法將其檢索出來。

37776-96b08e14fc175968.png

重要提示:我在寫這篇教程的時候注意到一個問題,在我的測試中,也就是建立一個新的記錄並將其儲存到聯絡人資料庫的時候,通過應用訪問聯絡人詳情資訊(通過點選聯絡人所在的行單元格)並不可用。而且會在控制檯出現以下資訊:

CNUI ERROR] error calling service – Couldn’t communicate with a helper application.

在網上我並不能找到任何可用的幫助,我只好就此罷休,將其作為 BUG 報告給了蘋果。要牢牢記住,在測試應用的時候,要避免建立一個新的聯絡人。

總結

在本教程的結尾,我希望我已經講清楚新的 Contacts 框架的易用性了。如果你過去曾經使用過 AddressBook API,那麼你會發現在使用 Contacts 聯絡人的時候一切都發生了巨大的變化。你可以盡情地把玩這個示例應用,對其進行修改,以及按照你的意願對其進行擴充套件。這個應用仍有提升的空間,但是千萬不要忘記了使用者隱私協議,並且你必須要尊重使用者關於是否准許應用訪問聯絡人的選擇。不要錯過了官方文件,你會在那裡發現更有意思的東西。我希望你能夠享受本篇教程,並能發現其中有用的知識。下次我們再見,希望你擁有美好、積極的一天!

作為參考,你可以在這裡下載完整的 Xcode 專案

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg

相關文章