
Checking and editing the details of a calendar event
Learn how to use EventKitUI to see details, edit events and choose calendars.
To display an interface for viewing and editing calendar events and reminders with the native UI, use the EventKitUI framework, which works in conjunction with view controllers create, edit, delete and display existing events.
Interaction with the native calendar
Create a class within a Swift file that contains the logic for interacting with the native iOS calendar.
Represent the gateway to access and modify calendars with a constant that stores an instance of the EKEventStore
class and a list of EKEvent
objects that will be displayed in the app's interface.
import EventKit
@MainActor
class ViewModel: ObservableObject {
private let calendarStore = EKEventStore()
@Published var appointments: [EKEvent] = []
}
In the class, create a method to request permission to access the user's calendar synchronously. If permission is granted, another function is called to fetch events from the user’s calendar.
func requestAccessAndFetch() async {
do {
let isGranted = try await calendarStore.requestFullAccessToEvents()
guard isGranted else { return }
fetchUpcomingAppointments()
} catch {
print("Access error: \(error.localizedDescription)")
}
}
func fetchUpcomingAppointments() {
let now = Date()
let thirtyDaysLater = Calendar.current.date(byAdding: .day, value: 30, to: now)!
let calendars = calendarStore.calendars(for: .event)
let filter = calendarStore.predicateForEvents(
withStart: now,
end: thirtyDaysLater,
calendars: calendars
)
let events = calendarStore
.events(matching: filter)
.sorted { $0.startDate < $1.startDate }
self.appointments = events
}
We’re setting the search range from the current date to 30 days in the future. We’ll use all active calendars for events and apply a predicate filter to define the search criteria. This will allow us to populate the future interface with the user’s upcoming events.
Now let’s write a function to create a new EKEvent
object. This object will serve as a template for a new event to be passed to the EKEventEditViewController
from the EventKit UI framework in the future, pre-populating its values.
func createTemplateEvent() -> EKEvent {
let entry = EKEvent(eventStore: calendarStore)
entry.title = "New Appointment"
entry.startDate = Date().addingTimeInterval(3600)
entry.endDate = entry.startDate.addingTimeInterval(3600)
return entry
}
The final step is to create a computed property that provides access to the event store in this view model for future views. These views will be used to view and edit events with this view model.
var eventStore: EKEventStore {
calendarStore
}
Here is what it looks like once you are done:
import EventKit
@MainActor
class ViewModel: ObservableObject {
private let calendarStore = EKEventStore()
@Published var appointments: [EKEvent] = []
func requestAccessAndFetch() async {
do {
let isGranted = try await calendarStore.requestFullAccessToEvents()
guard isGranted else { return }
fetchUpcomingAppointments()
} catch {
print("Access error: \(error.localizedDescription)")
}
}
func fetchUpcomingAppointments() {
let now = Date()
let thirtyDaysLater = Calendar.current.date(byAdding: .day, value: 30, to: now)!
let calendars = calendarStore.calendars(for: .event)
let filter = calendarStore.predicateForEvents(withStart: now, end: thirtyDaysLater, calendars: calendars)
let events = calendarStore.events(matching: filter).sorted { $0.startDate < $1.startDate }
self.appointments = events
}
func createTemplateEvent() -> EKEvent {
let entry = EKEvent(eventStore: calendarStore)
entry.title = "New Appointment"
entry.startDate = Date().addingTimeInterval(3600)
entry.endDate = entry.startDate.addingTimeInterval(3600)
return entry
}
var eventStore: EKEventStore {
calendarStore
}
}
View the details of an event
Create a struct that conforms to UIViewControllerRepresentable
, the protocol required to use a UIViewController
, from UIKit, inside a SwiftUI view. This is necessary because EventKit UI was initially designed to be used in conjunction with UIKit.
Create two properties, the first one to store the event that will have the details displayed, and a second one representing a callback function to run when the user leaves the screen.
struct EventDetailView: UIViewControllerRepresentable {
let appointment: EKEvent
let onDismiss: () -> Void
}
Now implement the makeUIViewController(context:)
method, which is mandatory to conform to the UIViewControllerRepresentable
protocol. It creates the view controller object and configures its initial state. Also implement the updateUIViewController(_:context:)
method, but keep it empty, since it won’t be necessary for this particualr use case.
struct EventDetailView: UIViewControllerRepresentable {
let appointment: EKEvent
let onDismiss: () -> Void
func makeUIViewController(context: Context) -> UINavigationController {
let viewer = EKEventViewController()
viewer.event = appointment
viewer.allowsEditing = true
viewer.allowsCalendarPreview = true
viewer.delegate = context.coordinator
return UINavigationController(rootViewController: viewer)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
// Not used, but needed for conforming with the UIViewControllerRepresentable protocol
}
}
Inside the method, create an instance of EKEventViewController
to configure it with the event to be displayed. Set allowsEditing
and allowsCalendarPreview
to true.
Then set the delegate
to a context.coordinator
value, which will be implemented later.
We are returning the event view controller wrapped in a UINavigationController
, which will provide a basic navigation structure to the view.
Next step is to implement the coordinator of our view controller, which will conform to the EKEventViewDelegate
protocol and be responsible for handling the actions associated with it. The coordinator is responsible for communicating changes from the view controller to other parts of the SwiftUI interface.
struct EventDetailView: UIViewControllerRepresentable {
...
func makeCoordinator() -> Coordinator {
Coordinator(onDismiss: onDismiss)
}
class Coordinator: NSObject, EKEventViewDelegate {
let onDismiss: () -> Void
init(onDismiss: @escaping () -> Void) {
self.onDismiss = onDismiss
}
func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) {
controller.dismiss(animated: true, completion: onDismiss)
}
}
}
The eventViewController(_:didCompleteWith:)
function is called automatically when the user closes the event view controller in any way.
By calling the dismiss(animated:completion:)
method from the view controller, you have the opportunity to invoke a callback function, which can perform actions like reloading the list of events on the SwiftUI interface.
Here is the complete code of the EventDetailView
:
import SwiftUI
import EventKitUI
struct EventDetailView: UIViewControllerRepresentable {
let appointment: EKEvent
let onDismiss: () -> Void
func makeUIViewController(context: Context) -> UINavigationController {
let viewer = EKEventViewController()
viewer.event = appointment
viewer.allowsEditing = true
viewer.allowsCalendarPreview = true
viewer.delegate = context.coordinator
return UINavigationController(rootViewController: viewer)
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onDismiss: onDismiss)
}
class Coordinator: NSObject, EKEventViewDelegate {
let onDismiss: () -> Void
init(onDismiss: @escaping () -> Void) {
self.onDismiss = onDismiss
}
func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) {
controller.dismiss(animated: true, completion: onDismiss)
}
}
}
Edit or create calendar events
Creating the view for editing and creating calendar events is a process that is similar to viewing event details. It involves creating a view that conforms to UIViewControllerRepresentable
. Instead of making an EKEventViewController
, you will create a EKEventEditViewController
and implement the coordinator that conforms to the EKEventEditViewDelegate
protocol.
Another key difference is that you will need access to an EKEventStore
object to create and save events.
Here is the complete code for the EventEditView
:
import SwiftUI
import EventKitUI
struct EventEditView: UIViewControllerRepresentable {
let appointment: EKEvent
let store: EKEventStore
let onDismiss: () -> Void
func makeUIViewController(context: Context) -> EKEventEditViewController {
let editor = EKEventEditViewController()
editor.event = appointment
editor.eventStore = store
editor.editViewDelegate = context.coordinator
return editor
}
func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onDismiss: onDismiss)
}
class Coordinator: NSObject, EKEventEditViewDelegate {
let onDismiss: () -> Void
init(onDismiss: @escaping () -> Void) {
self.onDismiss = onDismiss
}
func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
controller.dismiss(animated: true, completion: onDismiss)
}
}
}
Notice that in the makeUIViewController(context:)
method, the object created is of the type EKEventEditViewController
and that the event store is set up.
Then the Coordinator
class conforms to EKEventEditViewDelegate
, which means that the method implemented is the eventEditViewController(_:didCompleteWith:)
method.
Creating the UI
Now, with the event handling logic from the topics above, combine SwiftUI with EventKitUI to display upcoming events in a list, create and edit events using the native calendar system interface as you currently do.
struct ContentView: View {
@StateObject private var coordinator = ViewModel()
@State private var selectedAppointment: EKEvent?
@State private var showViewer = false
@State private var showEditor = false
}
You need a ViewModel
instance to manage event loading and creation, as well as properties to select an event and control the presentation of the viewing and editing/creation views.
Now let’s list all the events.
List(coordinator.appointments, id: \.eventIdentifier) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.startDate.formatted(date: .abbreviated, time: .shortened))
.foregroundStyle(.secondary)
}
.onTapGesture {
selectedAppointment = item
showViewer = true
}
}
The code above lists the appointments. For each event, it displays the title and formatted date. Tapping an item sets the selectedAppointment
property to the selected event, sets showViewer
to true, which will display the EventDetailView
later.
Let’s set up the UI to create a new event by using the toolbar(content:)
modifier at the List
view.
.toolbar {
Button("New") {
selectedAppointment = coordinator.createTemplateEvent()
showEditor = true
}
}
The button creates a new, empty event to populate the EventEditView
, which is used to create and save a new event.
Now let’s set up the function to load the existing events in the user interface.
.task {
await coordinator.requestAccessAndFetch()
}
The task(priority:_:)
modifier is invoked when the view is loaded and asynchronously fetches the upcoming events by calling the ViewModel
’s fetching method.
Now let’s create the modal to present the event details view and the event editor view.
.sheet(isPresented: $showViewer) {
if let appointment = selectedAppointment {
EventDetailView(appointment: appointment) {
Task { await coordinator.requestAccessAndFetch() }
showViewer = false
}
}
}
.sheet(isPresented: $showEditor) {
if let appointment = selectedAppointment {
EventEditView(appointment: appointment, store: coordinator.eventStore) {
Task { await coordinator.requestAccessAndFetch() }
showEditor = false
}
}
}
Here is the complete code of the ContentView
:
import SwiftUI
import EventKit
struct ContentView: View {
@StateObject private var coordinator = ViewModel()
@State private var selectedAppointment: EKEvent?
@State private var showViewer = false
@State private var showEditor = false
var body: some View {
NavigationStack {
List(coordinator.appointments, id: \.eventIdentifier) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.startDate.formatted(date: .abbreviated, time: .shortened))
.foregroundStyle(.secondary)
}
.onTapGesture {
selectedAppointment = item
showViewer = true
}
}
.navigationTitle("Appointments")
.toolbar {
Button("New") {
selectedAppointment = coordinator.createTemplateEvent()
showEditor = true
}
}
.task {
await coordinator.requestAccessAndFetch()
}
.sheet(isPresented: $showViewer) {
if let appointment = selectedAppointment {
EventDetailView(appointment: appointment) {
Task { await coordinator.requestAccessAndFetch() }
showViewer = false
}
}
}
.sheet(isPresented: $showEditor) {
if let appointment = selectedAppointment {
EventEditView(appointment: appointment, store: coordinator.eventStore) {
Task { await coordinator.requestAccessAndFetch() }
showEditor = false
}
}
}
}
}
}
When running the code in the Xcode simulator or on a physical device, you can view all the fields available to the user when creating or editing an event.
The interface allows you to set the event title, location, start and end date and time, configure recurrence, set alerts, add attachments, and access several other options provided by the interface.