初窺 iOS 9 的 Contacts 框架

發表於2015-11-11

contacts-framework.jpg

iOS 9為使用者和開發者展示了很多新的技術和在現有技術上的優化。正如我們看到的,在這個版本里有很多第一次展示的新的內容,也有很多已有的框架和類的變化和更新。除此之外,始終驚喜的是,有一些舊的APIs被放棄和不再建議使用,為新的全新開發的或用來做過渡的APIs讓位。在iOS9中的例子就是全新的Contacts framework, 它以更流行的模式來替代舊的 AddressBook 框架,更簡單和直接。

以前用過AddressBook API的每個開發者都可以肯定的說它在iOS SDK中肯定不屬於能簡單使用的那一部分。總體上,AddressBook很難理解和掌握,而且對新手而言更是如此。這一切歸咎於歷史原因,而新的Contacts 框架理解起來更簡單和易於使用。聯絡人資訊可以在很短時間內被獲取,建立或更新,跟聯絡人相關的開發時間能被很大的縮短,變更和修改可以很快被完成。

在以下的段落中我們會強調Contacts框架的最重要的部分。我不會展示太多細節,因為你可以在蘋果官方文件和 WWDC 2015 session 223 video裡找到相關內容。

因此,首先,我會從關鍵地方開始,那就是使用者隱私。使用者經常被一個應用詢問它是否有權利獲取使用者的聯絡人資訊。如果使用者同意,那麼應用就可以自由的同使用者的聯絡人資料庫互動。如果不同意,使用者禁止APP獲取聯絡人資訊,那麼這個決定必須被APP採納並且絕對不能同聯絡人資料庫互動。一會兒我們將會更仔細的談論它,然後我們將會看到所有可能的場景是怎樣以程式設計方式被處理的。不僅如此,要時刻記住使用者始終有權在裝置設定裡更改應用是否能獲取資訊的許可權,所以你應該在執行任何相關任務之前,經常檢查你的應用是否有獲取聯絡人資訊的權利。

聯絡人資料的最大來源一直都是裝置裡的資料庫。但是,當一個應用請求聯絡人資訊時,Contacts框架不僅僅是在那裡查詢。實際上,它也會搜尋其他來源,比如你的iCloud賬戶(當然如果你已經連線了的話),然後給應用返回從各個源頭獲取到的資訊的整合。這非常有用,因為你沒有必要除開搜尋裝置資料庫以外去建立其他搜尋聯絡人資訊的方法。你在同一時間就得到了所有,然後以自己的方式使用它們。

Contacts框架有服務於各個特定的目標的類。所有的都很重要,但是有一個是使用的最多的,叫做CNContactStore。這個類以程式設計方式展示了聯絡人資料庫,並且提供了許多實現不同任務的方法,例如獲取,儲存或者更新記錄,許可權檢查和許可權請求,很多很多。一個單獨的聯絡人記錄被CNContact類展示,但是記住這個類的特性是不可變的。如果你想建立一個新的聯絡人記錄或者更新一個已存在的聯絡人記錄,你必須使用CNMutableContact類。注意同Contacts框架打交道的時候,特別是獲取聯絡人資訊時,你應該始終在後臺執行緒中執行這些任務。如果一個聯絡人資訊獲取任務花去了太多時間而且在主執行緒執行,那麼你的應用有可能無響應,然後最終導致非常糟糕的互動體驗。

當匯入聯絡人資訊到APP時,所有的聯絡人的屬性都需要的情況是很少見的。從所有的Contacts 框架要搜尋的資料來源中獲取所有聯絡人資料的操作,能被證明是一個非常耗資源的程式,所以你應該避免這樣做除非你確定你是真的將會用到所有的資料片段,哪怕是最後一個。萬幸的是Contacts框架提供了可以獲取部分結果的方法,意味著只是聯絡人的一部分屬性值而不是所有。例如,你可以只是請求名或姓,家庭地址,家庭電話等,通過排除你不需要的那些資料來節約很多資源。

除了Contacts框架提供的所有以程式設計方式獲取聯絡人資訊的方式之外,它也提供了一些可以與應用協作的預設的UI,以這種方式直接的以及視覺化的訪問聯絡人資訊。提供的UI跟Contacts應用基本一樣,這意味著有一個contact picker view controller 連同詳細資訊卡一起,可以被用來獲取聯絡人和屬性(它可以被定製化到一個級別),和一個contacts view controller 可用來展示聯絡人細節資訊以及實現某些動作(例如,打一個電話)。

上面提到的內容的細節將會在這個指南的後面看到。再一次的,訪問官方文件來獲取更多我已經展示的或我將要展示的內容的資訊。我們現在來看demo應用將會是什麼樣的,然後我們來學習Contacts框架的類。你會發現同這個新的框架互動非常簡單和有趣。

Demo APP 快速瀏覽

通過這個教程的demo應用,我會向你展示儘量多的關於這個新框架的內容。事實上,在接下來的部分我會向你展示怎樣去:

  1. 檢查這個應用是否被授權獲取聯絡人資訊以及怎樣發起授權請求。
  2. 使用3個不同的方法獲取聯絡人資訊。其中一個包括使用picker view controller
  3. 訪問獲取到的聯絡人的屬性以及合理的格式化它們以用來展示。
  4. 使用預設Contacts ?UI 的來選取,檢視, 甚至編輯聯絡人資訊。
  5. 建立一個新的聯絡人記錄。

我把這個demo應用命名為Birthdays,因為它的目的是對所有引入到該應用中的聯絡人的生日做展示。聯絡人的全名,照片(如果有)和家庭郵箱地址也會被展示。理想狀態下,這個應用應該可以是個生日提示器,當然,我們不會處理通知,簡訊傳送以及其它相關動作。

這個應用是基於導航模式的,而且它由下面的部分組成:

當這個應用開始時, ViewController是預設被展示的。它展示了先前我提到的所有引入的聯絡人的資訊,並且提供了獲取更多聯絡人資訊(右上角按鈕)操作,建立一個新聯絡人(左上角按鈕),以及通過點選一行來檢視聯絡人詳細資訊的方法。

t43_1_display_records.png

聯絡人詳細資訊將會展示在內嵌的聯絡人view controller 裡。正如你將會看到的那樣,你可以展示所有的屬性或者只選擇你感興趣展示的那部分。

獲取聯絡人資訊在後面將會是一個很有趣的部分。我會通過3種不同的處理方式向你展示3種方法。

  1. 第一種,我們將會輸入一個聯絡人名字(或者名字的一部分),然後通過點選鍵盤上的return按鈕,這個應用就會獲取匹配輸入名字的聯絡人資訊。
  2. 正如你將會在下面看到的截圖那樣,在螢幕中間有一個picker view。我們將會利用它來找到所有跟在picker裡選中的月份所匹配的聯絡人的生日月份,而且獲取操作會在點選Done按鈕的時候被觸發。
  3. 我們會利用框架提供的預設picker view controller來直接檢視和選擇聯絡人資訊。 注意在這個controller裡展示的聯絡人資訊是可以被定製化的,picker view controller裡的行為也一樣。稍後你將會看到怎樣做。

t43_2_fetch_contacts.png

這就是picker view controller,只展現了具有有效生日日期的聯絡人集合:

t43_3_picker_view_controller.png

應用的最後部分是關於建立一個新的聯絡人。這是個很簡單的任務,歸功於這個demo 應用我們將會利用下面的view controller 來鍵入將要被建立的聯絡人的姓名,家庭郵箱地址和生日(我們這裡不處理圖片,因為此刻它並不是最重要的)。

t43_2_fetch_contacts.png

這個demo應用的示範資料(示範聯絡人)將會是模擬器資料庫儲存的預設聯絡人。這些聯絡人資訊對於實現我們的目的足夠用了還非常好。當然,你可以使用你裝置裡面的聯絡人資訊,或者增加新的聯絡人到模擬器中。預設情況下模擬器聯絡人資訊不包含圖片,但你可以輕鬆地在圖片庫裡找到圖片並且新增。

像往常一樣,在這裡下載我們將會在後面用到的作為入門的初始工程。 下載完成後,開啟它然後瀏覽一下我們已經在裡面新增了的檔案。準備好後,就可以開始下面的部分了。

Contact Store類

同聯絡人打交道的時候有一個你一直都會用到的最基本的類就是CNContactStore 類。這個類實際上展現了存在於裝置上的聯絡人資料庫,而且負責管理所有在應用和這個實際資料庫之間的所有互動。再進一步說,它管理了所有關於拉取(fetching),儲存(saving)和更新(updating)聯絡人和群組記錄的工作。簡而言之,它就是大多數同聯絡人資訊互動的初始點,在馬上就要寫到的程式碼中你們將會看到這點。

除此之外,正如我在介紹中提到的,使用者隱私問題是iOS的重要組成部分,所以在處理這方面時要特別注意。普遍都瞭解的是使用者可以在第三方應用中選擇允許或者拒絕它們使用聯絡人資訊,所以非常重要的是保證無論何時去實現聯絡人相關的任務的時候,你的APP都被授權了可以這麼做。使用CNContactStore類,你可以檢視你的應用的當前授權狀態( current authorization status )。始終牢記使用者可以通過Setting 來隨時禁止你的應用獲取聯絡人資料資訊,儘管可能一開始你的應用是被允許訪問的,所以確保你的任務是否可以操作是非常重要的,當然也要在每個不同的情形下引導你的應用朝著對的方向發展。沒有被處理到的情形會最終導致糟糕的使用者體驗,而這恰是你必須要避免的。在這個嚮導中我們會嚴格考慮demo應用是否被授權的情況,甚至從這個部分就開始了。我們馬上要做的內容,就是隻要你想用就可以在你的工程裡自由使用。

你將會馬上看到的是在下面的場景中(區別於其他的場景) contacts store 類都是必須要用的:

  • 當獲取聯絡人資訊時
  • 當建立,儲存和更新一個聯絡人時
  • 當使用 Contact Picker view controller來選擇聯絡人時

牢記這條後,我們初始化一個 CNContactStore物件,而且我們會在整個類裡都使用到它。另一方面,我們也可以在任何要使用它的時候建立新的物件,但是因為這個類在程式碼裡代表了聯絡人資料庫,有什麼理由需要多個它的例項呢?所以,我們開始吧。首先開啟 AppDelegate.swift 檔案,初始化和宣告一個 CNContactStore屬性。在檔案的頂部,新增下面的程式碼:

必須在類宣告的頂部新增下面的框架:

非常好!現在,在我們處理應用的授權狀態以及針對這個狀態可以做的操作之前,我們先寫下兩個簡單且方便的方法。注意它們並不是為了繼續這個專案而需要的,不要它們也可以做我們的工作。但是,實現一些針對完成某個目的的小方法被證明了是非常便利的。

因此,第一個小方法就是從任何其它類中可以簡單訪問應用的delegate類(AppDelegate)的方法。通常,下面的程式碼可以實現訪問應用的delegate:

但是,我個人發現每次當我需要獲取app delegate時都要寫下所有的以上程式碼,這感覺像是被中途打擾了。如果我們編寫下面的這個類方法又會怎樣?

通過它,我們可以通過一種更簡便的方式來訪問app delegate的任何屬性或者方法。例如,我們可以像下面展示那樣在工程中的任何類中獲取contacts store屬性:

第二個我們將會在檔案中新增的便捷方法就是一個展示提示資訊的controller,提示資訊變數是通過引數傳遞的。實現起來並不複雜,但是有個特殊的地方要注意;一個提示controller必須被一個view controller展示,而應用的app delegate 並不是一個view controller。

為了解決這個問題,我們有必要找到當前app window上的最上層的view controller, 然後在這個view controller上展示這個提示controller。下面是實現方法:

現在我們做很重要的事了,那就是處理應用的授權狀態。這個狀態是被CNAuthorizationStatus列舉值展現的並且是屬於CNContactStore 類。它包含以下的4種值:

  1. NotDetermined: 這個狀態表示使用者現在為止還沒有允許或者拒絕對聯絡人資料庫的訪問。應用在裝置上第一次安裝時就會是這個狀態。
  2. Restricted: 這個狀態表示應用不僅不能訪問聯絡人資料,而且使用者也沒有許可權在Settings裡修改這個許可權。這個狀態可能是其他活躍的限制條件的結果(例如. Parental control)
  3. Denied: 當應用是這個狀態時,表示使用者已經選擇了不允許訪問聯絡人資料資訊。而這個只能被使用者本人改變。
  4. Authorized: 這是每個應用的理想狀態。當應用是這個狀態時,它可以自由訪問聯絡人資料庫並且實現需要聯絡人資料的任務。

有一件事必須弄清楚:安裝應用後,使用者第一次(且僅是第一次)想試圖與聯絡人資料互動(例如,獲取聯絡人資訊)時,iOS將會展示一個預定義好的提示controller來要求使用者對應用授權:

t43_5_ask_authorization_alert.png

如果使用者允許訪問,一切都好。但是,如果使用者拒絕訪問,那麼基於聯絡人資訊的所有功能都不可能被執行。在我們的demo應用裡,且在這個特定情景下,我們將展現一個自定義的提示資訊(使用我們上面已實現好的方法)來告訴使用者他必須在Settings裡設定允許訪問聯絡人資訊的許可權。我們將會在即將實現的新方法裡處理這種情況。當然,我們也會在那個方法裡考慮所有可能出現的授權狀態。首先來看看這個方法是什麼樣的,稍後將會對它做更多講解:

檢視上面的方法,你會看到它包含了一個completion handler,當應用被授予允許訪問時它的返回值是true,相反就是false。一些狀態很簡單,例如Authorized或者Restricted,當是這些狀態時completion handler 的值該是什麼都很清楚。但是,有趣的地方是Denied和NotDetermined的狀態值是在同一個情況裡被處理的,而且對它們兩個requestAccessForEntityType:completionHandler:方法都被呼叫了,讓應用請求訪問許可權。對於只是Denied情況,我先前說的自定義訊息將會被展示。

注意 requestAccessForEntityType:completionHandler:和authorizationStatusForEntityType:methods 方法都需要一個 CNEntityType 引數。它是一個列舉,只包含一個叫 Contacts的值。這個列舉實際上指明瞭我們要求訪問的實體。

以上方法從現在起將會被數次使用,下一部分馬上就要使用。每次當我們要針對聯絡人資訊做相關操作時都要使用它,這樣我們就要確保瞭解我們的操作是否可以繼續進行,當然也要處理每個可能遇到的情況以避免出現糟糕的使用者體驗。現在看來一切都好,因為我們已經準備好了一些可重複使用的程式碼,隨著我們不斷深入,這些程式碼將會在接下來被證明是非常便利的。

使用Predicates來獲取聯絡人資訊

正如我在本教程的簡介部分就提到了的,我們將會用3種不同的方法來獲取聯絡人資訊。其中一種就是通過在一個文字輸入框裡輸入我們想獲取的聯絡人(聯絡人們)的一部分或者全部的的姓名(不管是名還是姓),然後向聯絡人框架(Contacts framework)獲取結果。我們從這裡開始,而實現它的關鍵方法就是unifiedContactsMatchingPredicate:keysToFetch:error:方法。

這個方法屬於CNContactStore 類的一部分,需要2個重要的引數:

  1. Predicate: 一個NSPredicate物件作為返回結果的過濾器。非常重要且必須強調的是隻有從CNContact類中得到的predicates物件才會被接受,普通的你自己建立的predicates物件 (看這裡)就不行。在所有CNContact類中支援獲取predicate的函式中,有一個叫做predicateForContactsMatchingName:,我們將會使用它。
  2. keysToFetch: 通過設定這個變數,你指定你想獲取的聯絡人的部分資訊。它是一個陣列包含了描述被搜尋聯絡人 (CNContact 物件)屬性值的字串。框架提供預定義好的常量字串來作為鍵值(keys)。

注意:這個方法可以返回一個exception, 因此它必須在一個使用try關鍵字的do-catch 宣告裡被呼叫。錯誤情況是被宣告裡的catch 處理的。

unifiedContactsMatchingPredicate:keysToFetch:error:方法的返回結果是一個符合給出的predicate變數的所有CNContact物件的陣列,或者出現了什麼錯誤時返回nil。

記住這些之後,是時候繼續實現的步驟了。這次開啟AddContactViewController.swift檔案,直接找到檔案的頂部。在這裡引入Contacts framework, 如果沒有它什麼都不能做。

現在我們找到textFieldShouldReturn: delegate 方法。最開始我們使用在application delegate裡建立的最後一個方法,這樣我們就可以檢視應用是否有獲取聯絡人許可權以便繼續後面的工作:

如果是被授權了的,我們就可以準備匹配聯絡人資訊的predicate 和keys。同它們一起,我們還要宣告一些其他的變數:一個儲存結果(如果有的話)的陣列變數,一個字串變數用來儲存當沒有匹配聯絡人資訊結果時或者獲取操作失敗時要展示的自定義訊息。

在這裡請注意我們是怎樣指明predicate 和keys 陣列的,然後我們繼續。在下一步,我們將會試圖獲取聯絡人資料,如果操作成功的話,那麼我們一開始建立的contacts陣列將會被填滿返回的結果。如果沒有找到聯絡人資訊或獲取操作失敗了,我們接下來會展示一個自定義的訊息;有了這些之後這個方法裡的實現程式碼基本上就完了。

正如你見到的,我們在else 方法裡什麼都沒實現,但馬上在後面我們就會重新訪問它並且新增上缺失的程式碼。這這部分最重要的是要了解我們是怎樣獲取匹配給出名字的聯絡人資訊,以及在沒得到預想情況下時我們是怎樣處理的。

展示獲取到的聯絡人

最好的情況就是我們的獲取操作可以返回匹配的聯絡人資訊,然後有必要在ViewController的tableview裡展示它們。但是,第一步就是要告訴ViewController聯絡人資訊已經是被獲取到了,這一切操作都發生在AddContactViewController裡。最好且最簡單的實現過程就是使用熟知的Delegate 模式。因此,我們按照這個思路繼續實現以填補應用中的銜接點。

在AddContactViewController.swift 檔案中,在類上方建立下面這隻有一個方法的協議:

通過使用上面的這個代理方法,我們不僅可以讓ViewController 類知道聯絡人資訊已經被獲取到了,而且還可以通過它傳遞新獲取到的聯絡人資訊。

然後,在 AddContactViewController裡新增下面的代理宣告:

回想一下,我們在上面的 textFieldShouldReturn: 方法最後的else部分留了空白,現在是時候新增上缺失的程式碼了。事實上,只缺少了兩行程式碼:一個是我們呼叫上面剛宣告的delegate方法,第二個就是在navigation controller 中彈出view controller。

正如你看到的,當處理涉及UI操作時我們都會使用主執行緒。這是一個你不應該忘記的非常重要的細節,否則UI就不能在合適時間被更新,你就會遭遇APP應用的一些非預期反應。

現在是時候轉到 ViewController.swift 檔案中去處理獲取到的聯絡人資訊了。首先,我們在這個類裡也必須先引入Contacts framework。

接下來,我們需要接受新建的自定義協議,因此需要在類名後面新增上協議的名字:

現在,有必要宣告一個 CNContact 物件的陣列。這個陣列會儲存所有從獲取請求裡得到的聯絡人,它將會是我們tableview的datasource。因此,在ViewController的頂部新增下面的程式碼:

還有我們必須更新tableview將要展示的行的數量,如下:

在我們實現先前宣告的delegate 方法之前,必須要宣告ViewController 類就是AddContactViewControllerDelegate 協議的delegate。這個將會在 prepareForSegue:方法裡實現:

最後,我們必須實現我們自定義的delegate 方法。在這裡面我們將會一個一個的得到所有返回的聯絡人資訊然後把它們都新增到contacts陣列裡。最後,我們會重新載入tableview讓它展示最新的聯絡人資訊。

現在我們來展示聯絡人資訊。在每個cell裡將會展示聯絡人的姓和名,如果有出生日期就展示,沒有就展示一個簡短的資訊,如果有影像和家庭郵箱那麼也展示。下面你將看到的實現程式碼在後面會被更改一點點,不過已經足夠讓你瞭解一個聯絡人的屬性資訊是怎樣被獲取到的。所以,讓我們看一看吧:

讓我們來過一過上面的實現步驟。首先,我們設定聯絡人全名是通過把姓和名連線起來的方法。稍後我會給你展示另一個方法來獲取全名,現在先使用這個方法。接下來,我們設定生日日期資訊。如果有生日資訊,我們就用最簡單的方法展示它。注意這只是一個臨時的方案,稍後我們會用一個更合適的方法來構建生日日期。同樣,非常重要的一點是瞭解生日資訊並非是一個NSDate物件。取代它的是一個 NSDateComponents 物件,當然這個物件可以轉化為一個 NSDate物件然後轉化為一個String物件。

下面我們要設定的就是影像資料。如果它不存在,你將會在它的位置上看到一個我在自定義cell的xib 檔案中新增的 imgContactImage 的背景顏色,

最終,剩下了家庭郵件地址我們要設定了。你看到了我們使用了一個loop迴圈來遍歷所有的郵件地址直到找到我們要的那個。這樣做是因為聯絡人資訊的emailAddresses屬性包含了所有存在的以郵箱地址為標籤值( labeled values )的(CNLabeledValue)物件。

如果你現在執行應用,根據你輸入的名字而選出來的聯絡人資訊,上面的實現程式碼可能有用,但是也有可能沒用。在第二個案例中應用就會崩潰,不過你不必擔心。我們稍後會改進。我故意沒有給你展現上面的函式的最終實現程式碼,就是因為按現在這種方式能夠更清楚的向你展示所有東西都是怎樣工作的。

重新獲取聯絡人

應用可能崩潰的原因就是並非所有的你需要獲取的聯絡人屬性資訊都存在。因為這樣,CNContact 類包含了一個叫 isKeyAvailable: 的方法,它必須在你訪問任何聯絡人屬性資訊之前被呼叫。例如,在我們試圖展示生日日期,圖片和郵箱地址之前應該新增下面的檢查方法:

如果一個key值沒有被找到,那麼必須進行的操作就是重新獲取這個聯絡人的資訊然後再次展示。這就是我們現在要做的,更詳細的就是我們將要在ViewController裡建立一個新的方法。但是,在我們這樣做之前,我們先通過新增 isKeyAvailable:方法來修補一下展示聯絡人詳細資訊的實現程式碼。實際上,不是對於上面的屬性新增三個不同的條件判斷的方法,而是我們只建立一個判斷不存在屬性的方法,為了以防萬一有什麼是缺少的,我們要呼叫即將要實現的一個方法來重新獲取聯絡人資訊。我故意沒有提到聯絡人姓名這個關鍵詞,因為在後面的部分將會看到更多關於它的資訊。

上面呼叫的方法就是我們馬上要實現的方法。除此之外,我想我們增加的判斷條件已經很直白了你應該能理解它的邏輯。注意通過這個更改,應用再也不會崩潰了,即使是在結果中包含了不可用的鍵值資訊時也一樣。

現在我們來看新的方法:

首先我們檢查這個應用是否被授權了可以訪問聯絡人資料庫。然後我們指定想要獲取的聯絡人的部分鍵值資訊,然後重新針對給出的聯絡人資訊進行獲取。注意這次我們使用了一個新的方法來做這件事,就是unifiedContactWithIdentifier:keysToFetch:方法。它的目的就是獲取滿足識別符號引數值的一個特定的聯絡人資料。一旦這個結果返回後,我們就用這個新聯絡人資訊去替代在陣列裡的舊資訊。最後,我們重新載入tableview的特定的行。

如果你想,可以再試試執行這個APP。萬一有某些聯絡人資訊沒獲取到時,你最好始終做一下重新獲取聯絡人資訊的操作,這樣就能確保你的應用不會給使用者展示任何“驚喜”。

格式化輸出結果

正如目前你看到的,在cell上展示每個聯絡人生日資訊之前我們沒有做任何合適的格式轉換。我們只是連結和展示了生日日期屬性,現在在已經學習了先前的重要知識後,是時候來處理這個問題了。

我們會通過在ViewController 中建立一個新的自定義方法來修補生日日期。在這方法裡,我們將會使用一個NSDateFormatter物件來轉換日期資訊為一個本地化字串,但是首先,我們必須把日期元件(也就是日期部分)轉換成一個NSDate物件。我們來看看這個新方法:

上面方法的引數是一個用 NSDateComponents物件表示的日期(在我們的情況下就是出生日期物件)。返回值當然就是一個字串。為了把dateComponents物件轉換為NSDate物件,不會用到超過一行程式碼。我們使用NSCalendar類來執行這個轉換,日期物件已經準備好了要被即將初始化的日期格式器(date formatter)處理。給日期格式器設定當前地理資訊是一個必須的操作因為這樣就可以獲得一個本地化描述的日期資訊。最終,我們給日期物件設定一個偏好的格式(不是太長也不是太短),然後我們執行最終的轉換操作。轉換後的值最終返回給了呼叫者。

現在我們修復日期資訊的展示方法,僅僅簡單通過呼叫上面的函式就可以了:

非常好。現在生日日期將會以一個更美觀時尚的方式展現出來了。

現在我們來看看關於展示姓和名的方面。CNContact 類提供了內嵌格式轉換器,它可以幫助我們輕鬆地格式化兩種資料:聯絡人的全名資訊 (CNContactFormatter) 和地址(CNPostalAddressFormatter)。現在我們就將使用第一個,這樣聯絡人全名就自動被聯絡人框架(Contacts framework)格式化了。

我們來最後一次更改聯絡人的展示方法如下:

如你看到的,這一行 cell.lblFullname.text = “(currentContact.givenName) (currentContact.familyName)”程式碼已經被替換為了如下:

很顯然的,我們不再需要通過手動連結姓和名來建立聯絡人的全名了。CNContactFormatter幫我們做了並且它輸出了一個本地化的字串(通過以適當的順序設定名字部分,依靠裝置的本地化設定)。

但是,上面的操作會引起一些複雜操作,因為聯絡人格式器(contact formatter )需要訪問與一個聯絡人名字相關的所有關鍵字(keys),即使是那些我們並沒有在獲取陣列裡指定的那些關鍵字。但是,我們不需要一個個的寫下它們所有。所有相關關鍵字都被一個key descriptor指定了,而這個decriptor 替代了在關鍵字陣列裡指定的所有單個關鍵字。

為了使這個更具體,找到AddContactViewController檔案,在 textFieldShouldReturn:方法裡,把下面的程式碼:

替換成下面使用了key descriptor的程式碼

這個descriptor被組織的方式很具體,正如上面展示那樣。除了這個,其它關鍵字資訊保持不變。

上面的改變必須在refetchContact: 方法(在 ViewController裡)裡執行。你所要做的就是利用上面那行程式碼替換關鍵字陣列的定義,來做吧:

通過這些處理,我們已經在程式碼裡做了所有跟格式相關的改變。當然,你也可以仍然使用單個關鍵字來獲取單個名字資訊,這始終取決於你的要求。

使用自定義過濾器獲取聯絡人

在這篇教程裡我最先展示之一的就是怎樣使用predicates來獲取聯絡人資訊。我們使用Contacts framework 的一個predicate來獲取匹配某個給出的名字的聯絡人,但是假如你記得的話,這個方法有一個普遍的劣勢;我們必須只能使用框架的內嵌predicates而我們不能使用自己的predicates。現在的問題就是,我們可以怎樣通過使用自定義的過濾器來獲取聯絡人?

這個問題對於我們的demo應用來說可以更具體化,因此我們可以問自己,我們可以怎樣基於聯絡人的生日月份來獲取聯絡人資訊?在AddContactViewController中,有一個picker view展示了所有的月份,我們現在想要做的就是選取一個月份,點選Done按鈕,然後最終得到記錄中生日月份跟選擇的月份相同的聯絡人。

你可能猜到了,有一個解決方法就是“新增”自定義過濾器,但是這整個過程比起使用predicates更多一點人工操作。總體上,我們將要看到的處理方法是基於蘋果推薦的基於這種情況下的使用CNContactStore類的 enumerateContactsWithFetchRequest(_:usingBlock) 方法。這個方法獲取到了所有的聯絡人資訊,因此自定義標準可以通過比較屬性值或者應用任何其他的自定義邏輯在block 部分(或者閉包)裡設定,最終保留你真正需要的那部分聯絡人資訊。

在我們的情形下需要檢查兩件事:首先,我們必須保證每個聯絡人的生日日期都被設定了以避免不想要的崩潰。第二,我們只是把生日月份和在picker view裡選擇的月份比較,如果有匹配的name我們就保留這個聯絡人記錄到陣列裡。這樣做相當簡單,因為生日日期是由NSDateComponents物件展示的,因此我們可以直接訪問月份資訊。不止如此,剩下的更簡單:所有我們要看到的內容都已經在前面的部分展示了,而且我也已經講解了。我們在這裡只會在AddContactViewController檔案的performDoneItemTap自定義方法裡寫入新的程式碼,因為我們想要只在view controller 裡的Done 按鈕被按下時才去獲取基於月份選擇的聯絡人記錄。

請看:

正如你看到的,在完結時我們呼叫了delegate以便讓新的聯絡人資訊可以在ViewController裡的tableview中更新,然後我們彈出view controller。這上面的程式碼可以在很多情況下都對你有用,因為你要做的事情僅僅是改變上面block裡面的過濾標準的引數。

Contact Picker View Controller

所有我們的聯絡人管理和目前所做的工作都全部是程式設計式的,但是故事並沒有在這裡結束。Contacts framework提供了view controllers (UI) 來直接的、視覺化的訪問聯絡人資訊並且馬上同它互動。這些被提供的view ?controllers和Contacts 應用裡的很相似,一個picker controller被提供給你去選擇一個聯絡人記錄(或許多聯絡人記錄),一個view controller 來檢視聯絡人詳細資訊,和一個表單來編輯資訊。當選擇聯絡人時重寫預設的行為是被允許的,並且有delegate 方法讓你自己處理得到的結果。

在這部分我們將要向你展示picker view controller是怎樣被使用來選擇和匯入聯絡人記錄到你自己的應用中。沒多少準備工作要做,但是自定義化的層級取決於每個應用的自身需要。Contact frameowrk 允許設定三種可選的predicates,讓你可以限制聯絡人的展示和改變預設行為。

  1. predicateForEnablingContact: 這個可能是你要用的最多的一個predicate。有了它,你可以指定在picker controller裡面哪些聯絡人資訊可用。你可以用那個方法來過濾出需要的聯絡人,比如僅僅讓那些有著有效地生日日期的聯絡人才可以被選擇。
  2. predicateForSelectionOfContact: 有了它,你可以控制picker view controller在哪種條件下可以返回被選中的聯絡人,以及對於其它的選中模式,什麼時候它可以展示details view controller .
  3. predicateForSelectionOfProperty: 使用它,你可以指定一個屬性的預設響應是否應該被執行(比如當點選一個電話號碼時是否需要建立打電話行為),或者被點選的屬性是否應該被返回。

在這裡我們將要只使用第一個predicate,向picker view controller請求,這個picker view controller只允許那些有著生日日期的聯絡人可用。使用其它兩個並不困難,不過我們在這裡並不需要他們;為了給你一個參考我推薦你去看相關的文件.

又回到我們的應用中,開啟AddContactViewController.swift檔案。找到最頂端,引入ContactsUI框架:

接下來,採用CNContactPickerDelegate協議,這樣我們就可以處理返回的聯絡人記錄:

從現在起我們的工作要在showContacts: IBAction方法裡進行。這個方法會讓在AddContactViewController底部的按鈕進行操作。讓我們來看實現程式碼:

就是這樣簡單!在這個demo應用中當點選一個聯絡人時我們不能展示它的詳細卡資訊。但是如果你想在你的應用中這麼做的話,在詳細介面中很簡單的就可以控制要被展示的屬性值。你所要做的就是在一個名叫 displayedPropertyKeys的屬性中宣告一個你想要展示的屬性的關鍵字的陣列。例如,如果我們想要在應用中展示詳細資訊,那麼我們需要在展示picker view controller之前新增下面的這行程式碼:

先前我們採用了CNContactPickerDelegate協議,現在就要來實現一個必須的delegate方法了。在這個方法裡,我們將會得到被選中的聯絡人記錄,然後我們會通過我們自定義的delegate 方法來把這些記錄返回到ViewController裡。

如果你想要展示聯絡人詳細資訊而且想要處理一個返回的屬性值,那麼你需要使用contactPicker:didSelectContactProperty:這個delegate方法。我們不會在這裡實現它,因為我們不需要它。你可以在 這裡.找到所有delegate方法的集合。

可以再次驗證這個應用了。這次使用 “Open contacts to select” 按鈕來展示picker view controller。你會發現聯絡人中沒有有效的生日日期的是不會展示的。選擇一個聯絡人,你將會看到這個聯絡人將會在ViewController裡的tableview中被展示。

25.png

Contacts View Controller

目前為止我們實現了3種方法來允許獲取聯絡人資訊並且把它們新增到我們的應用中。但是,只是在tableview中展示它們肯定不好;我們要求的更多,要求在一個新的view controller 裡面展示一個被選中的聯絡人。實際上,我們不會建立一個自定義的view controller ,但是我們會使用Contacts framework 提供的contact view controller。 使用它我們不僅可以檢視聯絡人的資料,還可以編輯資料。當然,這個方法是由 CNContactViewController 實現的。

我們回到 ViewController.swift 檔案,然後處理當使用者點選了一個聯絡人時的情況。在我們展示一個 CNContactViewController物件之前,我們必須確保這個選中的聯絡人的細節資訊對應的所有關鍵字都是可用的。儘管我們在展示每個行時都要檢查關鍵字是否可用以及如果有必要我們甚至會重新獲取聯絡人,但是我們還是不能百分百保證排除使用者點選某一行時可能會快於重新獲取聯絡人這個動作。因此,這個操作必須要做。

先前,我們使用 CNContact類的 isKeyAvailable: 方法來檢查一個獲取到的聯絡人的某個關鍵字是否可用。除開這個方法,類還提供了另外一個方法叫做 areKeysAvailable:,我們可以利用它來保證contacts view controller 需要的所有的關鍵字都存在。這個方法包含了僅僅一個變數,一個關鍵字字串或者字串描述器(key descriptors 同我們多次用來獲取聯絡人的關鍵字字串類似)。在CNContactViewController中,我們必須給CNContactViewController.descriptorForRequiredKeys() 設定一個特定關鍵字數值組成的陣列引數,這個類方法將會自動檢查所有的關鍵字是否存在。如果這些關鍵字都存在,我們就展示contacts view controller. 如果不存在,我們會像先前做的那樣,給descriptorForRequiredKeys()方法指明應該要獲取的特定的關鍵字資訊然後使用它來重新獲取聯絡人。

還有,貫穿這個demo應用的用來獲取聯絡人資訊的關鍵字陣列再次被證明了使用起來非常便利。不是在我剛才解釋的檢查可用性裡,而是在制定哪些屬性應該被展示在contacts view controller 裡。你可以在下面的實現程式碼裡看到它是怎樣被使用的。一個額外的提示,記住如果你忽略了這個屬性,那麼所有聯絡人的屬性值(不僅僅是那些我們想要展示的)都會被contacts view controller 展示。

說了這麼多,來看看相應的程式碼吧:

在上面的程式碼段中可以看到我們簡單的使用了contacts view controller 例項物件的displayedPropertyKeys屬性就指定了想要被展示的屬性值。另一個細節值得注意的就是我們利用contactStore屬性來向contacts view controller 提供 contact ?store 例項。如果應用中沒有一個已存在的CNContactStore例項那麼這個操作也不是強制要做的,因為CNContactsViewController會自動建立一個新的。其它的都已經被討論過了。最後,不要忘記在檔案頂端引入下面的框架:

建立和儲存一個新的聯絡人

現在我們已經看到了關於這個新的聯絡人框架的很多新特性。但是,還有一個部分至今還沒有被討論到,那就是怎樣在程式碼裡建立一個新的聯絡人資訊然後把它儲存到資料庫中。因此,正如你瞭解的,在這個部分我們將會覆蓋這個方面。我不會花過多精力講解怎樣更新一個已有的聯絡人記錄,因為這個跟我們馬上要看到的任務很類似,所以我把這部分留給你去找到這兩個任務的不同點。

除了CNContact類展示了一個單個的聯絡人記錄和它的所有的屬性外,聯絡人框架(Contacts framework)提供了另一個類,叫CNMutableContact。正如這個類的名字表達的,它與第一個類很相似;但是,這個類允許重新給一個聯絡人的屬性賦值,建立一個新的或者更新一個已有的聯絡人記錄。真實的儲存(更新)操作都被我們熟知的contact store(CNContactStore)類處理,不過這是建立新的聯絡人的最後一步了。你馬上就會看到詳細的細節步驟。

總體上說,使用CNMutableContact類給一個聯絡人記錄設定屬性值包含了獲取屬性值時的相反操作。意味著比起給簡單屬性只需要直接賦值一個值(例如,名字),特殊屬性必須進行特殊的操作。例如:

當給一個聯絡人記錄設定一個生日日期時,一個NSDateComponents物件必須被建立好並且恰當的賦值給相應的屬性。

當設定一個聯絡人影像時,一個NSData物件必須要賦值給它。

當設定郵箱地址時,對於每個單獨的郵箱地址必須建立一個CNLabeledValue物件,然後所有的這些物件要被組成一個陣列賦值給emailAddresses屬性。

上面僅僅是舉的一些例子。當然還有更多的需要被小心對待的聯絡人屬性,但是不管怎樣,正如你要看到的做這些操作都不會很難。

又回到我們的demo應用,這次我們要切換到 CreateContactViewController.swift 檔案。在這裡面,你會找到一個空的自定義的叫 createContact()的方法。接下來我們要做的事情都寫在這裡。簡單的,我們建立一個新的CNMutableContact的例項,然後我們給所有感興趣的屬性賦值,最後我們利用contact store把這個新的記錄儲存到資料庫中。讓我們來看看實現程式碼:

從頭開始分析,第一步就是初始化一個在後面一直要被用到的CNMutableContact物件。很顯然設定姓和名屬性非常簡單。接下來的家庭郵箱地址屬性必須被建立成一個CNLabeledValue物件,建立的方法在上面已經被展示出來了。一旦新的郵箱地址被建立好了,它就作為一個郵箱地址陣列的一部分賦值給了emailAddresses屬性。當然在我們這個例子中沒有任何其他的地址。最後,我們基於使用者選擇的日期給新聯絡人設定生日日期。如上展示的利用NSCalendar類從一個NSDate物件建立一個NSDateComponents物件非常簡單。請注意日曆單元(year, month, day)是怎樣被結合在一起而組成一個最終的理想屬性值的。

給出的程式碼片段中最有趣的部分就是一個新的聯絡人記錄是怎樣被儲存的。正如你可能注意到了,有必要先建立一個CNSaveRequest物件,然後把新的聯絡人物件加給它。直到這時都沒有任何實質性的儲存操作。接下來馬上就要發生儲存操作了,那就是當contact store 物件的 executeSaveRequest:方法被呼叫的時候

當新的聯絡人不能被儲存時,一個警告資訊將會被展示給使用者。

執行應用程式然後利用ViewController左邊的導航欄按鈕來建立一個新的聯絡人。儲存你的記錄,然後用我們先前提到的任何一個方法來搜尋它。

26.png

重要提示:我注意到了當我在寫這篇教程時,我的測試當中,當建立一個新的聯絡人記錄並且儲存到聯絡人資料庫時,想要通過應用來訪問聯絡人詳細資訊(通過點選一個聯絡人)時是不再允許的。除錯皮膚上會出現下面的資訊:

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

目前網站上面還沒有什麼有用的幫助,只是把這個作為了一個bug提交給了Apple。記住這點,當你要測試應用時要避免同時建立一個聯絡人記錄。

總結

到了這個教程的最後了,我希望我已經使你清楚看到了與新的聯絡人框架Contactsframework打交道是多麼的簡單。如果你在以前使用過AddressBook API, ?你就可以證實與聯絡人打交道時在這裡看到的方法是包含了巨大的改變。你可以儘可能的使用這個demo應用,以任何你想要的方式來改變它擴充套件它。它始終都有提高的空間。不過千萬不要忘記使用者隱私設定和必須尊重使用者對於是否允許這個應用訪問聯絡人資訊的選擇。不要錯過官方文件,你也會在那裡找到很多有趣的東西。我希望你喜歡這個教程並且認為它有用;下次見,希望你過得愉快!

作為參考,你可以在這裡下載全部的 Xcode project

相關文章