89、iOS 联系人与日历功能开发指南

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() {
    // 对象无效,停止使用
    // 可以进行清理操作,如从数据结构中移除
}
  1. 更新 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)
  1. 创建事件并添加邀请信息 :在创建事件时,将选择的联系人信息添加到事件的邀请列表中(假设事件类有相应的邀请属性)。
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)
        }
    }
}
  1. 保存事件 :将包含邀请信息的事件保存到数据库。
try self.database.save(ev, span:.thisEvent, commit:true)

4.4 性能优化建议

在开发过程中,为了提高应用的性能,需要注意以下几点:
| 优化点 | 建议 |
| ---- | ---- |
| EKEventStore 实例 | 尽量只创建一个 EKEventStore 实例,并在应用的生命周期内保持该实例,避免频繁创建和销毁。 |
| 数据查询 | 在查询日历或事件时,使用谓词进行精确查询,避免不必要的全量查询。例如,只查询特定日期范围内的事件。 |
| 内存管理 | 及时释放不再使用的与日历或联系人相关的对象,避免内存泄漏。例如,在视图控制器销毁时,释放相关的选择器和视图控制器实例。 |

5. 总结与展望

5.1 功能总结

通过前面的介绍,我们了解了 iOS 中联系人选择器、 CNContactViewController 以及日历功能的开发方法。可以实现联系人的选择、编辑,日历的创建、事件和提醒事项的管理等功能。同时,也掌握了处理数据库变化、结合联系人与日历功能以及性能优化的方法。

5.2 未来应用拓展

在未来的应用开发中,可以进一步拓展这些功能。例如:
- 社交化功能 :结合社交网络,实现与好友共享日历和事件,增加社交互动性。
- 智能提醒 :利用机器学习算法,根据用户的使用习惯和历史数据,提供更智能的提醒服务。
- 跨平台同步 :实现与其他平台的日历和联系人数据同步,方便用户在不同设备上使用。

总之,iOS 的联系人与日历功能为开发者提供了丰富的接口和工具,通过合理的应用和拓展,可以开发出功能强大、用户体验良好的应用。在实际开发中,要根据具体需求进行灵活运用,并不断优化和改进。

"Mstar Bin Tool"是一款专门针对Mstar系列芯片开发的固件处理软件,主要用于智能电视及相关电子设备的系统维护深度定制。该工具包特别标注了"LETV USB SCRIPT"模块,表明其对乐视品牌设备具有兼容性,能够通过USB通信协议执行固件读写操作。作为一款专业的固件编辑器,它允许技术人员对Mstar芯片的底层二进制文件进行解析、修改重构,从而实现系统功能的调整、性能优化或故障修复。 工具包中的核心组件包括固件编译环境、设备通信脚本、操作界面及技术文档等。其中"letv_usb_script"是一套针对乐视设备的自动化操作程序,可指导用户完成固件烧录全过程。而"mstar_bin"模块则专门处理芯片的二进制数据文件,支持固件版本的升级、降级或个性化定制。工具采用7-Zip压缩格式封装,用户需先使用解压软件提取文件内容。 操作前需确认目标设备采用Mstar芯片架构并具备完好的USB接口。建议预先备份设备原始固件作为恢复保障。通过编辑器修改固件参数时,可调整系统配置、增删功能模块或修复已知缺陷。执行刷机操作时需严格遵循脚本指示的步骤顺序,保持设备供电稳定,避免中断导致硬件损坏。该工具适用于具备嵌入式系统知识的开发人员或高级用户,在进行设备定制化开发、系统调试或维护修复时使用。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值