前端开发入门到精通的在线学习网站

网站首页 > 资源文章 正文

初窥iOS 9联系人框架

qiguaw 2025-02-08 12:34:22 资源文章 12 ℃ 0 评论

本文由CocoaChina译者WangYue翻译

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是默认被展示的。它展示了先前我提到的所有引入的联系人的信息,并且提供了获取更多联系人信息(右上角按钮)操作,创建一个新联系人(左上角按钮),以及通过点击一行来查看联系人详细信息的方法。

联系人详细信息将会展示在内嵌的联系人view controller 里。正如你将会看到的那样,你可以展示所有的属性或者只选择你感兴趣展示的那部分。

获取联系人信息在后面将会是一个很有趣的部分。我会通过3种不同的处理方式向你展示3种方法。

  1. 第一种,我们将会输入一个联系人名字(或者名字的一部分),然后通过点击键盘上的return按钮,这个应用就会获取匹配输入名字的联系人信息。

  2. 正如你将会在下面看到的截图那样,在屏幕中间有一个picker view。我们将会利用它来找到所有跟在picker里选中的月份所匹配的联系人的生日月份,而且获取操作会在点击Done按钮的时候被触发。

  3. 我们会利用框架提供的默认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属性。在文件的顶部,添加下面的代码:

varcontactStore = CNContactStore

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

import Contacts

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

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

UIApplication.sharedApplication.delegate as! AppDelegate

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

class func getAppDelegate -> AppDelegate {

returnUIApplication.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种值:

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

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

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

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

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

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

func requestForAccess(completionHandler: (accessGranted: Bool) -> Void) {

let authorizationStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts)

switchauthorizationStatus {

case.Authorized:

completionHandler(accessGranted: true)

case.Denied, .NotDetermined:

self.contactStore.requestAccessForEntityType(CNEntityType.Contacts, completionHandler: { (access, accessError) -> Void in

ifaccess {

completionHandler(accessGranted: access)

}

else{

ifauthorizationStatus == 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个重要的参数:

  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, 如果没有它什么都不能做。

import Contacts

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

func textFieldShouldReturn(textField: UITextField) -> Bool {

AppDelegate.getAppDelegate.requestForAccess { (accessGranted) -> Void in

ifaccessGranted {

}

}

returntrue

}

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

func textFieldShouldReturn(textField: UITextField) -> Bool {

AppDelegate.getAppDelegate.requestForAccess { (accessGranted) -> Void in

ifaccessGranted {

let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!)

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

varcontacts = [CNContact]

varmessage: String!

}

}

returntrue

}

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

func textFieldShouldReturn(textField: UITextField) -> Bool {

AppDelegate.getAppDelegate.requestForAccess { (accessGranted) -> Void in

ifaccessGranted {

let predicate = CNContact.predicateForContactsMatchingName(self.txtLastName.text!)

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

varcontacts = [CNContact]

varmessage: String!

let contactsStore = AppDelegate.getAppDelegate.contactStore

do{

contacts = trycontactsStore.unifiedContactsMatchingPredicate(predicate, keysToFetch: keys)

ifcontacts.count == 0 {

message = "No contacts were found matching the given name."

}

}

catch{

message = "Unable to fetch contacts."

}

ifmessage != nil {

dispatch_async(dispatch_get_main_queue, { -> Void in

AppDelegate.getAppDelegate.showMessage(message)

})

}

else{

}

}

}

returntrue

}

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

展示获取到的联系人

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


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

protocol AddContactViewControllerDelegate {

func didFetchContacts(contacts: [CNContact])

}

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

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

vardelegate: AddContactViewControllerDelegate!

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

func textFieldShouldReturn(textField: UITextField) -> Bool {

AppDelegate.getAppDelegate.requestForAccess { (accessGranted) -> Void in

ifaccessGranted {

...

ifmessage != nil {

...

}

else{

dispatch_async(dispatch_get_main_queue, { -> Void in

self.delegate.didFetchContacts(contacts)

self.navigationController?.popViewControllerAnimated(true)

})

}

}

}

returntrue

}

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

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

import Contacts

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

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, AddContactViewControllerDelegate

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

varcontacts = [CNContact]

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

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

returncontacts.count

}

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

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

iflet identifier = segue.identifier {

ifidentifier == "idSegueAddContact"{

let addContactViewController = segue.destinationViewController as! AddContactViewController

addContactViewController.delegate = self

}

}

}

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

func didFetchContacts(contacts: [CNContact]) {

forcontact incontacts {

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.

iflet birthday = currentContact.birthday {

cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)"

}

else{

cell.lblBirthday.text = "Not available birthday data"

}

// Set the contact image.

iflet imageData = currentContact.imageData {

cell.imgContactImage.image = UIImage(data: imageData)

}

// Set the contact's home email address.

varhomeEmailAddress: String!

foremailAddress incurrentContact.emailAddresses {

ifemailAddress.label == CNLabelHome {

homeEmailAddress = emailAddress.value as! String

break

}

}

ifhomeEmailAddress != nil {

cell.lblEmail.text = homeEmailAddress

}

else{

cell.lblEmail.text = "Not available home email"

}

returncell

}

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

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

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

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

重新获取联系人

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

ifcurrentContact.isKeyAvailable(CNContactBirthdayKey) {

...

}

ifcurrentContact.isKeyAvailable(CNContactImageDataKey) {

...

}

ifcurrentContact.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.

iflet birthday = currentContact.birthday {

cell.lblBirthday.text = "\(birthday.year)-\(birthday.month)-\(birthday.day)"

}

else{

cell.lblBirthday.text = "Not available birthday data"

}

// Set the contact image.

iflet imageData = currentContact.imageData {

cell.imgContactImage.image = UIImage(data: imageData)

}

// Set the contact's work email address.

varhomeEmailAddress: String!

foremailAddress incurrentContact.emailAddresses {

ifemailAddress.label == CNLabelHome {

homeEmailAddress = emailAddress.value as! String

break

}

}

ifhomeEmailAddress != nil {

cell.lblEmail.text = homeEmailAddress

}

else{

cell.lblEmail.text = "Not available home email"

}

}

returncell

}

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

现在我们来看新的方法:

func refetchContact(contact contact: CNContact, atIndexPath indexPath: NSIndexPath) {

AppDelegate.getAppDelegate.requestForAccess { (accessGranted) -> Void in

ifaccessGranted {

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

do{

let contactRefetched = tryAppDelegate.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。万一有某些联系人信息没获取到时,你最好始终做一下重新获取联系人信息的操作,这样就能确保你的应用不会给用户展示任何“惊喜”。

微信号:CocoaChinabbs

(长按上图,可自动识别二维码)

--------------------------------------

商务合作QQ:645047738

投稿邮箱:support@cocoachina.com

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表