Highlight
iOS 17 的
EKEventEditViewController运行在独立进程,无需请求日历权限即可添加事件;新增 write-only 权限让 App 只写不读;虚拟会议扩展让音视频通话 App 直接出现在日历的位置选择器中。
核心内容
日历集成的三种角色
App 与日历集成有三种常见角色:
- 添加事件:预订、购票、约见面,把确认信息写进日历
- 展示事件:自定义日历小组件、日程规划器
- 双向管理:查看和编辑现有事件,创建新日历
不同角色对应不同的权限需求和实现方式。
三种权限级别
iOS 17 把日历权限分成三级:
- 无权限:用
EventKitUI或 Siri Event Suggestions 添加事件,不需要任何权限 - Write-only:直接写事件到日历,但不能读现有事件
- Full access:读取、修改、删除现有事件,创建新日历
权限请求应该遵循最小必要原则。大部分只需要添加事件的 App,用 EventKitUI 就够了,完全不需要弹权限框。
EventKitUI:零权限添加事件
EKEventEditViewController 在 iOS 17 中运行在独立进程。你创建事件、填好属性、弹出编辑器,用户确认后事件直接保存到日历。整个过程你的 App 不接触任何日历数据,所以不需要请求权限。
这是最简单、最安全的集成方式。用户可以在编辑器里选择日历、修改时间,有完全的控制权。
Siri Event Suggestions:轻量添加
适合餐厅预订、机票、演出票等场景。创建 INReservation 对象,包装成 INInteraction,donate 给系统。事件会出现在日历的收件箱里,用户可以一键添加或忽略。
不需要弹权限框,不显示任何 UI,适合后台自动推荐场景。
Write-only Access:自定义 UI 场景
如果你的 App 需要自定义编辑界面、批量添加事件、或静默添加事件,可以申请 write-only 权限。iOS 17 新增了 requestWriteOnlyAccessToEvents() 方法。
限制:不能读现有事件(包括自己添加的),不能读日历列表,不能创建新日历。
Full Access:谨慎使用
只有核心功能需要显示、更新或删除现有事件时才申请。需要在 Info.plist 中配置 NSCalendarsFullAccessUsageDescription。
获取 full access 后,用 predicateForEvents 创建查询条件,events(matching:) 获取结果。注意返回的数组无序,需要自行排序。
虚拟会议扩展
音视频通话 App 可以实现 EKVirtualConferenceProvider,让日历 App 在添加位置时直接展示你的会议选项。用户选择后,日历事件会显示你的品牌图标和加入按钮,点击直接跳转回你的 App。
只需实现两个方法:fetchAvailableRoomTypes() 提供会议室类型,fetchVirtualConference() 根据选择的类型返回会议链接和详情。
详细内容
用 EventKitUI 添加事件
(05:49)
import EventKitUI
// 创建 event store
let store = EKEventStore()
// 创建事件并填写详情
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."
// 创建编辑器视图控制器
let eventEditViewController = EKEventEditViewController()
eventEditViewController.event = event
eventEditViewController.eventStore = store
eventEditViewController.editViewDelegate = self
// 弹出编辑器
present(eventEditViewController, animated: true)
关键点:
EKEventEditViewController在 iOS 17 运行在独立进程- 不需要请求日历权限
- 用户可以在编辑器中修改事件详情和选择日历
- 实现
EKEventEditViewDelegate可以知道用户是否点击了”添加”
Siri Event Suggestions
(09:17)
import Intents
import CoreLocation
// 创建预订引用
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
)
// 创建 intent 和 response
let intent = INGetReservationDetailsIntent(reservationContainerReference: reservationReference)
let intentResponse = INGetReservationDetailsIntentResponse(code: .success, userActivity: nil)
intentResponse.reservations = [reservation]
// 创建并捐赠交互
let interaction = INInteraction(intent: intent, response: intentResponse)
interaction.donate()
关键点:
INSpeakableString需要唯一的vocabularyIdentifierINDateComponentsRange定义事件时间范围INRestaurantReservation是INReservation的子类,还有机票、酒店等子类interaction.donate()把事件推荐给系统,出现在日历收件箱
Write-only 权限添加事件
(12:41)
import EventKit
// 创建 event store
let store = EKEventStore()
// 请求 write-only 权限
guard try await store.requestWriteOnlyAccessToEvents() else { return }
// 创建事件
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."
// 保存事件
guard try store.save(event, span: .thisEvent) else { return }
关键点:
requestWriteOnlyAccessToEvents()是 iOS 17 新增方法- Info.plist 需要
NSCalendarsWriteOnlyAccessUsageDescription - 必须设置
calendar属性,用defaultCalendarForNewEvents - title、startDate、endDate 是必填项,其他可选
获取事件(Full Access)
(15:51)
import EventKit
// 创建 event store
let store = EKEventStore()
// 请求 full access
guard try await store.requestFullAccessToEvents() else { return }
// 创建查询条件:当前月份
guard let interval = Calendar.current.dateInterval(of: .month, for: Date()) else { return }
let predicate = store.predicateForEvents(
withStart: interval.start,
end: interval.end,
calendars: nil
)
// 获取事件
let events = store.events(matching: predicate)
// 按开始时间排序
let sortedEvents = events.sorted {
$0.compareStartDate(with: $1) == .orderedAscending
}
关键点:
requestFullAccessToEvents()是 iOS 17 新增方法- Info.plist 需要
NSCalendarsFullAccessUsageDescription predicateForEvents指定时间范围和可选的日历列表events(matching:)返回无序数组,需要手动排序- 尽量用最短的时间范围,提升性能
虚拟会议扩展
(19:18)
import EventKit
class VirtualConferenceProvider: EKVirtualConferenceProvider {
// 提供可用的会议室类型
override func fetchAvailableRoomTypes() async throws -> [EKVirtualConferenceRoomTypeDescriptor] {
let title = "My Room"
let identifier = "my_room"
let roomType = EKVirtualConferenceRoomTypeDescriptor(title: title, identifier: identifier)
return [roomType]
}
// 根据选择的类型提供会议详情
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
)
}
}
关键点:
- 在 Xcode 中创建 Virtual Conference Extension target
fetchAvailableRoomTypes()返回会议室类型列表,显示在日历的位置选择器中fetchVirtualConference()根据用户选择的类型返回会议链接EKVirtualConferenceURLDescriptor的 URL 建议用 Universal Links,可以直接打开你的 AppconferenceDetails显示在事件详情的虚拟会议区域
兼容旧版本系统
import EventKit
let store = EKEventStore()
if #available(iOS 17.0, macOS 14.0, *) {
// iOS 17+ 使用新方法
let granted = try await store.requestWriteOnlyAccessToEvents()
} else {
// 旧版本使用兼容方法
let granted = try await store.requestAccess(to: .event)
}
关键点:
- iOS 17 之前用
requestAccess(to:),之后用requestWriteOnlyAccessToEvents()或requestFullAccessToEvents() - iOS 17 之前的 App 需要
NSCalendarsUsageDescription - 使用 EventKitUI 的 App 还需要
NSContactsUsageDescription - 缺少这些描述字符串会导致崩溃
核心启发
1. 票务预订 App 一键添加日历
- 做什么:用户买电影票、演唱会票后,一键把场次信息添加到系统日历
- 为什么值得做:用
EKEventEditViewController不需要任何权限,用户信任度高 - 怎么开始:创建
EKEvent,填入电影名、影院地址、开场时间,弹出编辑器让用户确认
2. 餐厅预订助手
- 做什么:预订餐厅后,自动在日历收件箱中推荐事件,包含地址和导航
- 为什么值得做:Siri Event Suggestions 不需要权限和 UI,用户体验无缝
- 怎么开始:创建
INRestaurantReservation,donate 给系统,用户可以在日历收件箱中一键添加
3. 团队日程管理工具
- 做什么:读取用户的系统日历,在 App 中展示工作日程和个人日程的整合视图
- 为什么值得做:full access 可以获取所有日历事件,帮助用户统筹时间
- 怎么开始:请求
requestFullAccessToEvents(),用predicateForEvents查询,把结果和自己的日程数据合并展示
4. 视频会议 App 的日历集成
- 做什么:让用户在创建日历事件时,直接从位置选择器中选择你的会议服务
- 为什么值得做:虚拟会议扩展让品牌出现在系统日历中,提供一键加入的快捷方式
- 怎么开始:创建 Virtual Conference Extension target,实现两个协议方法,配置 Universal Links 跳回 App
关联 Session
- What’s new in privacy — 日历权限改进与隐私设计
- Broaden your reach with Siri Event Suggestions — Siri 事件建议 API 详解
- Build better document-based apps — 文档型 App 开发
评论
GitHub Issues · utterances