Checking and editing the details of a calendar event

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.