转载

iOS 9 by Tutorials 笔记(十二)

在 iOS 9 之前,开发者只能使用 C API 来访问 iOS 设备上的通讯录,随着 iOS 9 的推出,Apple 彻底废除了之前的做法,介绍了两种全新的面向对象的高级框架来管理用户通讯录( Contacts 和 ContactsUI )

本章将展示如何使用这两个框架:

  1. 使用 ContactsUI 框架显示和选择联系人
  2. 添加联系人到用户的通讯录中
  3. 搜索用户通讯录并使用 NSPredicate 来过滤

Getting started

本章的 Start Demo 也很简单,主界面是一个 tableView ,每行 cell 显示一个联系人信息,包括:联系人头像、名字、邮箱地址

iOS 9 by Tutorials 笔记(十二)

App 初始化的时候会提供几个联系人的信息以供显示,下面来完成我们的第一个任务:使用 ContactsUI 框架来显示联系人的详细细节信息。

App 的主要类:

  • FriendsViewController.swift UITableViewController 类
  • Friend.swift Model 类,代表每个联系人
  • FriendCell.swift 管理着每个 cell 的显示效果

Displaying a contact

第一步为 cell 加一个 Disclosure Indicator ,新增的标识告诉用户可点击进入详情页面

iOS 9 by Tutorials 笔记(十二)

在显示用户详细信息之前,我们需要将 Friend 实例转换成一个 CNContact

Convert friends to CNContacts

Contacts框架将每个联系人看做是 CNContact 类的一个实例,包含了很多联系人属性,如 givenName 、 familyName 、 emailAddresses 、 imageData 等

现在来做转换,在 Friend.swift 中 import Contacts ,添加一个 extension

extension Friend {     var contactValue: CNContact { // 1       let contact = CNMutableContact()     // 2     contact.givenName = firstName     contact.familyName = lastName     // 3     contact.emailAddresses = [       CNLabeledValue(label: CNLabelWork, value: workEmail)     ] // 4     if let profilePicture = profilePicture {       let imageData =         UIImageJPEGRepresentation(profilePicture, 1)       contact.imageData = imageData } // 5     return contact.copy() as! CNContact   } } 

首先创建了一个 CNMutableContact 实例(CNContact 的可变子类),接着更新了相关属性(从 Friend 结构体之前定义的常量中获取相关属性)。 emailAddresses 是一个 CNLabeledValue 对象的数组,意味着每个 email 都对应着一个标签 label ,有许多这样的标记,暂且这里设定为 CNLabelWork 。最后我们返回一个不可变的拷贝对象( CNContact )

CNContact 是线程安全的,而 CNMutableContact 不是

Showing the contact's information

接着实现点击联系人列表进入详情页面,在 FriendsViewController.swift 中导入

import Contacts   import ContactsUI   

添加 UITableViewDelegate 方法:

//MARK: UITableViewDelegate extension FriendsViewController {     override func tableView(tableView: UITableView,     didSelectRowAtIndexPath indexPath: NSIndexPath) {       tableView.deselectRowAtIndexPath(indexPath,         animated: true)       // 1       let friend = friendsList[indexPath.row]       let contact = friend.contactValue       // 2       let contactViewController =         CNContactViewController(forUnknownContact: contact)       contactViewController.navigationItem.title = "Profile"       contactViewController.hidesBottomBarWhenPushed = true       // 3       contactViewController.allowsEditing = false       contactViewController.allowsActions = false       // 4       navigationController?.pushViewController         (contactViewController, animated: true)   } } 

观察注释 2 ,实例化了一个 CNContactViewController ,这是 ContactsUI 框架用来展示联系人信息用的。这里用到了 forUnknownContact 来初始化是因为该联系人并不存在于 iOS 的通讯录中,随后通过 contactViewController 的相关属性对 navigation bar 和 tab bar 做了些配置

运行,选中某个 cell, ContactsUI 框架会展示选中联系人的信息:

iOS 9 by Tutorials 笔记(十二)

如果我们要添加更多的好友,可以使用 ContactsUI 类中的 CNContactPickerViewController 来让用户从联系人中选择添加到 App

Picking your friends

我们在 SB 中给 FriendsViewController 加一个 AddButton ( UIBarButtonItem )

iOS 9 by Tutorials 笔记(十二)

为这个 AddButton 创建一个关联(target-action)的方法

@IBAction func addFriends(sender: UIBarButtonItem) {   let contactPicker = CNContactPickerViewController()   presentViewController(contactPicker, animated: true, completion: nil) } 

现在点击 AddButton 会展示一个 CNContactPickerViewController ,此时如果你选中任意一个联系人,只会将你带到详情页面,并不能将选中的联系人添加回主页面列表

iOS 9 by Tutorials 笔记(十二)

要解决这个问题,需要利用到 CNContactPickerDelegate

Conforming to CNContactPickerDelegate

CNContactPickerDelegate 有五个可选方法,目前我们只对 contactPicker(_:didSelectContacts:) 感兴趣,当你实现了该方法, CNContactPickerViewController 就会知道你想要支持多选中,下面让我们来实现下

extension FriendsViewController: CNContactPickerDelegate {     func contactPicker(picker: CNContactPickerViewController,     didSelectContacts contacts: [CNContact]) {     // TODO   } } 

最后一个参数 contacts ,存储着选中的多个联系人信息( CNContact 数组)。回顾一下, 在 FriendsViewController 中我们使用的数据源是数组: friendsList ( [Friend] )。所以这里转换一下,将 [CNContact] -> [Friend] ,我们将这种转换放到 model 中来实现

为 Friend 再添加一个初始方法,可通过传入一个 CNContact 来初始化一个 Friend

init(contact: CNContact){     firstName = contact.givenName   lastName = contact.familyName   workEmail = contact.emailAddresses.first!.value as! String   if let imageData = contact.imageData{     profilePicture = UIImage(data: imageData)   } else {     profilePicture = nil   } } 

contact.emailAddresses.first! 代表一个 CNLabeledValue 对象,所以通过 .value 来提取具体的值。

有了 [CNContact] -> [Friend] 转换方法,我们来实现这个代理方法:

extension FriendsViewController: CNContactPickerDelegate {     func contactPicker(picker: CNContactPickerViewController,     didSelectContacts contacts: [CNContact]) {     let newFriends = contacts.map { Friend(contact: $0) }     for friend in newFriends {        if !friendsList.contains(friend){          friendsList.append(friend)         }     }     tableView.reloadData()   } } 

将选中的 [CNContact] 转化成 [Friend] ,再填加到数据源数组 friendsList 中

最后别忘了在 addFriends 方法中设置 delegate

contactPicker.delegate = self   

运行,现在可以选中多个联系人了

iOS 9 by Tutorials 笔记(十二)

选择完毕后按 Done,添加了几个朋友回到了主界面

iOS 9 by Tutorials 笔记(十二)

注意这里有个问题!如果你选中的联系人没有 email,那么 App 会崩溃掉,这是因为之前我们在 Friend 的初始化方法 init(contact:) 中对 email 地址使用了强制解包,没有 email 的 contact 就会崩溃。

有没有办法只允许用户选择存在 email 的联系人呢?当然可以,在 presentViewController(_:animated:completion:): 之前先筛选一下子呗:

contactPicker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")   

属性 predicateForEnablingContact 让你决定筛选哪些联系人可以被选定,这里我们限定了 email 不为空

现在运行,单击添加按钮,你会看到 email 不存在的联系人都变灰色了(不可选中状态)

iOS 9 by Tutorials 笔记(十二)

现在你可以很自然地从通讯录中创建好友了

Saving friends to the user's contacts

接下来我们实现:当用户在 table view cell 上向左滑动,会显示一个 Create Contact 操作按钮来将当前对应的联系人添加到系统通讯录中

先来实现 UI 层面上效果,在 FriendsViewController.swift 的 delegate 中添加:

override func tableView(tableView: UITableView,     editActionsForRowAtIndexPath indexPath: NSIndexPath)   -> [UITableViewRowAction]? {   let createContact = UITableViewRowAction(style: .Normal,     title: "Create Contact") { rowAction, indexPath in     tableView.setEditing(false, animated: true)     // TODO: Add the contact   }   createContact.backgroundColor = BlueColor   return [createContact] } 

上面的代码创建了一个名称为 Create Contact 的 row action ,背景是蓝色的

iOS 9 by Tutorials 笔记(十二)

在你访问或修改用户通讯录之前,最重要的事情是记得先向用户申请权限,Contacts 框架里已经内建了权限功能,你不能在没有授权的情况下访问或修改通讯录

之前我们使用 CNContactPickerViewController 时并没有向用户鉴权,是因为使用 CNContactPickerViewController 的时候,你的 App 并不直接参与访问或修改通讯录。

Asking for permission

我们来完善上面的代码,在 TODO 的地方申请通讯录权限:

override func tableView(tableView: UITableView,     editActionsForRowAtIndexPath indexPath: NSIndexPath)   -> [UITableViewRowAction]? {   let createContact = UITableViewRowAction(style: .Normal,     title: "Create Contact") { rowAction, indexPath in     // 将系统默认的左滑删除关掉了     tableView.setEditing(false, animated: true)     // 申请通讯录权限     let contactStore = CNContactStore()     contactStore.requestAccessForEntityType(CNEntityType.Contacts) {       userGrantedAccess, _ in       guard userGrantedAccess else {         self.presentPermissionErrorAlert()         return       }     }   }   createContact.backgroundColor = BlueColor   return [createContact] } 

在上面的代码中,我们首先创建了 CNContactStore 的实例,表示用户的通讯录,接着通过 requestAccessForEntityType(:completion:) 来向用户申请权限,而用户最终的反馈结果将以 completion handler 闭包的形式通过一个 Bool 参数 userGrantedAccess 传回来。最后为了更好的用户体验,当 userGrantedAccess 为 NO 的时候,我们会弹一个 Alert 说明理由,并引导用户去 Setting 里重新分配权限。

关于 presentPermissionErrorAlert :

func presentPermissionErrorAlert() {     dispatch_async(dispatch_get_main_queue()) {     let alert =       UIAlertController(title: "Could Not Save Contact",         message: "How am I supposed to add the contact if " +         "you didn't give me permission?",         preferredStyle: .Alert)      let openSettingsAction = UIAlertAction(title: "Settings",       style: .Default, handler: { alert in         UIApplication.sharedApplication()           .openURL(             NSURL(string: UIApplicationOpenSettingsURLString)!)     })      let dismissAction = UIAlertAction(title: "OK",       style: .Cancel, handler: nil)      alert.addAction(openSettingsAction)     alert.addAction(dismissAction)     self.presentViewController(alert, animated: true,       completion: nil)   } } 

这里我们使用了 dispatch_async(dispatch_get_main_queue()) 是因为 requestAccessForEntityType(:completion:) 的 completion handler 会在后台线程中执行,因此弹窗这种 UI 操作还是要回主线程

弹窗的第一个 UIAlertAction 使用 UIApplicationOpenSettingsURLString key 来打开 Settings

运行一下,左滑 cell 选择 Create Contact 创建联系人,弹出

iOS 9 by Tutorials 笔记(十二)

选择 Don't Allow ,guard 判断并执行一个弹窗操作,引导用户去 Setting 里授权

iOS 9 by Tutorials 笔记(十二)

按下 Setting ,弹窗将会把你呆到 Settings 界面

iOS 9 by Tutorials 笔记(十二)

Saving friends to contacts

下面处理授权通过的情况下如何将好友保存到通讯录

func saveFriendToContacts(friend: Friend) {     // 1   let contact = friend.contactValue.mutableCopy()     as! CNMutableContact   // 2   let saveRequest = CNSaveRequest()   // 3   saveRequest.addContact(contact,     toContainerWithIdentifier: nil)   do {     // 4     let contactStore = CNContactStore()     try contactStore.executeSaveRequest(saveRequest)     // Show Success Alert     dispatch_async(dispatch_get_main_queue()) {       let successAlert = UIAlertController(title: "Contacts Saved",         message: nil, preferredStyle: .Alert)       successAlert.addAction(UIAlertAction(title: "OK",         style: .Cancel, handler: nil))       self.presentViewController(successAlert, animated: true,         completion: nil)       }   } catch {     // Show Failure Alert     dispatch_async(dispatch_get_main_queue()) {       let failureAlert = UIAlertController(         title: "Could Not Save Contact",         message: "An unknown error occurred.",         preferredStyle: .Alert)       failureAlert.addAction(UIAlertAction(title: "OK",         style: .Cancel, handler: nil))       self.presentViewController(failureAlert, animated: true,         completion: nil)     }   }  } 

因为 addContact:toContainerWithIdentifier: 需要一个 CNMutableContact 作为参数,所以第一步先做个转换;第二步创建了 CNSaveRequest 对象,我们利用该对象来传递增加、更新或删除联系人等操作信息给通讯录( CNContactStore );第三步告诉 CNSaveRequest 你想要增加一个联系人到通讯录中;最后执行保存操作

无论 contactStore.executeSaveRequest(saveRequest) 成功还是失败,都会弹窗提醒用户

回到 tableView(_:editActionsForRowAtIndexPath:) 在 guard block 后保存当前索引对应的 friend

let friend = self.friendsList[indexPath.row]   self.saveFriendToContacts(friend)   

重置模拟器运行,现在你可以左划 cell 将当前联系人添加到系统通讯录中了

iOS 9 by Tutorials 笔记(十二)

细心的童鞋或许已经发现:可以重复添加联系人到通讯录中,下面来修正:

Checking for existing contacts

为了去重,我们在保存联系人之前先判断一下,在 saveFriendToContacts(_:): 一开始添加

//1 let contactFormatter = CNContactFormatter()   //2 let contactName = contactFormatter     .stringFromContact(friend.contactValue)! //3 let predicateForMatchingName = CNContact     .predicateForContactsMatchingName(contactName) //4 let matchingContacts = try! CNContactStore()     .unifiedContactsMatchingPredicate(predicateForMatchingName,     keysToFetch: []) //4 guard matchingContacts.isEmpty else {     dispatch_async(dispatch_get_main_queue()) {     let alert = UIAlertController(       title: "Contact Already Exists", message: nil,       preferredStyle: .Alert)     alert.addAction(UIAlertAction(title: "OK", style: .Cancel,       handler: nil))     self.presentViewController(alert, animated: true,       completion: nil)   }   return } 
  1. CNContactFormatter 根据本机环境(通讯录)来定义联系人的样式,有点类似于 NSDateFormatter 做的事情
  2. 使用这种 formatter 来得到一个联系人的姓名
  3. 用上面获得的联系人姓名创建一个谓词
  4. 利用这个谓词来获取所有匹配姓名的联系人,注意这一步返回的是一个 [CNContact] 数组
  5. 判断一下,如果有重复的联系人就给用户弹个警告窗,直接返回了

在 unifiedContactsMatchingPredicate(_:keysToFetch:) 方法中,我们为参数 keysToFetch 传入了一个空数组,因为当时我们并不需要访问或修改获取到的联系人。但是假如要访问获取到的联系人 first name ,你就要添加 CNContactGivenNameKey 到 keysToFetch 数组中去。

-EOF-
正文到此结束
Loading...