本文由CocoaChina譯者WangYue(微博)翻譯
By?gabriel theodoropoulos
原文:A First Look at Contacts Framework in iOS 9
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應用,我會向你展示盡量多的關於這個新框架的內容。事實上,在接下來的部分我會向你展示怎樣去:
檢查這個應用是否被授權獲取聯系人信息以及怎樣發起授權請求。
使用3個不同的方法獲取聯系人信息。其中一個包括使用picker view controller
訪問獲取到的聯系人的屬性以及合理的格式化它們以用來展示。
使用默認Contacts ?UI 的來選取,查看, 甚至編輯聯系人信息。
創建一個新的聯系人記錄。
我把這個demo應用命名為Birthdays,因為它的目的是對所有引入到該應用中的聯系人的生日做展示。聯系人的全名,照片(如果有)和家庭郵箱地址也會被展示。理想狀態下,這個應用應該可以是個生日提示器,當然,我們不會處理通知,短信發送以及其它相關動作。
這個應用是基於導航模式的,而且它由下面的部分組成:
當這個應用開始時, ViewController是默認被展示的。它展示了先前我提到的所有引入的聯系人的信息,並且提供了獲取更多聯系人信息(右上角按鈕)操作,創建一個新聯系人(左上角按鈕),以及通過點擊一行來查看聯系人詳細信息的方法。
聯系人詳細信息將會展示在內嵌的聯系人view controller 裡。正如你將會看到的那樣,你可以展示所有的屬性或者只選擇你感興趣展示的那部分。
獲取聯系人信息在後面將會是一個很有趣的部分。我會通過3種不同的處理方式向你展示3種方法。
第一種,我們將會輸入一個聯系人名字(或者名字的一部分),然後通過點擊鍵盤上的return按鈕,這個應用就會獲取匹配輸入名字的聯系人信息。
正如你將會在下面看到的截圖那樣,在屏幕中間有一個picker view。我們將會利用它來找到所有跟在picker裡選中的月份所匹配的聯系人的生日月份,而且獲取操作會在點擊Done按鈕的時候被觸發。
我們會利用框架提供的默認picker view controller來直接查看和選擇聯系人信息。 注意在這個controller裡展示的聯系人信息是可以被定制化的,picker view controller裡的行為也一樣。稍後你將會看到怎樣做。
這就是picker view controller,只展現了具有有效生日日期的聯系人集合:
應用的最後部分是關於創建一個新的聯系人。這是個很簡單的任務,歸功於這個demo 應用我們將會利用下面的view controller 來鍵入將要被創建的聯系人的姓名,家庭郵箱地址和生日(我們這裡不處理圖片,因為此刻它並不是最重要的)。
這個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屬性。在文件的頂部,添加下面的代碼:
var contactStore = CNContactStore()
必須在類聲明的頂部添加下面的框架:
import Contacts
非常好!現在,在我們處理應用的授權狀態以及針對這個狀態可以做的操作之前,我們先寫下兩個簡單且方便的方法。注意它們並不是為了繼續這個項目而需要的,不要它們也可以做我們的工作。但是,實現一些針對完成某個目的的小方法被證明了是非常便利的。
因此,第一個小方法就是從任何其它類中可以簡單訪問應用的delegate類(AppDelegate)的方法。通常,下面的代碼可以實現訪問應用的delegate:
UIApplication.sharedApplication().delegate as! AppDelegate
但是,我個人發現每次當我需要獲取app delegate時都要寫下所有的以上代碼,這感覺像是被中途打擾了。如果我們編寫下面的這個類方法又會怎樣?
class func getAppDelegate() -> AppDelegate { return UIApplication.sharedApplication().delegate as! AppDelegate }
通過它,我們可以通過一種更簡便的方式來訪問app delegate的任何屬性或者方法。例如,我們可以像下面展示那樣在工程中的任何類中獲取contacts store屬性:
AppDelegate.getAppDelegate().contactStore
第二個我們將會在文件中添加的便捷方法就是一個展示提示信息的controller,提示信息變量是通過參數傳遞的。實現起來並不復雜,但是有個特殊的地方要注意;一個提示controller必須被一個view controller展示,而應用的app delegate 並不是一個view controller。
為了解決這個問題,我們有必要找到當前app window上的最上層的view controller, 然後在這個view controller上展示這個提示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 類。它包含以下的4種值:
NotDetermined: 這個狀態表示用戶現在為止還沒有允許或者拒絕對聯系人數據庫的訪問。應用在設備上第一次安裝時就會是這個狀態。
Restricted: 這個狀態表示應用不僅不能訪問聯系人數據,而且用戶也沒有權限在Settings裡修改這個權限。這個狀態可能是其他活躍的限制條件的結果(例如. Parental control)
Denied: 當應用是這個狀態時,表示用戶已經選擇了不允許訪問聯系人數據信息。而這個只能被用戶本人改變。
Authorized: 這是每個應用的理想狀態。當應用是這個狀態時,它可以自由訪問聯系人數據庫並且實現需要聯系人數據的任務。
有一件事必須弄清楚:安裝應用後,用戶第一次(且僅是第一次)想試圖與聯系人數據交互(例如,獲取聯系人信息)時,iOS將會展示一個預定義好的提示controller來要求用戶對應用授權:
如果用戶允許訪問,一切都好。但是,如果用戶拒絕訪問,那麼基於聯系人信息的所有功能都不可能被執行。在我們的demo應用裡,且在這個特定情景下,我們將展現一個自定義的提示信息(使用我們上面已實現好的方法)來告訴用戶他必須在Settings裡設置允許訪問聯系人信息的權限。我們將會在即將實現的新方法裡處理這種情況。當然,我們也會在那個方法裡考慮所有可能出現的授權狀態。首先來看看這個方法是什麼樣的,稍後將會對它做更多講解:
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) } }
查看上面的方法,你會看到它包含了一個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個重要的參數:
Predicate: 一個NSPredicate對象作為返回結果的過濾器。非常重要且必須強調的是只有從CNContact類中得到的predicates對象才會被接受,普通的你自己創建的predicates對象 (看這裡)就不行。在所有CNContact類中支持獲取predicate的函數中,有一個叫做predicateForContactsMatchingName:,我們將會使用它。
keysToFetch: 通過設置這個變量,你指定你想獲取的聯系人的部分信息。它是一個數組包含了描述被搜索聯系人 (CNContact 對象)屬性值的字符串。框架提供預定義好的常量字符串來作為鍵值(keys)。
注意:這個方法可以返回一個exception, 因此它必須在一個使用try關鍵字的do-catch 聲明裡被調用。錯誤情況是被聲明裡的catch 處理的。
unifiedContactsMatchingPredicate:keysToFetch:error:方法的返回結果是一個符合給出的predicate變量的所有CNContact對象的數組,或者出現了什麼錯誤時返回nil。
記住這些之後,是時候繼續實現的步驟了。這次打開AddContactViewController.swift文件,直接找到文件的頂部。在這裡引入Contacts framework, 如果沒有它什麼都不能做。
import Contacts
現在我們找到textFieldShouldReturn: delegate 方法。最開始我們使用在application delegate裡創建的最後一個方法,這樣我們就可以查看應用是否有獲取聯系人權限以便繼續後面的工作:
func textFieldShouldReturn(textField: UITextField) -> Bool { AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in if accessGranted { } } return true }
如果是被授權了的,我們就可以准備匹配聯系人信息的predicate 和keys。同它們一起,我們還要聲明一些其他的變量:一個存儲結果(如果有的話)的數組變量,一個字符串變量用來存儲當沒有匹配聯系人信息結果時或者獲取操作失敗時要展示的自定義消息。
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 }
在這裡請注意我們是怎樣指明predicate 和keys 數組的,然後我們繼續。在下一步,我們將會試圖獲取聯系人數據,如果操作成功的話,那麼我們一開始創建的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裡。最好且最簡單的實現過程就是使用熟知的Delegate 模式。因此,我們按照這個思路繼續實現以填補應用中的銜接點。
在AddContactViewController.swift 文件中,在類上方創建下面這只有一個方法的協議:
protocol AddContactViewControllerDelegate { func didFetchContacts(contacts: [CNContact]) }
通過使用上面的這個代理方法,我們不僅可以讓ViewController 類知道聯系人信息已經被獲取到了,而且還可以通過它傳遞新獲取到的聯系人信息。
然後,在 AddContactViewController裡添加下面的代理聲明:
var delegate: AddContactViewControllerDelegate!
回想一下,我們在上面的 textFieldShouldReturn: 方法最後的else部分留了空白,現在是時候添加上缺失的代碼了。事實上,只缺少了兩行代碼:一個是我們調用上面剛聲明的delegate方法,第二個就是在navigation controller 中彈出view controller。
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就不能在合適時間被更新,你就會遭遇APP應用的一些非預期反應。
現在是時候轉到 ViewController.swift 文件中去處理獲取到的聯系人信息了。首先,我們在這個類裡也必須先引入Contacts framework。
import Contacts
接下來,我們需要接受新建的自定義協議,因此需要在類名後面添加上協議的名字:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, AddContactViewControllerDelegate
現在,有必要聲明一個 CNContact 對象的數組。這個數組會保存所有從獲取請求裡得到的聯系人,它將會是我們tableview的datasource。因此,在ViewController的頂部添加下面的代碼:
var contacts = [CNContact]()
還有我們必須更新tableview將要展示的行的數量,如下:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contacts.count }
在我們實現先前聲明的delegate 方法之前,必須要聲明ViewController 類就是AddContactViewControllerDelegate 協議的delegate。這個將會在 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 } } }
最後,我們必須實現我們自定義的delegate 方法。在這裡面我們將會一個一個的得到所有返回的聯系人信息然後把它們都添加到contacts數組裡。最後,我們會重新加載tableview讓它展示最新的聯系人信息。
func didFetchContacts(contacts: [CNContact]) { for contact in contacts { self.contacts.append(contact) } tblContacts.reloadData() }
現在我們來展示聯系人信息。在每個cell裡將會展示聯系人的姓和名,如果有出生日期就展示,沒有就展示一個簡短的信息,如果有圖像和家庭郵箱那麼也展示。下面你將看到的實現代碼在後面會被更改一點點,不過已經足夠讓你了解一個聯系人的屬性信息是怎樣被獲取到的。所以,讓我們看一看吧:
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)" // 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 home 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 }
讓我們來過一過上面的實現步驟。首先,我們設置聯系人全名是通過把姓和名連接起來的方法。稍後我會給你展示另一個方法來獲取全名,現在先使用這個方法。接下來,我們設置生日日期信息。如果有生日信息,我們就用最簡單的方法展示它。注意這只是一個臨時的方案,稍後我們會用一個更合適的方法來構建生日日期。同樣,非常重要的一點是了解生日信息並非是一個NSDate對象。取代它的是一個 NSDateComponents 對象,當然這個對象可以轉化為一個 NSDate對象然後轉化為一個String對象。
下面我們要設置的就是圖像數據。如果它不存在,你將會在它的位置上看到一個我在自定義cell的xib 文件中添加的 imgContactImage 的背景顏色,
最終,剩下了家庭郵件地址我們要設置了。你看到了我們使用了一個loop循環來遍歷所有的郵件地址直到找到我們要的那個。這樣做是因為聯系人信息的emailAddresses屬性包含了所有存在的以郵箱地址為標簽值( labeled values )的(CNLabeledValue)對象。
如果你現在運行應用,根據你輸入的名字而選出來的聯系人信息,上面的實現代碼可能有用,但是也有可能沒用。在第二個案例中應用就會崩潰,不過你不必擔心。我們稍後會改進。我故意沒有給你展現上面的函數的最終實現代碼,就是因為按現在這種方式能夠更清楚的向你展示所有東西都是怎樣工作的。
重新獲取聯系人
應用可能崩潰的原因就是並非所有的你需要獲取的聯系人屬性信息都存在。因為這樣,CNContact 類包含了一個叫 isKeyAvailable: 的方法,它必須在你訪問任何聯系人屬性信息之前被調用。例如,在我們試圖展示生日日期,圖片和郵箱地址之前應該添加下面的檢查方法:
if currentContact.isKeyAvailable(CNContactBirthdayKey) { ... } if currentContact.isKeyAvailable(CNContactImageDataKey) { ... } if currentContact.isKeyAvailable(CNContactEmailAddressesKey) { ... }
如果一個key值沒有被找到,那麼必須進行的操作就是重新獲取這個聯系人的信息然後再次展示。這就是我們現在要做的,更詳細的就是我們將要在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 }
上面調用的方法就是我們馬上要實現的方法。除此之外,我想我們增加的判斷條件已經很直白了你應該能理解它的邏輯。注意通過這個更改,應用再也不會崩潰了,即使是在結果中包含了不可用的鍵值信息時也一樣。
現在我們來看新的方法:
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:方法。它的目的就是獲取滿足標識符參數值的一個特定的聯系人數據。一旦這個結果返回後,我們就用這個新聯系人信息去替代在數組裡的舊信息。最後,我們重新加載tableview的特定的行。
如果你想,可以再試試運行這個APP。萬一有某些聯系人信息沒獲取到時,你最好始終做一下重新獲取聯系人信息的操作,這樣就能確保你的應用不會給用戶展示任何“驚喜”。
格式化輸出結果
正如目前你看到的,在cell上展示每個聯系人生日信息之前我們沒有做任何合適的格式轉換。我們只是鏈接和展示了生日日期屬性,現在在已經學習了先前的重要知識後,是時候來處理這個問題了。
我們會通過在ViewController 中創建一個新的自定義方法來修補生日日期。在這方法裡,我們將會使用一個NSDateFormatter對象來轉換日期信息為一個本地化字符串,但是首先,我們必須把日期組件(也就是日期部分)轉換成一個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 { // Set the birthday info. if let birthday = currentContact.birthday { cell.lblBirthday.text = getDateStringFromComponents(birthday) } ... } return cell }
非常好。現在生日日期將會以一個更美觀時尚的方式展現出來了。
現在我們來看看關於展示姓和名的方面。CNContact 類提供了內嵌格式轉換器,它可以幫助我們輕松地格式化兩種數據:聯系人的全名信息 (CNContactFormatter) 和地址(CNPostalAddressFormatter)。現在我們就將使用第一個,這樣聯系人全名就自動被聯系人框架(Contacts framework)格式化了。
我們來最後一次更改聯系人的展示方法如下:
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幫我們做了並且它輸出了一個本地化的字符串(通過以適當的順序設置名字部分,依靠設備的本地化設置)。
但是,上面的操作會引起一些復雜操作,因為聯系人格式器(contact formatter )需要訪問與一個聯系人名字相關的所有關鍵字(keys),即使是那些我們並沒有在獲取數組裡指定的那些關鍵字。但是,我們不需要一個個的寫下它們所有。所有相關關鍵字都被一個key descriptor指定了,而這個decriptor 替代了在關鍵字數組裡指定的所有單個關鍵字。
為了使這個更具體,找到AddContactViewController文件,在 textFieldShouldReturn:方法裡,把下面的代碼:
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
替換成下面使用了key descriptor的代碼
let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
這個descriptor被組織的方式很具體,正如上面展示那樣。除了這個,其它關鍵字信息保持不變。
上面的改變必須在refetchContact: 方法(在 ViewController裡)裡執行。你所要做的就是利用上面那行代碼替換關鍵字數組的定義,來做吧:
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] ... } } }
通過這些處理,我們已經在代碼裡做了所有跟格式相關的改變。當然,你也可以仍然使用單個關鍵字來獲取單個名字信息,這始終取決於你的要求。
使用自定義過濾器獲取聯系人
在這篇教程裡我最先展示之一的就是怎樣使用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 按鈕被按下時才去獲取基於月份選擇的聯系人記錄。
請看:
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") } } } }
正如你看到的,在完結時我們調用了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,讓你可以限制聯系人的展示和改變默認行為。
predicateForEnablingContact: 這個可能是你要用的最多的一個predicate。有了它,你可以指定在picker controller裡面哪些聯系人信息可用。你可以用那個方法來過濾出需要的聯系人,比如僅僅讓那些有著有效地生日日期的聯系人才可以被選擇。
predicateForSelectionOfContact: 有了它,你可以控制picker view controller在哪種條件下可以返回被選中的聯系人,以及對於其它的選中模式,什麼時候它可以展示details view controller .
predicateForSelectionOfProperty: 使用它,你可以指定一個屬性的默認響應是否應該被執行(比如當點擊一個電話號碼時是否需要創建打電話行為),或者被點擊的屬性是否應該被返回。
在這裡我們將要只使用第一個predicate,向picker view controller請求,這個picker view controller只允許那些有著生日日期的聯系人可用。使用其它兩個並不困難,不過我們在這裡並不需要他們;為了給你一個參考我推薦你去看相關的文檔.
又回到我們的應用中,打開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) }
就是這樣簡單!在這個demo應用中當點擊一個聯系人時我們不能展示它的詳細卡信息。但是如果你想在你的應用中這麼做的話,在詳細界面中很簡單的就可以控制要被展示的屬性值。你所要做的就是在一個名叫 displayedPropertyKeys的屬性中聲明一個你想要展示的屬性的關鍵字的數組。例如,如果我們想要在應用中展示詳細信息,那麼我們需要在展示picker view controller之前添加下面的這行代碼:
contactPickerViewController.displayedPropertyKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]
先前我們采用了CNContactPickerDelegate協議,現在就要來實現一個必須的delegate方法了。在這個方法裡,我們將會得到被選中的聯系人記錄,然後我們會通過我們自定義的delegate 方法來把這些記錄返回到ViewController裡。
func contactPicker(picker: CNContactPickerViewController, didSelectContact contact: CNContact) { delegate.didFetchContacts([contact]) navigationController?.popViewControllerAnimated(true) }
如果你想要展示聯系人詳細信息而且想要處理一個返回的屬性值,那麼你需要使用contactPicker:didSelectContactProperty:這個delegate方法。我們不會在這裡實現它,因為我們不需要它。你可以在 這裡.找到所有delegate方法的集合。
可以再次驗證這個應用了。這次使用 “Open contacts to select” 按鈕來展示picker view controller。你會發現聯系人中沒有有效的生日日期的是不會展示的。選擇一個聯系人,你將會看到這個聯系人將會在ViewController裡的tableview中被展示。
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 展示。
說了這麼多,來看看相應的代碼吧:
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") } } }) } }
在上面的代碼段中可以看到我們簡單的使用了contacts view controller 實例對象的displayedPropertyKeys屬性就指定了想要被展示的屬性值。另一個細節值得注意的就是我們利用contactStore屬性來向contacts view controller 提供 contact ?store 實例。如果應用中沒有一個已存在的CNContactStore實例那麼這個操作也不是強制要做的,因為CNContactsViewController會自動創建一個新的。其它的都已經被討論過了。最後,不要忘記在文件頂端引入下面的框架:
import?ContactsUI
創建和保存一個新的聯系人
現在我們已經看到了關於這個新的聯系人框架的很多新特性。但是,還有一個部分至今還沒有被討論到,那就是怎樣在代碼裡創建一個新的聯系人信息然後把它儲存到數據庫中。因此,正如你了解的,在這個部分我們將會覆蓋這個方面。我不會花過多精力講解怎樣更新一個已有的聯系人記錄,因為這個跟我們馬上要看到的任務很類似,所以我把這部分留給你去找到這兩個任務的不同點。
除了CNContact類展示了一個單個的聯系人記錄和它的所有的屬性外,聯系人框架(Contacts framework)提供了另一個類,叫CNMutableContact。正如這個類的名字表達的,它與第一個類很相似;但是,這個類允許重新給一個聯系人的屬性賦值,創建一個新的或者更新一個已有的聯系人記錄。真實的保存(更新)操作都被我們熟知的contact store(CNContactStore)類處理,不過這是創建新的聯系人的最後一步了。你馬上就會看到詳細的細節步驟。
總體上說,使用CNMutableContact類給一個聯系人記錄設置屬性值包含了獲取屬性值時的相反操作。意味著比起給簡單屬性只需要直接賦值一個值(例如,名字),特殊屬性必須進行特殊的操作。例如:
當給一個聯系人記錄設置一個生日日期時,一個NSDateComponents對象必須被創建好並且恰當的賦值給相應的屬性。
當設置一個聯系人圖像時,一個NSData對象必須要賦值給它。
當設置郵箱地址時,對於每個單獨的郵箱地址必須創建一個CNLabeledValue對象,然後所有的這些對象要被組成一個數組賦值給emailAddresses屬性。
上面僅僅是舉的一些例子。當然還有更多的需要被小心對待的聯系人屬性,但是不管怎樣,正如你要看到的做這些操作都不會很難。
又回到我們的demo應用,這次我們要切換到 CreateContactViewController.swift 文件。在這裡面,你會找到一個空的自定義的叫 createContact()的方法。接下來我們要做的事情都寫在這裡。簡單的,我們創建一個新的CNMutableContact的實例,然後我們給所有感興趣的屬性賦值,最後我們利用contact store把這個新的記錄存儲到數據庫中。讓我們來看看實現代碼:
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對象。很顯然設置姓和名屬性非常簡單。接下來的家庭郵箱地址屬性必須被創建成一個CNLabeledValue對象,創建的方法在上面已經被展示出來了。一旦新的郵箱地址被創建好了,它就作為一個郵箱地址數組的一部分賦值給了emailAddresses屬性。當然在我們這個例子中沒有任何其他的地址。最後,我們基於用戶選擇的日期給新聯系人設置生日日期。如上展示的利用NSCalendar類從一個NSDate對象創建一個NSDateComponents對象非常簡單。請注意日歷單元(year, month, day)是怎樣被結合在一起而組成一個最終的理想屬性值的。
給出的代碼片段中最有趣的部分就是一個新的聯系人記錄是怎樣被存儲的。正如你可能注意到了,有必要先創建一個CNSaveRequest對象,然後把新的聯系人對象加給它。直到這時都沒有任何實質性的存儲操作。接下來馬上就要發生存儲操作了,那就是當contact store 對象的 executeSaveRequest:方法被調用的時候
當新的聯系人不能被存儲時,一個警告信息將會被展示給用戶。
運行應用程序然後利用ViewController左邊的導航欄按鈕來創建一個新的聯系人。保存你的記錄,然後用我們先前提到的任何一個方法來搜索它。
重要提示:我注意到了當我在寫這篇教程時,我的測試當中,當創建一個新的聯系人記錄並且存儲到聯系人數據庫時,想要通過應用來訪問聯系人詳細信息(通過點擊一個聯系人)時是不再允許的。調試面板上會出現下面的信息:
[CNUI ERROR] error calling service – Couldn’t communicate with a helper application.
目前網站上面還沒有什麼有用的幫助,只是把這個作為了一個bug提交給了Apple。記住這點,當你要測試應用時要避免同時創建一個聯系人記錄。
總結
到了這個教程的最後了,我希望我已經使你清楚看到了與新的聯系人框架Contactsframework打交道是多麼的簡單。如果你在以前使用過AddressBook API, ?你就可以證實與聯系人打交道時在這裡看到的方法是包含了巨大的改變。你可以盡可能的使用這個demo應用,以任何你想要的方式來改變它擴展它。它始終都有提高的空間。不過千萬不要忘記用戶隱私設置和必須尊重用戶對於是否允許這個應用訪問聯系人信息的選擇。不要錯過官方文檔,你也會在那裡找到很多有趣的東西。我希望你喜歡這個教程並且認為它有用;下次見,希望你過得愉快!
作為參考,你可以在這裡下載全部的 Xcode project.