iOS 联系人与日历功能开发指南
1. 联系人选择器配置
在开发过程中,联系人选择器是一个常用的组件。可以通过以下代码配置联系人选择器,使其仅显示有电子邮件地址的联系人,并在用户选择时仅展示电子邮件地址:
picker.displayedPropertyKeys = [CNContactEmailAddressesKey]
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
上述代码的含义如下:
-
displayedPropertyKeys
:指定选择器中显示的联系人属性,这里仅显示电子邮件地址。
-
predicateForEnablingContact
:设置一个谓词,用于筛选出有电子邮件地址的联系人。
1.1 委托方法实现
我们只需实现委托方法的第二种形式(参数为
CNContactProperty
),代码的实际作用是:“仅启用有电子邮件地址的联系人。当用户点击启用的联系人时,显示详细信息。在详细信息视图中,仅显示电子邮件地址。当用户点击电子邮件地址时,将其报告给委托方法并关闭选择器。”
1.2 多选功能
若要启用多选功能,需实现另外一对委托方法:
contactPicker(_:didSelect:)
contactPicker(_:didSelectContactProperties:)
这会在界面中显示一个“完成”按钮,用户点击该按钮时会调用委托方法。不过,若多选属性选择界面配置不当,可能会导致界面笨拙、混乱,甚至使应用陷入停滞,因此在决定使用前需谨慎测试。
2. CNContactViewController 介绍
CNContactViewController
是一个
UIViewController
,根据实例化方式的不同,有三种类型:
| 类型 | 实例化方法 | 说明 |
| ---- | ---- | ---- |
| 现有联系人 |
init(for:)
| 初始显示一个联系人,可选择显示二级编辑界面 |
| 新联系人 |
init(forNewContact:)
| 仅包含编辑界面 |
| 未知联系人 |
init(forUnknownContact:)
| 初始显示一个联系人,可选择显示二级编辑界面 |
2.1 配置属性
对于第一种和第三种类型,可以通过以下属性配置联系人的初始显示:
-
allowsActions
:若为
true
,界面中会显示额外的按钮,如“分享联系人”、“添加到收藏夹”和“分享我的位置”等,具体显示哪些按钮取决于所显示的信息类别。
-
displayedPropertyKeys
:限制该联系人显示的属性。
-
message
:显示在联系人姓名下方的字符串。
2.2 委托方法
CNContactViewController
有两个委托方法(
CNContactViewControllerDelegate
):
-
contactViewController(_:shouldPerformDefaultActionFor:)
:用于第一种和第三种类型,在联系人的初始显示中使用。类似于选择器的
predicateForSelectionOfProperty
的实时版本,但含义相反:返回
true
表示点击的属性应触发邮件应用、地图应用或其他合适的操作。这包括界面顶部的消息和邮件按钮。你会获得
CNContactProperty
,因此知道点击的内容,若返回
false
则可自行采取行动。
-
contactViewController(_:didCompleteWith:)
:三种类型都会使用。当用户关闭编辑界面时调用。若用户在编辑界面中点击“完成”,你将收到已保存到数据库中的编辑后的联系人;若用户取消编辑界面,若调用此委托方法,收到的联系人将为
nil
。
2.3 显示现有联系人
要在
CNContactViewController
中显示现有联系人,需使用已填充了所有显示所需信息的
CNContact
调用
init(for:)
。为实现此目的,
CNContactViewController
提供了一个类方法
descriptorForRequiredKeys
,在从存储中获取联系人时,需要调用该方法设置键:
let pred = CNContact.predicateForContacts(matchingName: "Snidely")
let keys = CNContactViewController.descriptorForRequiredKeys()
let snides = try CNContactStore().unifiedContacts(matching: pred, keysToFetch: [keys])
guard let snide = snides.first else {
print("no snidely")
return
}
现在我们有了一个信息充足的联系人
snide
,可以在后续调用
CNContactViewController
的
init(for:)
时使用它。需要注意的是,将信息不足的联系人传递给
CNContactViewController
的
init(for:)
会导致应用崩溃。
实例化
CNContactViewController
后,设置其委托(
CNContactViewControllerDelegate
),并将视图控制器推送到现有的
UINavigationController
堆栈中:
let vc = CNContactViewController(for:snide)
vc.delegate = self
vc.message = "Nyah ah ahhh"
self.navigationController?.pushViewController(vc, animated: true)
2.4 创建新联系人
若要使用
CNContactViewController
让用户创建新联系人,可使用
init(forNewContact:)
进行实例化。参数可以为
nil
,也可以是已创建并部分填充的
CNMutableContact
,但这些属性仅为建议,因为用户将看到联系人编辑界面并可更改你设置的任何内容。
let con = CNMutableContact()
con.givenName = "Dudley"
con.familyName = "Doright"
let npvc = CNContactViewController(forNewContact: con)
npvc.delegate = self
self.present(UINavigationController(rootViewController: npvc), animated:true)
在实现
contactViewController(_:didCompleteWith:)
时,必须自行关闭所呈现的导航控制器。
2.5 编辑未知联系人
若要使用
CNContactViewController
让用户编辑未知联系人,可使用
init(forUnknownContact:)
进行实例化。必须提供一个
CNContact
参数,可使用
CNMutableContact
从头创建。同时,必须将视图控制器的
contactStore
设置为
CNContactStore
实例,否则视图控制器将无法使用。然后设置委托并将视图控制器推送到现有的导航控制器中:
let con = CNMutableContact()
con.givenName = "Johnny"
con.familyName = "Appleseed"
con.phoneNumbers.append(CNLabeledValue(label: "woods", value: CNPhoneNumber(stringValue: "555-123-4567")))
let unkvc = CNContactViewController(forUnknownContact: con)
unkvc.message = "He knows his trees"
unkvc.contactStore = CNContactStore()
unkvc.delegate = self
unkvc.allowsActions = false
self.navigationController?.pushViewController(unkvc, animated: true)
界面中包含以下两个按钮(还有其他按钮):
-
创建新联系人
:显示编辑界面,包含“取消”和“完成”按钮。
-
添加到现有联系人
:显示联系人选择器,用户可点击“取消”或点击现有联系人。若用户点击现有联系人,则显示该联系人的编辑界面,将部分联系人的字段合并进来,同时包含“取消”和“更新”按钮。
若框架认为部分联系人与现有联系人相同,将显示第三个按钮,明确提供更新该特定联系人的选项,结果就像用户点击了“添加到现有联系人”并选择了该现有联系人一样:显示该联系人的编辑界面,将部分联系人的字段合并进来,同时包含“取消”和“更新”按钮。在编辑界面中,若用户点击“取消”,则不会收到任何通知,
contactViewController(_:didCompleteWith:)
甚至不会被调用。
3. 日历功能开发
3.1 日历数据库访问
用户的日历信息构成了一个日历事件数据库,该数据库还包括提醒事项。用户可以通过“日历”应用与日历事件进行交互,通过“提醒事项”应用与提醒事项进行交互。我们的代码可以通过
EventKit
框架访问该数据库,同时需要导入
EventKitUI
框架以提供让用户在应用内与日历交互的界面。
数据库通过
EKEventStore
类的实例进行访问。该实例获取成本较高,但维护成本较低,因此通常的策略是实例化并保留一个
EKEventStore
实例:
let database = EKEventStore()
3.2 数据库访问授权
访问数据库需要用户授权,可使用
EKEventStore
类进行此操作。虽然只有一个数据库,但访问日历事件和访问提醒事项被视为两种不同的访问形式,需要分别进行授权。
- 要了解当前的授权状态,可使用类方法
authorizationStatus(for:)
,传入
EKEntityType
,可以是
.event
(访问日历事件)或
.reminder
(访问提醒事项)。
- 若状态为
.notDetermined
,要请求系统显示授权请求警报,可调用实例方法
requestAccess(to:completion:)
。同时,
Info.plist
中必须包含一些文本,用于系统授权请求警报解释应用访问的原因,相关键为“Privacy — Calendars Usage Description”(
NSCalendarsUsageDescription
)或“Privacy — Reminders Usage Description”(
NSRemindersUsageDescription
)。
3.3 日历数据库内容
从
EKEventStore
实例开始,可以获取两种类型的对象:日历或日历项。
3.3.1 日历
日历表示一个命名(标题)的日历项集合,即事件或提醒事项,是
EKCalendar
的实例。但
EKCalendar
实例并不包含或链接到其日历项,要获取和创建日历项,需直接与
EKEventStore
本身进行交互。日历的
allowedEntityTypes
尽管是复数形式,但可能只返回一种实体类型,无法创建允许两种类型的日历。
日历有多种类型(
type
,
EKCalendarType
),反映了其来源的性质:
| 类型 | 说明 |
| ---- | ---- |
|
.local
| 用户在本地创建和维护的日历 |
|
.calDAV
、
.exchange
| 存储在网络上的远程日历 |
|
.birthday
| 从地址簿信息自动生成的生日日历 |
日历的类型由其源(
source
,
EKSource
)补充和包含,源的
sourceType
(
EKSourceType
)可以是
.local
、
.exchange
、
.calDAV
(包括 iCloud)等。源也有标题和唯一标识符(
sourceIdentifier
),可以获取
EKEventStore
已知的所有源的数组,也可以通过标识符指定源,通常会仅使用源而忽略日历的类型属性。
请求日历有三种方式:
| 方式 | 方法 | 说明 |
| ---- | ---- | ---- |
| 所有日历 |
calendars(for:)
| 获取允许特定日历项类型(
.event
或
.reminder
)的所有日历,可以向
EKEventStore
或
EKSource
发送此消息 |
| 特定日历 |
calendar(withIdentifier:)
| 通过先前获取的日历标识符从
EKEventStore
中获取单个日历 |
| 默认日历 |
defaultCalendarForNewEvents
或
defaultCalendarForNewReminders
| 获取特定日历项类型的默认日历,特别适用于创建新日历项的情况 |
此外,还可以通过初始化方法
init(for:eventStore:)
创建日历,并指定日历所属的源。
根据源的不同,日历的可修改方式也不同。日历的
isSubscribed
可能为
true
,若
isImmutable
为
true
,则无法删除日历或更改其属性,但
allowsContentModifications
可能仍为
true
,此时可以添加、删除和更改其事件。
3.3.2 日历项
日历项(
EKCalendarItem
)可以是日历事件(
EKEvent
)或提醒事项(
EKReminder
),可以将其视为描述某事发生时间的备忘录。如前所述,不能从日历中获取日历项,而是日历项有一个日历,需从整个
EKEventStore
中获取。获取日历项主要有两种方式:
-
通过谓词
:根据谓词(
NSPredicate
)获取所有事件或提醒事项:
-
events(matching:)
-
enumerateEvents(matching:using:)
-
fetchReminders(matching:completion:)
EKEventStore
以
predicateFor
开头的方法提供所需的谓词。
-
通过标识符
:通过先前获取的日历项标识符,使用
calendarItem(withIdentifier:)
获取单个日历项。
3.4 日历数据库更改
数据库的更改可以是原子性的,实现此功能有两个方面:
-
EKEventStore
用于保存和删除日历项及日历的方法有一个
commit:
参数。若传入
false
,则会将所命令的更改进行批量处理,但不执行,稍后可以调用
commit
(若改变主意,也可调用
reset
)。若传入
false
且后续未调用
commit
,则更改将不会生效。
- 抽象类
EKObject
作为所有其他持久对象类型(如
EKCalendar
、
EKCalendarItem
、
EKSource
等)的超类,为这些类提供了
refresh
、
rollback
和
reset
方法,以及只读属性
isNew
和
hasChanges
。
应用运行时,数据库可能会发生更改(用户可能进行同步操作,或使用“日历”应用进行编辑),这可能导致我们的信息过时。可以注册单个
EKEventStore
通知
.EKEventStoreChanged
,若收到该通知,应假设所持有的任何与日历相关的实例都无效。不过,每个与日历相关的实例都可以使用
refresh
方法进行刷新,需要注意的是,
refresh
方法返回一个布尔值,若返回
false
,则表示该对象确实无效,应停止使用它(可能已从数据库中删除)。
3.5 创建日历、事件和提醒事项
3.5.1 创建日历
下面是创建事件日历的示例,我们选择
.local
源类型,意味着日历将在设备本地创建:
let locals = self.database.sources.filter {$0.sourceType == .local}
guard let src = locals.first else {
print("failed to find local source")
return
}
let cal = EKCalendar(for:.event, eventStore:self.database)
cal.source = src
cal.title = "CoolCal"
try self.database.saveCalendar(cal, commit:true)
需要注意的是,在日历订阅到远程源的设备上,
.local
日历不可访问。为测试示例,可能需要暂时关闭日历应用的 iCloud 功能。
3.5.2 创建事件
EKEvent
是
EKCalendarItem
的子类,继承了一些重要属性。若使用过 iOS 或 macOS 上的“日历”应用,就会对
EKEvent
的配置有一定了解。它有标题和可选的备注,与一个日历相关联,还可以有一个或多个警报和一个或多个重复规则。
EKEvent
自身添加了重要的
startDate
和
endDate
属性,这些是
Date
类型,包含日期和时间。若事件的
isAllDay
属性为
true
,则日期的时间部分将被忽略,事件与一整天或连续的几天相关联;若
isAllDay
属性为
false
,则日期的时间部分很重要,事件通常由同一天的两个时间点界定。
创建事件虽然简单但繁琐,必须提供
startDate
和
endDate
。使用
DateComponents
是构造日期和进行日期计算的最简单方法,以下是创建事件并将其添加到新日历的示例:
func calendar(name:String ) -> EKCalendar? {
let cals = self.database.calendars(for:.event)
return cals.filter {$0.title == name}.first
}
guard let cal = self.calendar(name:"CoolCal") else {
print("failed to find calendar")
return
}
// 形成开始和结束日期
let greg = Calendar(identifier:.gregorian)
var comp = DateComponents(year:2018, month:8, day:10, hour:15)
let d1 = greg.date(from:comp)!
comp.hour = comp.hour! + 1
let d2 = greg.date(from:comp)!
// 形成事件
let ev = EKEvent(eventStore:self.database)
ev.title = "Take a nap"
ev.notes = "You deserve it!"
ev.calendar = cal
(ev.startDate, ev.endDate) = (d1,d2)
// 保存事件
try self.database.save(ev, span:.thisEvent, commit:true)
3.5.3 添加警报
警报是
EKAlarm
的实例,是一个非常简单的类,可以设置为在绝对日期或相对于事件时间的相对偏移量触发。在 iOS 设备上,警报通过本地通知触发:
let alarm = EKAlarm(relativeOffset:-3600) // 提前一小时
ev.addAlarm(alarm)
3.5.4 重复规则
重复规则由
EKRecurrenceRule
表示,日历项可以有多个重复规则,可通过其
recurrenceRules
属性以及
addRecurrenceRule(_:)
和
removeRecurrenceRule(_:)
方法进行操作。一个简单的
EKRecurrenceRule
由三个属性描述:
-
频率
:按天、周、月或年。
-
间隔
:微调频率中的“按”概念,值为 1 表示“每”,值为 2 表示“每隔”,依此类推。
-
结束
:可选,因为事件可能永远重复。它是一个
EKRecurrenceEnd
实例,描述事件重复的限制,可以是结束日期或最大重复次数。
更复杂的
EKRecurrenceRule
可以通过其初始化方法进行描述:
init(recurrenceWith type: EKRecurrenceFrequency,
interval: Int,
daysOfTheWeek: [EKRecurrenceDayOfWeek]?,
daysOfTheMonth: [NSNumber]?,
monthsOfTheYear: [NSNumber]?,
weeksOfTheYear: [NSNumber]?,
daysOfTheYear: [NSNumber]?,
setPositions: [NSNumber]?,
end: EKRecurrenceEnd?)
EKRecurrenceDayOfWeek
类允许指定周数和天数,例如“每月的第四个星期四”。许多数值可以为负数,表示从最后一个开始倒数。所有数字都是从 1 开始计数,而不是从 0 开始。
setPositions:
参数是一个数字数组,用于根据间隔过滤其他规范定义的重复事件,例如,若
daysOfTheWeek
是星期日,-1 表示最后一个星期日。
EKRecurrenceRule
旨在体现 iCalendar 标准规范(http://datatracker.ietf.org/doc/rfc5545)中的
RRULE
事件组件,实际上,文档会说明每个
EKRecurrenceRule
属性与
RRULE
属性的对应关系,若记录一个
EKRecurrenceRule
,显示的是底层的
RRULE
。
RRULE
可以描述一些非常复杂的重复规则。
综上所述,通过上述方法和代码示例,我们可以在 iOS 应用中实现联系人选择、编辑以及日历事件和提醒事项的创建、管理等功能。在实际开发中,需要根据具体需求进行适当的调整和扩展。
4. 日历功能的进一步应用与注意事项
4.1 日历与事件的关联及操作流程
在实际应用中,日历和事件的关联操作有一定的流程。以下是创建事件并关联到特定日历的 mermaid 流程图:
graph LR
A[获取 EKEventStore 实例] --> B[检查授权状态]
B -- 未授权 --> C[请求授权]
B -- 已授权 --> D[查找或创建日历]
C --> D
D --> E[创建 EKEvent 实例]
E --> F[配置事件属性(标题、日期等)]
F --> G[关联事件到日历]
G --> H[保存事件到数据库]
4.2 处理日历数据库变化的策略
当接收到
.EKEventStoreChanged
通知时,需要有一套处理策略来确保应用数据的准确性。以下是处理步骤:
1.
收到通知
:注册
EKEventStore
的
.EKEventStoreChanged
通知,当应用接收到该通知时,触发处理逻辑。
2.
检查对象有效性
:遍历所有与日历相关的实例,调用
refresh
方法。
if let calendarItem = someCalendarItem, !calendarItem.refresh() {
// 对象无效,停止使用
// 可以进行清理操作,如从数据结构中移除
}
- 更新 UI :若 UI 显示了日历或事件信息,根据刷新后的对象更新 UI 显示。
4.3 日历与联系人功能的结合应用
在某些场景下,可能需要将日历功能与联系人功能结合使用。例如,在创建事件时,邀请联系人参加。以下是实现此功能的大致步骤:
1.
选择联系人
:使用联系人选择器选择要邀请的联系人。
let picker = CNContactPickerViewController()
picker.displayedPropertyKeys = [CNContactEmailAddressesKey]
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
picker.delegate = self
self.present(picker, animated: true)
- 创建事件并添加邀请信息 :在创建事件时,将选择的联系人信息添加到事件的邀请列表中(假设事件类有相应的邀请属性)。
let ev = EKEvent(eventStore:self.database)
// 假设 ev 有 invitees 属性
if let selectedContacts = selectedContactsArray {
for contact in selectedContacts {
if let email = contact.emailAddresses.first?.value.stringValue {
ev.invitees?.append(email)
}
}
}
- 保存事件 :将包含邀请信息的事件保存到数据库。
try self.database.save(ev, span:.thisEvent, commit:true)
4.4 性能优化建议
在开发过程中,为了提高应用的性能,需要注意以下几点:
| 优化点 | 建议 |
| ---- | ---- |
| EKEventStore 实例 | 尽量只创建一个
EKEventStore
实例,并在应用的生命周期内保持该实例,避免频繁创建和销毁。 |
| 数据查询 | 在查询日历或事件时,使用谓词进行精确查询,避免不必要的全量查询。例如,只查询特定日期范围内的事件。 |
| 内存管理 | 及时释放不再使用的与日历或联系人相关的对象,避免内存泄漏。例如,在视图控制器销毁时,释放相关的选择器和视图控制器实例。 |
5. 总结与展望
5.1 功能总结
通过前面的介绍,我们了解了 iOS 中联系人选择器、
CNContactViewController
以及日历功能的开发方法。可以实现联系人的选择、编辑,日历的创建、事件和提醒事项的管理等功能。同时,也掌握了处理数据库变化、结合联系人与日历功能以及性能优化的方法。
5.2 未来应用拓展
在未来的应用开发中,可以进一步拓展这些功能。例如:
-
社交化功能
:结合社交网络,实现与好友共享日历和事件,增加社交互动性。
-
智能提醒
:利用机器学习算法,根据用户的使用习惯和历史数据,提供更智能的提醒服务。
-
跨平台同步
:实现与其他平台的日历和联系人数据同步,方便用户在不同设备上使用。
总之,iOS 的联系人与日历功能为开发者提供了丰富的接口和工具,通过合理的应用和拓展,可以开发出功能强大、用户体验良好的应用。在实际开发中,要根据具体需求进行灵活运用,并不断优化和改进。
超级会员免费看
47

被折叠的 条评论
为什么被折叠?



