WWDC给EventKit和EventKitUI框架带来了一些变化。在iOS 17中,人们的应用程序可以将事件添加到日历中,而无需提示用户使用IKEventEditViewController进行访问。
如果你的应用程序的目的是在编辑器UI中创建、配置和展示日历事件,可以考虑在你的应用程序中不提示用户授权而将事件保存到日历。
在这篇文章中,我将创建一个简单的应用来展示如何在iOS 17中向日历添加事件。上图显示了我们稍后将创建的应用程序。它只有一个显示票据信息的视图,当用户点击 "添加到日历 "按钮时,事件编辑器将显示出来,使用户能够保存事件或在保存事件之前改变一些信息。
首先,我们需要定义我们的票据结构,它包含一张票据的基本信息。
struct Ticket: Identifiable { var id: UUID = UUID() var title: String var theater: String var location: String var start: String var end: String var image: String }
|
然后在你的ContentView.swift中定义一个Ticket对象:
private let ticket: Ticket = Ticket(title: "哆啦A梦:大雄与天空的理想乡", theater: "Wanda Cinemas", location: "Orient Cinema Rongchuangmao", start: "2023-06-10T02:39:32Z", end: "2023-06-10T04:58:32Z", image: "movie")
|
在我们继续前进之前,我们需要检查我创建的票据视图。该票据视图包含几个部分:
- 电影的海报
- 剧院的名称
- 电影的名称
- 电影的开幕和闭幕日期
- 剧院的位置
- 添加"Add to Calendar "按钮
一些信息可以通过我们定义的Ticket轻松获取,但要注意电影的开场日期和结束日期的格式与Ticket.start和Ticket.end不同。我们需要在Ticket结构中添加一些成员变量来满足我们的需要。我将使用DateFormatter来将String日期改为目标日期:struct Ticket: Identifiable { ... private var dateFormatter: DateFormatter { let dfm = DateFormatter() dfm.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" return dfm } private var _startDate: Date? { if let startDate = dateFormatter.date(from: self.start) { return startDate } return nil } private var _endDate: Date? { if let endDate = dateFormatter.date(from: self.end) { return endDate } return nil } }
|
现在_startDate和_endDate以Date的格式存储电影的开场日期和结束日期。如果要访问单一信息,如年、月、日等,那就更好了。
为了达到这个目的,我将startDate和endDate添加到Ticket中,Ticket是包含每个日期的所有信息的tuples 图元:
struct Ticket: Identifiable { ... var startDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? { if let components = _startDate?.get(.day, .month, .year, .hour, .minute) { if let year = components.year, let day = components.day, let month = components.month, let hour = components.hour, let minute = components.minute { return (year, month, day, hour, minute) } } return nil } var endDate: (year: Int, month: Int, day: Int, hour: Int, minute: Int)? { if let components = _endDate?.get(.day, .month, .year, .hour, .minute) { if let year = components.year, let day = components.day, let month = components.month, let hour = components.hour, let minute = components.minute { return (year, month, day, hour, minute) } } return nil } } extension Date { func get(_ components: Calendar.Component..., calendar: Calendar = Calendar.current) -> DateComponents { return calendar.dateComponents(Set(components), from: self) } func get(_ component: Calendar.Component, calendar: Calendar = Calendar.current) -> Int { return calendar.component(component, from: self) } }
|
现在我们可以通过使用ticket.startDate.year的语法来访问单个信息,如年份。
Ticket View
Ticket View很容易实现,这里我只给出完整代码:
struct ContentView: View { private let ticket: Ticket = Ticket(title: "哆啦A梦:大雄与天空的理想乡", theater: "Wanda Cinemas", location: "Orient Cinema Rongchuangmao", start: "2023-06-10T02:39:32Z", end: "2023-06-10T04:58:32Z", image: "movie") var body: some View { ZStack(alignment: .bottom, content: { Image(ticket.image) .resizable() .aspectRatio(contentMode: .fit) HStack { VStack(alignment: .leading, spacing: 1) { Text(ticket.theater) .foregroundStyle(.blue) .bold() Text(ticket.title) .font(.system(size: 20)) if let startDate = ticket.startDate, let endDate = ticket.endDate { Text("\(startDate.month)月\(startDate.day)日 \(startDate.hour):\(startDate.minute)-\(endDate.hour):\(endDate.minute)") } HStack { Image(systemName: "mappin") .foregroundStyle(.red) Text(ticket.location) .foregroundStyle(.gray) } .padding(.top, 7) .font(.system(size: 16)) HStack { Image(systemName: "calendar") .foregroundStyle(.blue) Button("Add to calendar") { /// Add to calendar... } } .padding(.top, 7) } Spacer() } .padding() .frame(width: 350) .background(Color.white) }) .frame(width: 350) .clipShape(RoundedRectangle(cornerRadius: 10)) .shadow(radius: 5) } }
|
使用EventKitUI保存事件
在iOS上,EventKitUI框架被用来向用户适度地显示日历和提醒信息。
EventKitUI提供了视图控制器,用于查看和编辑日历和提醒信息,选择要查看的日历,以及确定将日历显示为只读还是可读可写。由于我们没有SwiftUI版本的EventKitUI,我们必须将UIViewController转换为View。
日历有什么新功能
正如我之前所说,在iOS 17中,你的应用程序可以在不提示用户使用IKEventEditViewController访问的情况下向日历添加事件。
这意味着你不必提供NSCalendarsUsageDescription键(这个键在iOS 17.0中已被废弃)或info.plist中的任何其他键,应用程序应该只请求完成其日历数据任务所需的特定访问级别。iOS 17 SDK还引入了新的日历使用描述字符串,能够在不提示用户访问的情况下向日历添加事件,以及新的只写访问。因为我们只添加事件,不需要任何密钥,所以我不打算在这里谈这些细节,你可以看看访问事件存储的细节。现在让我们看看如何创建EventEditViewController。
EKEventEditViewController
为了让IKEventEditViewController在SwiftUI中工作,我们需要向UIViewControllerRepresentable寻求帮助。我们的UIViewControllerType被定义为 EKEventEditViewController。
import EventKitUI struct EventEditViewController: UIViewControllerRepresentable { typealias UIViewControllerType = EKEventEditViewController func makeUIViewController(context: Context) -> EKEventEditViewController { } func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {} }
|
用EventKitUI添加一个事件是一个四步的过程:
- 创建一个event store 。
- 创建一个事件并填入细节。
- 创建一个配置为编辑事件的视图控制器。
- 展示视图控制器。
创建一个event store 很容易。只需一行代码:
private let store = EKEventStore()
|
创建一个ECEvent对象实例是一个比较复杂的过程,因为我们需要向对象实例添加详细信息。
在这里,我创建了一个名为event的私有变量,它将变量ticket中的信息转换为IKEvent类型:
struct EventEditViewController: UIViewControllerRepresentable { let ticket: Ticket private var event: EKEvent { let event = EKEvent(eventStore: store) event.title = ticket.title if let startDate = ticket.startDate, let endDate = ticket.endDate { let startDateComponents = DateComponents(year: startDate.year, month: startDate.month, day: startDate.day, hour: startDate.hour, minute: startDate.minute) event.startDate = Calendar.current.date(from: startDateComponents)! let endDateComponents = DateComponents(year: endDate.year, month: endDate.month, day: endDate.day, hour: endDate.hour, minute: endDate.minute) event.endDate = Calendar.current.date(from: endDateComponents)! event.location = ticket.location event.notes = "Don't forget to bring popcorn!" } return event } ... }
|
每个事件都需要一个标题。标题会在很多地方使用,包括小工具和通知,所以要保持简单。最重要的属性是开始和结束日期。使用日期组件来制作开始日期和结束日期。设置一个地点,让人们知道事件发生的地点。
包括一个完整的地址或使用一个MapKit句柄将启用地图建议和离开时间提醒等功能。最后,我添加一些注释以提供一些额外的细节。
现在你已经设置了事件属性,下一步是创建 EKEventEditViewController。赋予事件和事件存储属性。代码写在方法makeUIViewController(context:)里面。
func makeUIViewController(context: Context) -> EKEventEditViewController { let eventEditViewController = EKEventEditViewController() eventEditViewController.event = event eventEditViewController.eventStore = store return eventEditViewController }
|
添加到日历
现在回到我们的票据视图ticket view,我们还有一些剩余的工作要完成。添加一个名为showEventEditView的变量,用来显示EventEditViewController:
@State private var showEventEditView: Bool = false
当用户点击 "添加到日历Add to Calendar "按钮时,showEventEditView应该变为true,然后显示EventEditViewController:
Button("Add to calendar") { self.showEventEditView.toggle() } .sheet(isPresented: $showEventEditView, content: { EventEditViewController(ticket: self.ticket) })
|
现在,当我们点击该按钮时,事件编辑视图应该出现。然而,你可能会发现,当轻点取消或添加按钮时,事件编辑视图不会解散。
这是因为日历编辑发生在过程之外,检查被解雇控制器的属性可以帮助我们解雇视图。
启用撤消
由于UIViewControllerRepresentable不会自动将我们的视图控制器内发生的变化传达给我们的SwiftUI界面的其他部分。当我们希望我们的视图控制器与其他SwiftUI视图协调时,我们必须提供一个协调员实例来促进这些互动。
struct EventEditViewController: UIViewControllerRepresentable { @Environment(\.presentationMode) var presentationMode ... func makeCoordinator() -> Coordinator { return Coordinator(self) } class Coordinator: NSObject, EKEventEditViewDelegate { var parent: EventEditViewController init(_ controller: EventEditViewController) { self.parent = controller } func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) { parent.presentationMode.wrappedValue.dismiss() } } }
|
最后在方法makeUIViewController中添加以下代码:func makeUIViewController(context: Context) -> EKEventEditViewController { ... eventEditViewController.editViewDelegate = context.coordinator return eventEditViewController }
|
可以在GitHub上找到源代码