在 iOS 9 之前,开发者只能使用 C API 来访问 iOS 设备上的通讯录,随着 iOS 9 的推出,Apple 彻底废除了之前的做法,介绍了两种全新的面向对象的高级框架来管理用户通讯录( Contacts 和 ContactsUI )
本章将展示如何使用这两个框架:
ContactsUI
框架显示和选择联系人 NSPredicate
来过滤 本章的 Start Demo 也很简单,主界面是一个 tableView
,每行 cell 显示一个联系人信息,包括:联系人头像、名字、邮箱地址
App 初始化的时候会提供几个联系人的信息以供显示,下面来完成我们的第一个任务:使用 ContactsUI
框架来显示联系人的详细细节信息。
App 的主要类:
UITableViewController
类 第一步为 cell 加一个 Disclosure Indicator
,新增的标识告诉用户可点击进入详情页面
在显示用户详细信息之前,我们需要将 Friend
实例转换成一个 CNContact
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
不是
接着实现点击联系人列表进入详情页面,在 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
框架会展示选中联系人的信息:
如果我们要添加更多的好友,可以使用 ContactsUI
类中的 CNContactPickerViewController
来让用户从联系人中选择添加到 App
我们在 SB 中给 FriendsViewController
加一个 AddButton ( UIBarButtonItem
)
为这个 AddButton 创建一个关联(target-action)的方法
@IBAction func addFriends(sender: UIBarButtonItem) { let contactPicker = CNContactPickerViewController() presentViewController(contactPicker, animated: true, completion: nil) }
现在点击 AddButton 会展示一个 CNContactPickerViewController
,此时如果你选中任意一个联系人,只会将你带到详情页面,并不能将选中的联系人添加回主页面列表
要解决这个问题,需要利用到 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
运行,现在可以选中多个联系人了
选择完毕后按 Done,添加了几个朋友回到了主界面
注意这里有个问题!如果你选中的联系人没有 email,那么 App 会崩溃掉,这是因为之前我们在 Friend 的初始化方法 init(contact:)
中对 email 地址使用了强制解包,没有 email 的 contact 就会崩溃。
有没有办法只允许用户选择存在 email 的联系人呢?当然可以,在 presentViewController(_:animated:completion:):
之前先筛选一下子呗:
contactPicker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
属性 predicateForEnablingContact
让你决定筛选哪些联系人可以被选定,这里我们限定了 email 不为空
现在运行,单击添加按钮,你会看到 email 不存在的联系人都变灰色了(不可选中状态)
现在你可以很自然地从通讯录中创建好友了
接下来我们实现:当用户在 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 ,背景是蓝色的
在你访问或修改用户通讯录之前,最重要的事情是记得先向用户申请权限,Contacts 框架里已经内建了权限功能,你不能在没有授权的情况下访问或修改通讯录
之前我们使用 CNContactPickerViewController 时并没有向用户鉴权,是因为使用 CNContactPickerViewController 的时候,你的 App 并不直接参与访问或修改通讯录。
我们来完善上面的代码,在 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 创建联系人,弹出
选择 Don't Allow ,guard 判断并执行一个弹窗操作,引导用户去 Setting 里授权
按下 Setting ,弹窗将会把你呆到 Settings
界面
下面处理授权通过的情况下如何将好友保存到通讯录
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 将当前联系人添加到系统通讯录中了
细心的童鞋或许已经发现:可以重复添加联系人到通讯录中,下面来修正:
为了去重,我们在保存联系人之前先判断一下,在 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 }
CNContactFormatter
根据本机环境(通讯录)来定义联系人的样式,有点类似于 NSDateFormatter
做的事情 [CNContact]
数组 在 unifiedContactsMatchingPredicate(_:keysToFetch:)
方法中,我们为参数 keysToFetch
传入了一个空数组,因为当时我们并不需要访问或修改获取到的联系人。但是假如要访问获取到的联系人 first name ,你就要添加 CNContactGivenNameKey
到 keysToFetch
数组中去。