
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
Start a class inside 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, the fetchUpcomingAppointments()
function is called to starts fetching 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 are setting the search range from the current date to 30 days ahead, using all active calendars for events and a predicate filter that defines the search criteria. This will the interface with upcoming events visible to the user.
Now write a function to create a new EKEvent
object. It will be the template of a new event to be passed to the EKEventEditViewController
from the EventKit UI framework and pre-populated 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
}
Now, this function creates a new event using EKEvent
. This template is needed to pass to an editor EKEventEditViewController
with pre-populated values.
The last step is to create an event store computed property to provide access to the store in this view model to the future views that will be used to view and edit events from here.
var eventStore: EKEventStore {
calendarStore
}
Here is how 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 implements UIViewControllerRepresentable
, the protocol required to use a UIViewController
, from UIKit
inside a SwiftUI
view. This is necessary because EventKitUI
was originally designed for UIKit, so we use the protocol mentioned above to bridge this gap between SwiftUI
and UIKit
.
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)
}
Start with the EKEvent
event to view and a callback function to run when the user leaves the screen.
Create an instance of EKEventViewController
to configure the event to be displayed. This function will enable two features, allowsEditing
to allow the user to edit the event and allowsCalendarPreview
to allow viewing details of the calendar. Finally, to delegate manipulation actions on the screen, use context.coordinator
.
The function return wraps everything in a UINavigationController
creating a navigation bar and a back button, essential when using UIKit
in SwiftUI
.
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
Since the event does not change dynamically during viewing, the updateUIViewController
function becomes required to comply with UIViewControllerRepresentable
.
func makeCoordinator() -> Coordinator {
Coordinator(onDismiss: onDismiss)
}
A function that creates the object that will act as the UIKit
delegate is required, and can indicate when the user ends the interaction with the event.
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 Coordinator
is a class that conforms to the EKEventViewDelegate
protocol. The eventViewController()
function is called automatically when the user confirms the action or exits the view. Call dismiss(animated:)
to close the view, and then execute the callback that will reload the event list in SwiftUI
. See the full code:
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
Create a struct that conforms to the UIViewControllerRepresentable
protocol, required to use UIKit
's UIViewController
inside SwiftUI
.
let appointment: EKEvent
let store: EKEventStore
let onDismiss: () -> Void
To do this, create input components:
appointment
: This can be an existing event to be edited or a newEKEvent
.store
: An instance ofEKEventStore
, required for the editor to save changes to the system calendar.onDismiss
: A callback that will be executed when the editing interface is closed.
func makeUIViewController(context: Context) -> EKEventEditViewController {
let editor = EKEventEditViewController()
editor.event = appointment
editor.eventStore = store
editor.editViewDelegate = context.coordinator
return editor
}
Now, an instance of EKEventEditViewController
, the native iOS event editor, is needed to define what will be edited indicated by editor.event
and the storage where it will be saved, editor.eventStore
. Finally, in this function, set the view's delegate, editViewDelegate
, as the Coordinator
, which will be responsible for capturing the moment when the user finishes editing.
func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
For protocol reasons, this function is required, even if empty. It would be used if we wanted to update the view's content based on state changes.
func makeCoordinator() -> Coordinator {
Coordinator(onDismiss: onDismiss)
}
Now, to pass the onDismiss
callback to be called when the editor is closed, create a function that returns a Coordinator
, which is an intermediate object between UIKit
and SwiftUI
.
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)
}
}
Finally, create a class for the Coordinator
that conforms to the EKEventEditViewDelegate
protocol. The eventEditViewController()
method is called when the user saves, cancels, or deletes the event, and the editor screen is dismissed with dismiss(animated:)
. See the full code:
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)
}
}
}
Main app interface
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.
@StateObject private var coordinator = ViewModel()
@State private var selectedAppointment: EKEvent?
@State private var showViewer = false
@State private var showEditor = false
Note that you need a ViewModel
instance to manage event loading, creation, and other processing, as well as variables to select an event and control the presentation of the viewing and editing/creation screen.
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 appointments. For each event, it displays the title and formatted date. Tapping an item saves the event to selectedAppointment
and showViewer
is activated, opening the detailed view.
.toolbar {
Button("New") {
selectedAppointment = coordinator.createTemplateEvent()
showEditor = true
}
}
A "New" button at the top of the screen allows the user to create a new event, which is generated by the ViewModel's createTemplateEvent()
. The edit screen is then displayed for the user to adjust the data.
.task {
await coordinator.requestAccessAndFetch()
}
The user's first interaction with the screen should be a request to the calendar, so the screen loads upcoming events. A Task
helps with this by running asynchronous code within the SwiftUI view.
.sheet(isPresented: $showViewer) {
if let appointment = selectedAppointment {
EventDetailView(appointment: appointment) {
Task { await coordinator.requestAccessAndFetch() }
showViewer = false
}
}
}
The detail view we call here EventDetailView
can be presented by a sheet
. The action on this view takes the selected event as a parameter, and when the user closes the viewer, the event list reloads.
.sheet(isPresented: $showEditor) {
if let appointment = selectedAppointment {
EventEditView(appointment: appointment, store: coordinator.eventStore) {
Task { await coordinator.requestAccessAndFetch() }
showEditor = false
}
}
}
A second sheet
, similar to the view, but for the struct we call EventEditView
, allows the user to edit or create events. After closing this editor, the events are reloaded. See the full code:
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.