大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Calendar 和 EventKit
了解如何将 Calendar 与你的 App 进行整合以帮助用户实现更有效的时间管理。探索如何从你的 App 中创建新事件,获取事件并实现虚拟会议扩展。我们将为你介绍针对日历访问级别的更改,以让你的 App 与用户保持联系,同时不侵犯他人日历数据中的隐私。
章节
- 0:41 - Integrate with Calendar
- 1:52 - Frameworks overview
- 4:36 - Adding events
- 4:56 - Adding: EventKitUI
- 8:07 - Adding: Siri Event Suggestions
- 10:59 - Adding: Write-only
- 12:16 - Adding: EventKit
- 14:23 - Full Access
- 18:01 - Virtual conferences
资源
- Accessing Calendar using EventKit and EventKitUI
- EventKit
- Explore the Human Interface Guidelines for privacy
- Siri Event Suggestions
- Universal Links for Developers
相关视频
WWDC23
WWDC20
-
下载
♪ ♪
Adam:大家好 我是 Adam 在本部视频中 我将向你介绍 你的 App 如何使用 Calendar 和 EventKit 帮助用户管理时间 首先 我会介绍一些 将 Calendar 整合到 App 中的方式 并对其中使用的框架进行概述 接着 我会通过一些具体示例 向你展示如何使用 这些框架实现常见功能 例如 添加事件 使用完全访问权限获取事件 以及实现虚拟会议的扩展
人们用日历记录时间、 计划未来 但是 Calendar 不仅仅只是一个 App 通过与 Calendar 整合 App 可以发挥不同的功能 将这些功能汇集起来 便可以创造 更加丰富的 Calendar 体验 一些 App 支持预定、 购票以及安排会面 这些都是通过添加事件来实现 而一些 App 则通过显示事件来实现 例如自定义日历小组件和计划器
还有一些 App 兼具这两种方式 它们通过查看和编辑事件 帮助用户管理日程安排
支持语音或视频通话的 App 也是 Calendar 体验的一部分 虚拟会议扩展不仅可以 提升使用 Calendar App 的体验 同时还为返回你的 App 提供了一种快捷方式
这些功能相互协调配合 为用户带来了时间管理的综合体验 接下来 我将通过具体示例 为你逐一介绍这几个方面
你可以使用两种框架 来实现与 Calendar 的整合 EventKit 框架可以 直接处理日历数据 EventKitUI 是一个 iOS 和 Mac Catalyst 框架 可以提供在 App 中 显示日历 UI 的视图控制器 接下来 我们来逐一了解一下
我们首先从 EventKit 中的 基本类型开始
EKEventStore 是 日历数据的主要联络点 你可以使用事件存储 来请求访问权限并获取或保存数据 并且你的 App 只能 拥有其中一个事件存储 EKEvent 类表示一个特定事件 具有标题、开始日期、 结束日期和地点等属性 日历中的每个事件 都会由 EKCalendar 类表示 Calendars 具有 标题和颜色两个属性 可以为事件进行着色 最后 每个日历账户 都会由一个 EKSource 表示 EKSource 是日历的集合 源对于在 UI 中 对日历进行分组十分有用
EventKit 是与日历数据 交互的基础框架 EventKitUI 构建于 EventKit 之上 以提供一些实用的内置视图 EventKitUI 一共提供了 3 个视图控制器 EKEventEditViewController 可以显示事件编辑器 你可以利用它添加新事件 或更改现有事件 EKEventViewController 可以显示事件详细信息 你可以利用它在你的 App 中 显示现有事件的相关信息 EKCalendarChooser 可以显示日历列表 并支持一种或多种选择 你可以利用它 选择日历添加事件 或选择 App 中 可见的日历
由于 Calendar 包含私人信息 所以系统在未经授权的情况下 禁止 App 读取或写入 Calendar 事件 App 在访问 Calendar 时 共有 3 种访问级别: 无访问权限、只写访问权限 以及完全访问权限 无访问权限的 App 可以使用 EventKitUI 或 Siri Event Suggestions 添加事件 具有只写访问权限的 App 可以 直接使用 EventKit 添加事件 最后 具有完全访问权限的 App 可以获取或更改现有事件 访问现有日历 以及创建新的日历
整合 Calendar 最常见的一种方式是 添加新事件 事件可以通过 以下几种方式添加到日历中 使用 EvenKitUI 或 Siri Event Suggestions 逐个添加事件 或使用 EventKit 直接保存事件 将大部分任务交由 EventKitUI 处理 是将事件添加到日历中 最简单的方式 使用 EKEventEditViewController 可以显示 已填写事件详细信息的编辑器 从而 用户可以 在决定是否保存事件前 选择日历或进行更改 在 iOS 17 中 该 UI 会在单独的进程中进行 这就意味着你无需请求 日历的访问权限
使用 EventKitUI 添加事件 共包含 4 步 首先 创建一个事件存储 接着 创建一个事件 并填写细节信息 然后 创建一个配置好的 视图控制器来编辑事件 最后 呈现视图控制器 接下来 让我们通过代码 更详细地了解一下这个过程 首先 创建一个 eventStore 接着 创建一个事件 并填入细节信息 你在此处设置的细节信息 将会在编辑器 UI 中使用 只要编辑器得到显示 用户便可以进行更改 但理想的情况下 他们只需要 轻点添加按钮来确认 因此填写恰当的细节信息 可以帮助他们节省时间 每个事件都需要有一个标题 该标题可用于很多地方 例如小组件和通知 因此尽量让标题简洁一些
开始日期和结束日期 是这里最重要的属性 使用日期组件创建开始日期
在你确定开始日期后 加上持续时间 便可以估计出结束日期 在计算日期时 请使用 Foundation 中的 Calendar 和 DateComponents 类型 否则 你将会在 夏令时附近得到错误的结果 在这个示例中 我在开始日期上加了两小时
如果事件发生在特定时区 那么你也需要确保进行了设置 默认时区是当前的系统时区
设置定位以让用户知道 事件发生的位置 包含完整的地址 或使用 MapKit 处理器 都可以启用 Apple 地图建议 以及离开时间提醒等功能 最后 添加一些说明 来为用户提供额外的细节信息
在设置完事件属性后 下一步就是创建 EKEventEditViewController 分配事件和事件存储属性 在编辑器中 用户可以 添加事件或取消事件 如果你想知道他们是否添加了事件 使用委托属性 并实现 EKEventEditViewDelegate 协议即可
最后 呈现编辑器 目前 该事件还没有添加到日历中 轻按添加按钮便可将其保存 轻按取消按钮则会 关闭编辑器而不保存任何内容 想要获取有关使用 EventKitUI 添加事件更完整的示例 请查看“使用 EventKit 和 EventKitUI 访问 Calendar” 示例代码中的 “DropInLessons”模块 另一种将事件添加到日历的方法是 使用 Siri Event Suggestions 添加 App 中的预定 Siri Event Suggestions API 是 Intents 框架的一部分 该 API 不需要提示用户 请求访问 Calendar 的权限 也不会在你的 App 中 呈现任何 UI 相反 这些事件会像邀请一样 出现在 Calendar 收件箱中 接着 用户便可以将其 添加到日历中或选择忽视
Siri Event Suggestions 支持餐厅或酒店的预定 航班或租车等旅行预定 以及音乐会或体育赛事等票务活动 如果预定稍后被取消或更改 该事件则会更新
使用 Siri Event Suggestions API 共包含 4 步 首先 创建 INReservation 然后 将预定封装在意图和响应中 接着 创建 INInteraction 最后 将交互提供给系统 我们一起看一下示例代码 预定需要唯一的引用 来让系统对其进行识别 通过使用唯一的 vocabularyIdentifier 和 spokenPhrase 创建 INSpeakableString 的实例 便可创建该引用 这样 用户在与 Siri 对话时 便可以使用词语来引用预定
使用 INDateComponentsRange 设置预订的开始时间和结束时间
接着 使用 CLPlacemark 类型 为事件指定位置
然后 通过创建其中一个 INReservation 子类的实例 将预定的信息进行整合 例如 在餐厅预定中 我们使用 INRestaurantReservation 该初始化定式还有 一些可选参数没有显示 并且每个子类都有自己的特定选项 想要进一步了解 请查看文档
下一步 使用预定引用 创建一个 INGetReservationDetailsIntent 接着 使用预定对象创建一个 INGetReservationDetailsIntentResponse
然后 使用意图和响应 创建一个 INInteraction
最后 调用 interaction 的 donate 对象
本示例只是简单介绍了 Siri Event Suggestions 的功能 想要进一步了解有关创建 Siri Event Suggestions 的信息 请查看 WWDC20 中的 “使用 Siri Event Suggestions 扩展功能”视频 EventKitUI 或 Siri Event Suggestions 可以为添加事件 创造最佳的体验 如果你的 App 只需要 显示自定义编辑 UI 同时添加多个事件 或在不与用户交互的情况下 在日历中添加事件 你可以使用只写访权限
如果你想请求只写访问权限 那么请添加 NSCalendarsWriteOnlyAccess UsageDescription 键值 到 Info.plist 中以解释 App 需要访问权限的原因 接着 该字符串便会 在请求提示中显示 这个是示例 App 的提示内容: “将 RepeatingLessons 保存到你选择的日历中”
只写访问权限同样也有一些局限 第一 用户可能会选择不允许访问 即便获取了访问权限 App 还是无法从日历中读取 任何现有事件 其中包括通过 同一个 App 添加的事件 此外 App 也无法读取日历 List 或创建新的日历 iOS 17 和 macOS Sonoma 现已新增只写访问权限 想要进一步了解有关 该限制对现有 App 的影响 请查看“隐私的新功能”视频
添加具备只写操作权限的新事件 和使用 EventKitUI 添加新事件类似 一开始都是创建事件存储 然后请求只写访问权限 如果获取了该权限 就会创建新事件并填写细节内容 最后 保存该事件 但我们再来仔细地看一下这个过程 首先 创建事件存储 然后 调用 requestWriteOnlyAccessToEvents 方法请求只写访问权限 其返回值表示权限是否得到授予 由于用户可能会拒绝访问请求 所以你需要确保妥善处理这种情况 但如果用户了解 App 需要访问权限的原因 一般情况下他们都会允许该请求 所以你应该在用户首次与需要访问 权限的功能交互时 发出该请求 接下来 创建事件并填写详细信息 这里会出现另外一个重要差异 使用 EventKitUI 时 你填写的详细信息 会显示在编辑器中 没有填写的部分会显示默认值 当你使用 EventKit 直接保存事件时 它不会为你填写任何内容 只有你设置的内容会得到保存 并且 其中一些属性为必填选项 否则保存便会失败
日历便是必填属性之一 你可以使用事件存储的 defaultCalendarForNewEvents 属性 来将配置过的日历 作为设置中的默认值
其他必填属性还包括 标题、开始日期和结束日期 除此之外 剩余属性都是可选择的 但最好还是尽可能多填写
填完详细信息后 使用事件存储中的 save 方法保存该事件
想要查看使用 EventKit 添加事件的完整示例 请查看“使用 EventKit 和 EventKitUI 访问 Calendar” 示例项目中的 “RepeatingLessons”模块
如果你的 App 想要在日历中 添加事件 那么你应该使用 EventKitUI、 Siri Event Suggestions 或只写访问权限 但对于极少数需要 读取日历数据的 App 你便需要完全访问权限
只有 App 的核心功能需要 显示、更新或删除现有事件时 你才需要请求完全访问权限 想要请求完全访问权限 你需要在 Info.plist 中添加 NSCalendarsFullAccess UsageDescription 键值 接着 该字符串 便会在请求提示中显示 由于 Calendar 包含敏感信息 因此你需要在提示完全访问权限时 描述包含多少数据 用户只有在极为信任 App 时 才会允许其读取日历 如果用户不信任你的 App 便可能拒绝该请求 只有当完全访问权限 是你 App 的核心体验 必不可少的一部分 并且用户清楚了解请求原因时 你才可以发出该请求
如果你的 App 确实需要 核心功能的完全访问权限 那么很可能是因为需要获取事件 为了实现这一点 首先要创建事件存储 接着 请求完全访问权限 然后 创建一个判断式 最后 从事件存储中获取事件 我们来看一下这部分的代码 和其他示例一样 我们首先要创建一个事件存储 App 只能有一个事件存储 因此 你可以对其重复利用 接着 调用 requestFullAccessToEvents 方法来请求完全访问权限 这样做会显示提示内容 并返权限是否得到批准 由于用户更可能会 拒绝完全访问请求 所以你需要确保妥善处理这种情况 在你获取了完全访问权限之后 你可以通过调用事件存储中的 predicateForEvents 方法 创建一个判断式 该判断式会描述你想获取的事件 并包含日期范围 和可选择的日历列表 这段代码的日期范围为当前月份 但为了达到最佳效果 你需要尽可能缩小日期范围 如果 calendars 的参数为 nil 其结果将会包括所有日历的事件 最后 将判断式传递到事件存储的 events(matching:) 方法 以获取事件 该方法便会返回一个匹配事件数组 由于该数组中的事件 不一定按照顺序排列 因此你可以根据需要对其进行排列 想要尝试运行获取事件的完整示例 请查看“使用 EventKit 和 EventKitUI 访问 Calendar” 示例项目中的 “MonthlyEvents”模块
如果想要支持 iOS 17 和 macOS Sonoma 之前的版本 你需要执行运行时可用性检查 在 iOS 17 和 macOS Sonoma 及更高版本上 你需要调用新的 requestAccess 方法 而在早期操作系统上 你则需要 调用旧版的 requestAccess 方法
并且 在 iOS 17 或 macOS Sonoma 之前的版本中 你还需要额外使用字符串 添加 NSCalendarsUsageDescription 键值来请求日历访问权限 此外 使用 EventKitUI 的 App 还需要添加 NSContactsUsageDescription 键值 因为 EventKitUI 需要 为 App 请求通讯录访问权限
如果 App 在请求访问权限时 缺少这些字符串 它便会发生崩溃
至此 我们已经介绍了几种 添加事件和获取事件的方法 但处理事件并非 整合日历的唯一方式 如果你的 App 支持 语音或视频通话 那么你还可以使用虚拟会议扩展 让用户将你的通话直接 添加到他们的事件中 此类扩展共有 2 种使用方式 在向事件添加位置时 自定义虚拟会议选项 会显示在位置选择器中 该示例使用了 FaceTime 通话 和 Skype 虚拟会议扩展 提供的选项 轻按其中一个 你便可以 将虚拟会议添加到事件中 接着 包含虚拟会议的事件 就会在事件详细信息中 显示自定义加入选项 创建虚拟会议扩展只需要几步 首先 在 Xcode 中创建 一个新的虚拟会议扩展项目 然后 在扩展协议中 有两种方法可以实现 实现 fetchAvailableRoomTypes 来提供可用的会议室类型 接着 实现 fetchVirtualConference 来为选定的会议室类型 提供虚拟会议对象 我们来看一个示例
首先 在 Xcode 中创建 一个虚拟会议扩展项目 这个新项目会包含 EKVirtualConferenceProvider 的预置子类 第一个重写的方法就是 fetchAvailableRoomTypes 接着 会议室类型就会 显示在位置选择器中
为每种类型选择一个标题 然后 在 UI 中 该标题便 会显示在你 App 图标的左边
此外 你还需为每种会议室类型 选择一个唯一标识符 该标识符用于告知扩展 所选择会议室的类型
接着 使用标题和标识符创建一个 EKVirtualConferenceRoomTypeDescriptor 的实例 如果你的 App 支持多种会议室类型 那么你便需要为每种类型创建实例 最后 返回会议室类型的数组
另外一个需要实现的方法是 fetchVirtualConference 当其中一种会议室类型被选择时 就需要调用该方法 标识符参数会告诉你 所选择的会议室类型
虚拟会议可以具有 一个或多个 URL 描述符 这些描述符会告知日历加入的方式 然后 创建一个 EKVirtualConferenceURLDescriptor 它需要带有 所要打开的 URL 以及可选标题
你需要在 URL 中使用通用链接 以便可以直接打开你的 App 标题有助于区分多个加入选项 但在这里我们无需设置标题 因为我们只有一种加入方式
将所有的额外信息 添加到详细信息字符串中 接着 该文本便会被添加到 事件详细信息 UI 中的 特殊虚拟会议部分中
最后 对这些信息进行整合 创建并返回 EKVirtualConferenceDescriptor 这里的标题有助于 区分多种会议室类型 但由于本示例只包含一种会议室类型 因此将类型设置为 nil
只需要使用这两种方法 你的 App 便可以 作为虚拟会议的选项 显示在日历 App 的位置选择器中
刚刚我们已经介绍了 几种整合日历的方式 那么现在来考虑一下 你的 App 可以如何进行利用 使用 EventKitUI 或 Siri Event Suggestions 你无需请求访问权限便可添加事件 如果你确实需要请求访问权限 那么请仅在必要时 请求所需的最低访问权限 如果你拥有语音或视频通话 App 那么便可以实现虚拟会议扩展 我非常期待看到 你的 App 与日历整合的方式 感谢你的观看 ♪ ♪
-
-
5:49 - Adding an event with EventKitUI
// Create an event store let store = EKEventStore() // Create an event let event = EKEvent(eventStore: store) event.title = "WWDC23 Keynote" let startDateComponents = DateComponents(year: 2023, month: 6, day: 5, hour: 10) let startDate = Calendar.current.date(from: startDateComponents)! event.startDate = startDate event.endDate = Calendar.current.date(byAdding: .hour, value: 2, to: startDate)! event.timeZone = TimeZone(identifier: "America/Los_Angeles") event.location = "1 Apple Park Way, Cupertino, CA, United States" event.notes = "Kick off an exhilarating week of technology and community." // Create a view controller let eventEditViewController = EKEventEditViewController() eventEditViewController.event = event eventEditViewController.eventStore = store eventEditViewController.editViewDelegate = self // Present the view controller present(eventEditViewController, animated: true)
-
9:17 - Siri Event Suggestions
// Create an INReservation let spokenPhrase = “Lunch at Caffè Macs” let reservationReference = INSpeakableString(vocabularyIdentifier: "df9bc3f5", spokenPhrase: spokenPhrase, pronunciationHint: nil) let duration = INDateComponentsRange(start: myEventStart, end: myEventEnd) let location = CLPlacemark(location: myCLLocation, name: "Caffè Macs", postalAddress: myAddress) let reservation = INRestaurantReservation(itemReference: reservationReference, reservationStatus: .confirmed, reservationHolderName: "Jane Appleseed", reservationDuration: duration, restaurantLocation: location) // Create an intent and response let intent = INGetReservationDetailsIntent(reservationContainerReference: reservationReference) let intentResponse = INGetReservationDetailsIntentResponse(code: .success, userActivity: nil) intentResponse.reservations = [reservation] // Create an INInteraction let interaction = INInteraction(intent: intent, response: intentResponse) // Donate the interaction to the system interaction.donate()
-
12:41 - Adding an event with write-only access
// Create an event store let store = EKEventStore() // Request write-only access guard try await store.requestWriteOnlyAccessToEvents() else { return } // Create an event let event = EKEvent(eventStore: store) event.calendar = store.defaultCalendarForNewEvents event.title = "WWDC23 Keynote" event.startDate = myEventStartDate event.endDate = myEventEndDate event.timeZone = TimeZone(identifier: "America/Los_Angeles") event.location = "1 Apple Park Way, Cupertino, CA, United States" event.notes = "Kick off an exhilarating week of technology and community." // Save the event guard try eventStore.save(event, span: .thisEvent) else { return }
-
15:51 - Fetch events
// Create an event store let store = EKEventStore() // Request full access guard try await store.requestFullAccessToEvents() else { return } // Create a predicate guard let interval = Calendar.current.dateInterval(of: .month, for: Date()) else { return } let predicate = store.predicateForEvents(withStart: interval.start, end: interval.end, calendars: nil) // Fetch the events let events = store.events(matching: predicate) let sortedEvents = events.sorted { $0.compareStartDate(with: $1) == .orderedAscending }
-
19:18 - Virtual conference extension
// Create the extension target class VirtualConferenceProvider: EKVirtualConferenceProvider { // Provide the room types override func fetchAvailableRoomTypes() async throws -> [EKVirtualConferenceRoomTypeDescriptor] { let title = "My Room" let identifier = "my_room" let roomType = EKVirtualConferenceRoomTypeDescriptor(title: title, identifier: identifier) return [roomType] } // Provide the virtual conference override func fetchVirtualConference(identifier: EKVirtualConferenceRoomTypeIdentifier) async throws -> EKVirtualConferenceDescriptor { let urlDescriptor = EKVirtualConferenceURLDescriptor(title: nil, url: myURL) let details = "Enter the meeting code 12345 to enter the meeting." return EKVirtualConferenceDescriptor(title: nil, urlDescriptors: [urlDescriptor], conferenceDetails: details) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。