干货:初窥iOS9联系人框架

移动开发
iOS 9为用户和开发者展示了很多新的技术和在现有技术上的优化。正如我们看到的,在这个版本里有很多第一次展示的新的内容,也有很多已有的框架和类的变化和更新。除此之外,始终惊喜的是,有一些旧的APIs被放弃和不再建议使用,为新的全新开发的或用来做过渡的APIs让位。在iOS9中的例子就是全新的Contacts framework, 它以更流行的模式来替代旧的 AddressBook 框架,更简单和直接。

[[155660]]

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属性。在文件的顶部,添加下面的代码:

  1. var contactStore = CNContactStore() 

必须在类声明的顶部添加下面的框架:

  1. import Contacts 

非常好!现在,在我们处理应用的授权状态以及针对这个状态可以做的操作之前,我们先写下两个简单且方便的方法。注意它们并不是为了继续这个项目而需要的,不要它们也可以做我们的工作。但是,实现一些针对完成某个目的的小方法被证明了是非常便利的。

因此,第一个小方法就是从任何其它类中可以简单访问应用的delegate类(AppDelegate)的方法。通常,下面的代码可以实现访问应用的delegate:

  1. UIApplication.sharedApplication().delegate as! AppDelegate 

但是,我个人发现每次当我需要获取app delegate时都要写下所有的以上代码,这感觉像是被中途打扰了。如果我们编写下面的这个类方法又会怎样?

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

通过它,我们可以通过一种更简便的方式来访问app delegate的任何属性或者方法。例如,我们可以像下面展示那样在工程中的任何类中获取contacts store属性:

  1. AppDelegate.getAppDelegate().contactStore 

第二个我们将会在文件中添加的便捷方法就是一个展示提示信息的controller,提示信息变量是通过参数传递的。实现起来并不复杂,但是有个特殊的地方要注意;一个提示controller必须被一个view controller展示,而应用的app delegate 并不是一个view controller。

为了解决这个问题,我们有必要找到当前app window上的最上层的view controller, 然后在这个view controller上展示这个提示controller。下面是实现方法:

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

现在我们做很重要的事了,那就是处理应用的授权状态。这个状态是被CNAuthorizationStatus枚举值展现的并且是属于CNContactStore 类。它包含以下的4种值:

  1. NotDetermined: 这个状态表示用户现在为止还没有允许或者拒绝对联系人数据库的访问。应用在设备上第一次安装时就会是这个状态。

  2. Restricted: 这个状态表示应用不仅不能访问联系人数据,而且用户也没有权限在Settings里修改这个权限。这个状态可能是其他活跃的限制条件的结果(例如. Parental control)

  3. Denied: 当应用是这个状态时,表示用户已经选择了不允许访问联系人数据信息。而这个只能被用户本人改变。

  4. Authorized: 这是每个应用的理想状态。当应用是这个状态时,它可以自由访问联系人数据库并且实现需要联系人数据的任务。

#p#

有一件事必须弄清楚:安装应用后,用户第一次(且仅是第一次)想试图与联系人数据交互(例如,获取联系人信息)时,iOS将会展示一个预定义好的提示controller来要求用户对应用授权:

t43_5_ask_authorization_alert.png

如果用户允许访问,一切都好。但是,如果用户拒绝访问,那么基于联系人信息的所有功能都不可能被执行。在我们的demo应用里,且在这个特定情景下,我们将展现一个自定义的提示信息(使用我们上面已实现好的方法)来告诉用户他必须在Settings里设置允许访问联系人信息的权限。我们将会在即将实现的新方法里处理这种情况。当然,我们也会在那个方法里考虑所有可能出现的授权状态。首先来看看这个方法是什么样的,稍后将会对它做更多讲解:

  1. func requestForAccess(completionHandler: (accessGranted: Bool) -> Void) { 
  2.     let authorizationStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts) 
  3.   
  4.     switch authorizationStatus { 
  5.     case .Authorized: 
  6.         completionHandler(accessGranted: true
  7.   
  8.     case .Denied, .NotDetermined: 
  9.         self.contactStore.requestAccessForEntityType(CNEntityType.Contacts, completionHandler: { (access, accessError) -> Void in 
  10.             if access { 
  11.                 completionHandler(accessGranted: access) 
  12.             } 
  13.             else { 
  14.                 if authorizationStatus == CNAuthorizationStatus.Denied { 
  15.                     dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  16.                         let message = "\(accessError!.localizedDescription)\n\nPlease allow the app to access your contacts through the Settings." 
  17.                         self.showMessage(message) 
  18.                     }) 
  19.                 } 
  20.             } 
  21.         }) 
  22.   
  23.     default
  24.         completionHandler(accessGranted: false
  25.     } 

查看上面的方法,你会看到它包含了一个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, 如果没有它什么都不能做。

  1. import Contacts 

现在我们找到textFieldShouldReturn: delegate 方法。最开始我们使用在application delegate里创建的最后一个方法,这样我们就可以查看应用是否有获取联系人权限以便继续后面的工作:

  1. func textFieldShouldReturn(textField: UITextField) -> Bool { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.   
  5.         } 
  6.     } 
  7.   
  8.     return true 

如果是被授权了的,我们就可以准备匹配联系人信息的predicate 和keys。同它们一起,我们还要声明一些其他的变量:一个存储结果(如果有的话)的数组变量,一个字符串变量用来存储当没有匹配联系人信息结果时或者获取操作失败时要展示的自定义消息。

  1. func textFieldShouldReturn(textField: UITextField) -> Bool { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.             let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!) 
  5.             let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey] 
  6.             var contacts = [CNContact]() 
  7.             var message: String! 
  8.   
  9.         } 
  10.     } 
  11.   
  12.     return true 

在这里请注意我们是怎样指明predicate 和keys 数组的,然后我们继续。在下一步,我们将会试图获取联系人数据,如果操作成功的话,那么我们一开始创建的contacts数组将会被填满返回的结果。如果没有找到联系人信息或获取操作失败了,我们接下来会展示一个自定义的消息;有了这些之后这个方法里的实现代码基本上就完了。

  1. func textFieldShouldReturn(textField: UITextField) -> Bool { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.             let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!) 
  5.             let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey] 
  6.             var contacts = [CNContact]() 
  7.             var message: String! 
  8.   
  9.             let contactsStore = AppDelegate.getAppDelegate().contactStore 
  10.             do { 
  11.                 contacts = try contactsStore.unifiedContactsMatchingPredicate(predicate, keysToFetch: keys) 
  12.   
  13.                 if contacts.count == 0 { 
  14.                     message = "No contacts were found matching the given name." 
  15.                 } 
  16.             } 
  17.             catch { 
  18.                 message = "Unable to fetch contacts." 
  19.             } 
  20.   
  21.   
  22.             if message != nil { 
  23.                 dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  24.                     AppDelegate.getAppDelegate().showMessage(message) 
  25.                 }) 
  26.             } 
  27.             else { 
  28.   
  29.             } 
  30.         } 
  31.     } 
  32.   
  33.     return true 

正如你见到的,我们在else 方法里什么都没实现,但马上在后面我们就会重新访问它并且添加上缺失的代码。这这部分最重要的是要了解我们是怎样获取匹配给出名字的联系人信息,以及在没得到预想情况下时我们是怎样处理的。

展示获取到的联系人

最好的情况就是我们的获取操作可以返回匹配的联系人信息,然后有必要在ViewController的tableview里展示它们。但是,第一步就是要告诉ViewController联系人信息已经是被获取到了,这一切操作都发生在AddContactViewController里。最好且最简单的实现过程就是使用熟知的Delegate 模式。因此,我们按照这个思路继续实现以填补应用中的衔接点。

在AddContactViewController.swift 文件中,在类上方创建下面这只有一个方法的协议:

  1. protocol AddContactViewControllerDelegate { 
  2.     func didFetchContacts(contacts: [CNContact]) 

通过使用上面的这个代理方法,我们不仅可以让ViewController 类知道联系人信息已经被获取到了,而且还可以通过它传递新获取到的联系人信息。

然后,在 AddContactViewController里添加下面的代理声明:

  1. var delegate: AddContactViewControllerDelegate! 

回想一下,我们在上面的 textFieldShouldReturn: 方法最后的else部分留了空白,现在是时候添加上缺失的代码了。事实上,只缺少了两行代码:一个是我们调用上面刚声明的delegate方法,第二个就是在navigation controller 中弹出view controller。

  1. func textFieldShouldReturn(textField: UITextField) -> Bool { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.             ... 
  5.   
  6.             if message != nil { 
  7.                 ... 
  8.             } 
  9.             else { 
  10.                 dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  11.                     self.delegate.didFetchContacts(contacts) 
  12.                     self.navigationController?.popViewControllerAnimated(true
  13.                 }) 
  14.             } 
  15.         } 
  16.     } 
  17.   
  18.     return true 

正如你看到的,当处理涉及UI操作时我们都会使用主线程。这是一个你不应该忘记的非常重要的细节,否则UI就不能在合适时间被更新,你就会遭遇APP应用的一些非预期反应。

现在是时候转到 ViewController.swift 文件中去处理获取到的联系人信息了。首先,我们在这个类里也必须先引入Contacts framework。

  1. import Contacts 

接下来,我们需要接受新建的自定义协议,因此需要在类名后面添加上协议的名字:

  1. class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, AddContactViewControllerDelegate 

现在,有必要声明一个 CNContact 对象的数组。这个数组会保存所有从获取请求里得到的联系人,它将会是我们tableview的datasource。因此,在ViewController的顶部添加下面的代码:

  1. var contacts = [CNContact]() 

还有我们必须更新tableview将要展示的行的数量,如下:

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

在我们实现先前声明的delegate 方法之前,必须要声明ViewController 类就是AddContactViewControllerDelegate 协议的delegate。这个将会在 prepareForSegue:方法里实现:

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

最后,我们必须实现我们自定义的delegate 方法。在这里面我们将会一个一个的得到所有返回的联系人信息然后把它们都添加到contacts数组里。最后,我们会重新加载tableview让它展示最新的联系人信息。

  1. func didFetchContacts(contacts: [CNContact]) { 
  2.     for contact in contacts { 
  3.         self.contacts.append(contact) 
  4.     } 
  5.   
  6.     tblContacts.reloadData() 

现在我们来展示联系人信息。在每个cell里将会展示联系人的姓和名,如果有出生日期就展示,没有就展示一个简短的信息,如果有图像和家庭邮箱那么也展示。下面你将看到的实现代码在后面会被更改一点点,不过已经足够让你了解一个联系人的属性信息是怎样被获取到的。所以,让我们看一看吧:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell 
  3.   
  4.     let currentContact = contacts[indexPath.row] 
  5.   
  6.     cell.lblFullname.text = "\(currentContact.givenName) \(currentContact.familyName)" 
  7.   
  8.   
  9.     // Set the birthday info. 
  10.     if let birthday = currentContact.birthday { 
  11.         cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)" 
  12.     } 
  13.     else { 
  14.         cell.lblBirthday.text = "Not available birthday data" 
  15.     } 
  16.   
  17.   
  18.     // Set the contact image. 
  19.     if let imageData = currentContact.imageData { 
  20.         cell.imgContactImage.image = UIImage(data: imageData) 
  21.     } 
  22.   
  23.   
  24.     // Set the contact's home email address. 
  25.     var homeEmailAddress: String! 
  26.     for emailAddress in currentContact.emailAddresses { 
  27.         if emailAddress.label == CNLabelHome { 
  28.             homeEmailAddress = emailAddress.value as! String 
  29.             break 
  30.         } 
  31.     } 
  32.   
  33.     if homeEmailAddress != nil { 
  34.         cell.lblEmail.text = homeEmailAddress 
  35.     } 
  36.     else { 
  37.         cell.lblEmail.text = "Not available home email" 
  38.     } 
  39.   
  40.   
  41.     return cell 

让我们来过一过上面的实现步骤。首先,我们设置联系人全名是通过把姓和名连接起来的方法。稍后我会给你展示另一个方法来获取全名,现在先使用这个方法。接下来,我们设置生日日期信息。如果有生日信息,我们就用最简单的方法展示它。注意这只是一个临时的方案,稍后我们会用一个更合适的方法来构建生日日期。同样,非常重要的一点是了解生日信息并非是一个NSDate对象。取代它的是一个 NSDateComponents 对象,当然这个对象可以转化为一个 NSDate对象然后转化为一个String对象。

下面我们要设置的就是图像数据。如果它不存在,你将会在它的位置上看到一个我在自定义cell的xib 文件中添加的 imgContactImage 的背景颜色,

最终,剩下了家庭邮件地址我们要设置了。你看到了我们使用了一个loop循环来遍历所有的邮件地址直到找到我们要的那个。这样做是因为联系人信息的emailAddresses属性包含了所有存在的以邮箱地址为标签值( labeled values )的(CNLabeledValue)对象。

如果你现在运行应用,根据你输入的名字而选出来的联系人信息,上面的实现代码可能有用,但是也有可能没用。在第二个案例中应用就会崩溃,不过你不必担心。我们稍后会改进。我故意没有给你展现上面的函数的最终实现代码,就是因为按现在这种方式能够更清楚的向你展示所有东西都是怎样工作的。

重新获取联系人

应用可能崩溃的原因就是并非所有的你需要获取的联系人属性信息都存在。因为这样,CNContact 类包含了一个叫 isKeyAvailable: 的方法,它必须在你访问任何联系人属性信息之前被调用。例如,在我们试图展示生日日期,图片和邮箱地址之前应该添加下面的检查方法:

  1. if currentContact.isKeyAvailable(CNContactBirthdayKey) { 
  2.     ... 
  3.   
  4. if currentContact.isKeyAvailable(CNContactImageDataKey) { 
  5.     ... 
  6.   
  7. if currentContact.isKeyAvailable(CNContactEmailAddressesKey) { 
  8.     ... 

如果一个key值没有被找到,那么必须进行的操作就是重新获取这个联系人的信息然后再次展示。这就是我们现在要做的,更详细的就是我们将要在ViewController里创建一个新的方法。但是,在我们这样做之前,我们先通过添加 isKeyAvailable:方法来修补一下展示联系人详细信息的实现代码。实际上,不是对于上面的属性添加三个不同的条件判断的方法,而是我们只创建一个判断不存在属性的方法,为了以防万一有什么是缺少的,我们要调用即将要实现的一个方法来重新获取联系人信息。我故意没有提到联系人姓名这个关键词,因为在后面的部分将会看到更多关于它的信息。

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell 
  3.   
  4.     let currentContact = contacts[indexPath.row] 
  5.   
  6.     cell.lblFullname.text = "\(currentContact.givenName) \(currentContact.familyName)" 
  7.   
  8.     if !currentContact.isKeyAvailable(CNContactBirthdayKey) || !currentContact.isKeyAvailable(CNContactImageDataKey) ||  !currentContact.isKeyAvailable(CNContactEmailAddressesKey) { 
  9.         refetchContact(contact: currentContact, atIndexPath: indexPath) 
  10.     } 
  11.     else { 
  12.         // Set the birthday info. 
  13.         if let birthday = currentContact.birthday { 
  14.             cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)" 
  15.         } 
  16.         else { 
  17.             cell.lblBirthday.text = "Not available birthday data" 
  18.         } 
  19.   
  20.         // Set the contact image. 
  21.         if let imageData = currentContact.imageData { 
  22.             cell.imgContactImage.image = UIImage(data: imageData) 
  23.         } 
  24.   
  25.         // Set the contact's work email address. 
  26.         var homeEmailAddress: String! 
  27.         for emailAddress in currentContact.emailAddresses { 
  28.             if emailAddress.label == CNLabelHome { 
  29.                 homeEmailAddress = emailAddress.value as! String 
  30.                 break 
  31.             } 
  32.         } 
  33.   
  34.         if homeEmailAddress != nil { 
  35.             cell.lblEmail.text = homeEmailAddress 
  36.         } 
  37.         else { 
  38.             cell.lblEmail.text = "Not available home email" 
  39.         } 
  40.     } 
  41.   
  42.     return cell 

上面调用的方法就是我们马上要实现的方法。除此之外,我想我们增加的判断条件已经很直白了你应该能理解它的逻辑。注意通过这个更改,应用再也不会崩溃了,即使是在结果中包含了不可用的键值信息时也一样。

#p#

现在我们来看新的方法:

  1. func refetchContact(contact contact: CNContact, atIndexPath indexPath: NSIndexPath) { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.             let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey] 
  5.   
  6.             do { 
  7.                 let contactRefetched = try AppDelegate.getAppDelegate().contactStore.unifiedContactWithIdentifier(contact.identifier, keysToFetch: keys) 
  8.                 self.contacts[indexPath.row] = contactRefetched 
  9.   
  10.                 dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  11.                     self.tblContacts.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic) 
  12.                 }) 
  13.             } 
  14.             catch { 
  15.                 print("Unable to refetch the contact: \(contact)", separator: "", terminator: "\n"
  16.             } 
  17.         } 
  18.     } 

首先我们检查这个应用是否被授权了可以访问联系人数据库。然后我们指定想要获取的联系人的部分键值信息,然后重新针对给出的联系人信息进行获取。注意这次我们使用了一个新的方法来做这件事,就是unifiedContactWithIdentifier:keysToFetch:方法。它的目的就是获取满足标识符参数值的一个特定的联系人数据。一旦这个结果返回后,我们就用这个新联系人信息去替代在数组里的旧信息。最后,我们重新加载tableview的特定的行。

如果你想,可以再试试运行这个APP。万一有某些联系人信息没获取到时,你最好始终做一下重新获取联系人信息的操作,这样就能确保你的应用不会给用户展示任何“惊喜”。

格式化输出结果

正如目前你看到的,在cell上展示每个联系人生日信息之前我们没有做任何合适的格式转换。我们只是链接和展示了生日日期属性,现在在已经学习了先前的重要知识后,是时候来处理这个问题了。

我们会通过在ViewController 中创建一个新的自定义方法来修补生日日期。在这方法里,我们将会使用一个NSDateFormatter对象来转换日期信息为一个本地化字符串,但是首先,我们必须把日期组件(也就是日期部分)转换成一个NSDate对象。我们来看看这个新方法:

  1. func getDateStringFromComponents(dateComponents: NSDateComponents) -> String! { 
  2.     if let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents) { 
  3.         let dateFormatter = NSDateFormatter() 
  4.         dateFormatter.locale = NSLocale.currentLocale() 
  5.         dateFormatter.dateStyle = NSDateFormatterStyle.MediumStyle 
  6.         let dateString = dateFormatter.stringFromDate(date) 
  7.   
  8.         return dateString 
  9.     } 
  10.   
  11.     return nil 

上面方法的参数是一个用 NSDateComponents对象表示的日期(在我们的情况下就是出生日期对象)。返回值当然就是一个字符串。为了把dateComponents对象转换为NSDate对象,不会用到超过一行代码。我们使用NSCalendar类来执行这个转换,日期对象已经准备好了要被即将初始化的日期格式器(date formatter)处理。给日期格式器设置当前地理信息是一个必须的操作因为这样就可以获得一个本地化描述的日期信息。最终,我们给日期对象设置一个偏好的格式(不是太长也不是太短),然后我们执行最终的转换操作。转换后的值最终返回给了调用者。

现在我们修复日期信息的展示方法,仅仅简单通过调用上面的函数就可以了:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     ... 
  3.   
  4.     if !currentContact.isKeyAvailable(CNContactBirthdayKey) || !currentContact.isKeyAvailable(CNContactImageDataKey) ||  !currentContact.isKeyAvailable(CNContactEmailAddressesKey) { 
  5.         refetchContact(contact: currentContact, atIndexPath: indexPath) 
  6.     } 
  7.     else { 
  8.         // Set the birthday info. 
  9.         if let birthday = currentContact.birthday { 
  10.             cell.lblBirthday.text = getDateStringFromComponents(birthday) 
  11.         } 
  12.         ... 
  13.     } 
  14.   
  15.     return cell 
  16.   

非常好。现在生日日期将会以一个更美观时尚的方式展现出来了。

现在我们来看看关于展示姓和名的方面。CNContact 类提供了内嵌格式转换器,它可以帮助我们轻松地格式化两种数据:联系人的全名信息 (CNContactFormatter) 和地址(CNPostalAddressFormatter)。现在我们就将使用第一个,这样联系人全名就自动被联系人框架(Contacts framework)格式化了。

我们来最后一次更改联系人的展示方法如下:

  1. func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 
  2.     let cell = tableView.dequeueReusableCellWithIdentifier("idCellContactBirthday") as! ContactBirthdayCell 
  3.   
  4.     let currentContact = contacts[indexPath.row] 
  5.   
  6.     cell.lblFullname.text = CNContactFormatter.stringFromContact(currentContact, style: .FullName) 
  7.   
  8.     ... 
  9.   
  10.     return cell 
  11.   

如你看到的,这一行 cell.lblFullname.text = “(currentContact.givenName) (currentContact.familyName)”代码已经被替换为了如下:

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

很显然的,我们不再需要通过手动链接姓和名来创建联系人的全名了。CNContactFormatter帮我们做了并且它输出了一个本地化的字符串(通过以适当的顺序设置名字部分,依靠设备的本地化设置)。

但是,上面的操作会引起一些复杂操作,因为联系人格式器(contact formatter )需要访问与一个联系人名字相关的所有关键字(keys),即使是那些我们并没有在获取数组里指定的那些关键字。但是,我们不需要一个个的写下它们所有。所有相关关键字都被一个key descriptor指定了,而这个decriptor 替代了在关键字数组里指定的所有单个关键字。

为了使这个更具体,找到AddContactViewController文件,在 textFieldShouldReturn:方法里,把下面的代码:

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

替换成下面使用了key descriptor的代码

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

这个descriptor被组织的方式很具体,正如上面展示那样。除了这个,其它关键字信息保持不变。

上面的改变必须在refetchContact: 方法(在 ViewController里)里执行。你所要做的就是利用上面那行代码替换关键字数组的定义,来做吧:

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

通过这些处理,我们已经在代码里做了所有跟格式相关的改变。当然,你也可以仍然使用单个关键字来获取单个名字信息,这始终取决于你的要求。

使用自定义过滤器获取联系人

在这篇教程里我最先展示之一的就是怎样使用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 按钮被按下时才去获取基于月份选择的联系人记录。

请看:

  1. func performDoneItemTap() { 
  2.     AppDelegate.getAppDelegate().requestForAccess { (accessGranted) -> Void in 
  3.         if accessGranted { 
  4.             var contacts = [CNContact]() 
  5.   
  6.             let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey] 
  7.   
  8.             do { 
  9.                 let contactStore = AppDelegate.getAppDelegate().contactStore 
  10.                 try contactStore.enumerateContactsWithFetchRequest(CNContactFetchRequest(keysToFetch: keys)) { (contact, pointer) -> Void in 
  11.   
  12.                     if contact.birthday != nil && contact.birthday!.month == self.currentlySelectedMonthIndex { 
  13.                         contacts.append(contact) 
  14.                     } 
  15.                 } 
  16.   
  17.                 dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  18.                     self.delegate.didFetchContacts(contacts) 
  19.                     self.navigationController?.popViewControllerAnimated(true
  20.                 }) 
  21.             } 
  22.             catch let error as NSError { 
  23.                 print(error.description, separator: "", terminator: "\n"
  24.             } 
  25.         } 
  26.     } 

正如你看到的,在完结时我们调用了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框架:

  1. import ContactsUI 

接下来,采用CNContactPickerDelegate协议,这样我们就可以处理返回的联系人记录:

  1. class AddContactViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, CNContactPickerDelegate 

从现在起我们的工作要在showContacts: IBAction方法里进行。这个方法会让在AddContactViewController底部的按钮进行操作。让我们来看实现代码:

  1. @IBAction func showContacts(sender: AnyObject) { 
  2.     let contactPickerViewController = CNContactPickerViewController() 
  3.   
  4.     contactPickerViewController.predicateForEnablingContact = NSPredicate(format: "birthday != nil"
  5.   
  6.     contactPickerViewController.delegate = self 
  7.   
  8.     presentViewController(contactPickerViewController, animated: true, completion: nil) 

就是这样简单!在这个demo应用中当点击一个联系人时我们不能展示它的详细卡信息。但是如果你想在你的应用中这么做的话,在详细界面中很简单的就可以控制要被展示的属性值。你所要做的就是在一个名叫 displayedPropertyKeys的属性中声明一个你想要展示的属性的关键字的数组。例如,如果我们想要在应用中展示详细信息,那么我们需要在展示picker view controller之前添加下面的这行代码:

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

先前我们采用了CNContactPickerDelegate协议,现在就要来实现一个必须的delegate方法了。在这个方法里,我们将会得到被选中的联系人记录,然后我们会通过我们自定义的delegate 方法来把这些记录返回到ViewController里。

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

如果你想要展示联系人详细信息而且想要处理一个返回的属性值,那么你需要使用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()方法指明应该要获取的特定的关键字信息然后使用它来重新获取联系人。

#p#

还有,贯穿这个demo应用的用来获取联系人信息的关键字数组再次被证明了使用起来非常便利。不是在我刚才解释的检查可用性里,而是在制定哪些属性应该被展示在contacts view controller 里。你可以在下面的实现代码里看到它是怎样被使用的。一个额外的提示,记住如果你忽略了这个属性,那么所有联系人的属性值(不仅仅是那些我们想要展示的)都会被contacts view controller 展示。

说了这么多,来看看相应的代码吧:

  1. func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { 
  2.     let selectedContact = contacts[indexPath.row] 
  3.   
  4.     let keys = [CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName), CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey] 
  5.   
  6.     if selectedContact.areKeysAvailable([CNContactViewController.descriptorForRequiredKeys()]) { 
  7.         let contactViewController = CNContactViewController(forContact: selectedContact) 
  8.         contactViewController.contactStore = AppDelegate.getAppDelegate().contactStore 
  9.         contactViewController.displayedPropertyKeys = keys 
  10.         navigationController?.pushViewController(contactViewController, animated: true
  11.     } 
  12.     else { 
  13.         AppDelegate.getAppDelegate().requestForAccess({ (accessGranted) -> Void in 
  14.             if accessGranted { 
  15.                 do { 
  16.                     let contactRefetched = try AppDelegate.getAppDelegate().contactStore.unifiedContactWithIdentifier(selectedContact.identifier, keysToFetch: [CNContactViewController.descriptorForRequiredKeys()]) 
  17.   
  18.                     dispatch_async(dispatch_get_main_queue(), { () -> Void in 
  19.                         let contactViewController = CNContactViewController(forContact: contactRefetched) 
  20.                         contactViewController.contactStore = AppDelegate.getAppDelegate().contactStore 
  21.                         contactViewController.displayedPropertyKeys = keys 
  22.                         self.navigationController?.pushViewController(contactViewController, animated: true
  23.                     }) 
  24.                 } 
  25.                 catch { 
  26.                     print("Unable to refetch the selected contact.", separator: "", terminator: "\n"
  27.                 } 
  28.             } 
  29.         }) 
  30.     } 

在上面的代码段中可以看到我们简单的使用了contacts view controller 实例对象的displayedPropertyKeys属性就指定了想要被展示的属性值。另一个细节值得注意的就是我们利用contactStore属性来向contacts view controller 提供 contact ?store 实例。如果应用中没有一个已存在的CNContactStore实例那么这个操作也不是强制要做的,因为CNContactsViewController会自动创建一个新的。其它的都已经被讨论过了。最后,不要忘记在文件顶端引入下面的框架:

  1. import?ContactsUI 

创建和保存一个新的联系人

现在我们已经看到了关于这个新的联系人框架的很多新特性。但是,还有一个部分至今还没有被讨论到,那就是怎样在代码里创建一个新的联系人信息然后把它储存到数据库中。因此,正如你了解的,在这个部分我们将会覆盖这个方面。我不会花过多精力讲解怎样更新一个已有的联系人记录,因为这个跟我们马上要看到的任务很类似,所以我把这部分留给你去找到这两个任务的不同点。

除了CNContact类展示了一个单个的联系人记录和它的所有的属性外,联系人框架(Contacts framework)提供了另一个类,叫CNMutableContact。正如这个类的名字表达的,它与第一个类很相似;但是,这个类允许重新给一个联系人的属性赋值,创建一个新的或者更新一个已有的联系人记录。真实的保存(更新)操作都被我们熟知的contact store(CNContactStore)类处理,不过这是创建新的联系人的最后一步了。你马上就会看到详细的细节步骤。

总体上说,使用CNMutableContact类给一个联系人记录设置属性值包含了获取属性值时的相反操作。意味着比起给简单属性只需要直接赋值一个值(例如,名字),特殊属性必须进行特殊的操作。例如:

当给一个联系人记录设置一个生日日期时,一个NSDateComponents对象必须被创建好并且恰当的赋值给相应的属性。

当设置一个联系人图像时,一个NSData对象必须要赋值给它。

当设置邮箱地址时,对于每个单独的邮箱地址必须创建一个CNLabeledValue对象,然后所有的这些对象要被组成一个数组赋值给emailAddresses属性。

上面仅仅是举的一些例子。当然还有更多的需要被小心对待的联系人属性,但是不管怎样,正如你要看到的做这些操作都不会很难。

又回到我们的demo应用,这次我们要切换到 CreateContactViewController.swift 文件。在这里面,你会找到一个空的自定义的叫 createContact()的方法。接下来我们要做的事情都写在这里。简单的,我们创建一个新的CNMutableContact的实例,然后我们给所有感兴趣的属性赋值,最后我们利用contact store把这个新的记录存储到数据库中。让我们来看看实现代码:

  1. func createContact() { 
  2.     let newContact = CNMutableContact() 
  3.   
  4.     newContact.givenName = txtFirstname.text! 
  5.     newContact.familyName = txtLastname.text! 
  6.   
  7.     let homeEmail = CNLabeledValue(label: CNLabelHome, value: txtHomeEmail.text!) 
  8.     newContact.emailAddresses = [homeEmail] 
  9.   
  10.     let birthdayComponents = NSCalendar.currentCalendar().components([NSCalendarUnit.Year, NSCalendarUnit.Month, NSCalendarUnit.Day], fromDate: datePicker.date) 
  11.     newContact.birthday = birthdayComponents 
  12.   
  13.     do { 
  14.         let saveRequest = CNSaveRequest() 
  15.         saveRequest.addContact(newContact, toContainerWithIdentifier: nil) 
  16.         try AppDelegate.getAppDelegate().contactStore.executeSaveRequest(saveRequest) 
  17.   
  18.         navigationController?.popViewControllerAnimated(true
  19.     } 
  20.     catch { 
  21.         AppDelegate.getAppDelegate().showMessage("Unable to save the new contact."
  22.     } 

从头开始分析,第一步就是初始化一个在后面一直要被用到的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应用,以任何你想要的方式来改变它扩展它。它始终都有提高的空间。不过千万不要忘记用户隐私设置和必须尊重用户对于是否允许这个应用访问联系人信息的选择。不要错过官方文档,你也会在那里找到很多有趣的东西。我希望你喜欢这个教程并且认为它有用;下次见,希望你过得愉快!

责任编辑:倪明 来源: AppCoda
相关推荐

2015-09-24 11:37:43

2012-03-01 19:44:18

Android

2016-03-18 11:19:57

ios9replaykit入门

2015-07-16 12:59:19

IOS9UIDynamics

2015-10-16 14:27:29

iOS9collectionV特性

2015-11-04 09:54:34

ios9人机界面ui

2015-09-16 09:55:12

ios9学习UIKit Dynam

2011-05-26 14:42:34

Android 手机

2012-02-24 09:25:58

2015-08-24 09:24:21

ios学习contacts fr

2015-08-20 09:00:23

ios9api

2015-07-02 17:32:28

iOS 9苹果

2015-09-25 09:44:24

ios9MapkitTrans

2012-05-02 09:22:50

BlackBerry BlackBerryRIM

2013-08-21 15:47:05

InnoDBMemcachedMemcached插件

2010-01-27 14:08:56

Android查询联系

2014-12-30 11:51:35

ListViewItem View

2015-02-11 15:40:40

XY苹果助手iOS9

2015-05-28 23:29:01

xy

2020-02-02 14:45:55

联系人开源工具
点赞
收藏

51CTO技术栈公众号