Swift and SwiftUI tutorials for Swift Developers

EventKit in SwiftUI

As an iOS Developer, sooner or later you will face the challenge of integrating your application with the user’s device calendar or reminders. Whether you are building a productivity app, a booking manager, or a fitness platform, accessing the user’s schedule is a critical feature. This is where EventKit comes into play.

Historically, EventKit was designed in the era of Objective-C and UIKit. However, with Apple’s modern paradigm, integrating EventKit in SwiftUI has become an essential skill in Swift programming.

In this extensive and detailed tutorial, you will learn from scratch what EventKit is, how to configure your project in Xcode, how to manage modern privacy permissions, and how to create a complete application using Swift and SwiftUI that works flawlessly across iOS, macOS, and watchOS.


1. What is EventKit?

EventKit is Apple’s native framework that provides access to calendar data (Events) and the Reminders app. By using EventKit, your SwiftUI application doesn’t have to create its own database to manage dates and appointments; instead, it reads and writes directly to the centralized database of iOS, macOS, or watchOS.

This means that any event your app creates using EventKit will automatically appear in Apple’s official Calendar app, in Google Calendar (if the user has it synced), or in Outlook.

The Core Architecture

To master Swift programming with EventKit, you need to understand its three pillars:

  1. EKEventStore: This is the main engine. Think of it as the connection to the database. You should only have one instance of EKEventStore alive in your app during its entire lifecycle.
  2. EKCalendar: Represents a specific calendar (e.g., “Work”, “Family”, or “Birthdays”).
  3. EKEvent and EKReminder: These are the individual objects that hold the data (title, start date, end date, location).

2. Xcode Configuration: The Permissions Barrier

Before writing a single line of code in Swift, we need to configure our project in Xcode. Apple is extremely strict with user privacy. If you try to access the calendar without declaring why you need it, your app will crash immediately.

Starting with iOS 17 and macOS 14, Apple introduced granular permission levels for calendars:

  • Write-only access: Allows your app to add events without being able to see what the user already has on their calendar.
  • Full access: Allows reading, editing, and deleting any event.

Adding keys to Info.plist

Open your project in Xcode, go to the Info.plist file (or the “Info” tab in the target settings) and add the following keys, depending on the access you need:

  1. For Full Access (Read and Write):
    • Key: Privacy - Calendars Full Access Usage Description (NSCalendarsFullAccessUsageDescription)
    • Value: “We need full access to read your events and display your daily schedule.”
  2. For Write-Only Access (Add events without reading):
    • Key: Privacy - Calendars Write Only Access Usage Description (NSCalendarsWriteOnlyAccessUsageDescription)
    • Value: “We need access to save this booking to your calendar.”

Note: If your app also requires reminders, add NSRemindersFullAccessUsageDescription.


3. Creating the EventKit Manager in Swift

Since we are working with SwiftUI, the best practice is to create a class that acts as an ObservableObject to manage all the EventKit logic. This keeps our view clean and separates the business logic from the interface.

Create a new Swift file named CalendarManager.swift:

import Foundation
import EventKit

@MainActor
class CalendarManager: ObservableObject {
    // The single instance of the store we will use
    let store = EKEventStore()
    
    // Published list of events for SwiftUI to react to
    @Published var events: [EKEvent] = []
    @Published var isAccessGranted: Bool = false
    
    // Method to request permissions
    func requestAccess() async {
        do {
            // Request full access to events
            let granted = try await store.requestFullAccessToEvents()
            self.isAccessGranted = granted
            
            if granted {
                fetchEvents()
            }
        } catch {
            print("Error requesting calendar permissions: \(error.localizedDescription)")
            self.isAccessGranted = false
        }
    }
    
    // Method to fetch events for the next month
    func fetchEvents() {
        guard isAccessGranted else { return }
        
        let calendars = store.calendars(for: .event)
        let now = Date()
        
        // Calculate the date 30 days from now
        guard let nextMonth = Calendar.current.date(byAdding: .day, value: 30, to: now) else { return }
        
        // Create a predicate (a search query)
        let predicate = store.predicateForEvents(withStart: now, end: nextMonth, calendars: calendars)
        
        // Fetch the events and assign them to our published variable
        self.events = store.events(matching: predicate)
    }
}

Code Explanation:

  • @MainActor: Ensures that any changes to @Published properties are made on the main thread, which is mandatory in SwiftUI to update the UI.
  • requestFullAccessToEvents(): This is the modern, asynchronous API introduced recently. It replaces the old closure-based functions (requestAccess(to:completion:)).
  • predicateForEvents: This is the efficient way to search in EventKit. Instead of fetching every event in history (which would crash the memory), we define a date range.

4. Building the Interface in SwiftUI

Now that we have our brain (the CalendarManager), let’s create the body. We are going to design a view in SwiftUI that first requests permissions and then displays a list of events.

import SwiftUI
import EventKit

struct ContentView: View {
    @StateObject private var calendarManager = CalendarManager()
    
    var body: some View {
        NavigationView {
            Group {
                if calendarManager.isAccessGranted {
                    EventsListView(events: calendarManager.events)
                } else {
                    PermissionView(manager: calendarManager)
                }
            }
            .navigationTitle("My Agenda")
        }
        .task {
            // Check the current permission status when loading the view
            let status = EKEventStore.authorizationStatus(for: .event)
            if status == .fullAccess {
                calendarManager.isAccessGranted = true
                calendarManager.fetchEvents()
            }
        }
    }
}

// Sub-view to request permissions
struct PermissionView: View {
    @ObservedObject var manager: CalendarManager
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "calendar.badge.exclamationmark")
                .font(.system(size: 60))
                .foregroundColor(.orange)
            
            Text("Calendar Access Required")
                .font(.title2)
                .bold()
            
            Text("To show your upcoming appointments, we need your permission to read the calendar.")
                .multilineTextAlignment(.center)
                .foregroundColor(.secondary)
                .padding(.horizontal)
            
            Button(action: {
                Task {
                    await manager.requestAccess()
                }
            }) {
                Text("Grant Permission")
                    .bold()
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding(.horizontal, 40)
        }
    }
}

// Sub-view to display the event list
struct EventsListView: View {
    var events: [EKEvent]
    
    var body: some View {
        List {
            if events.isEmpty {
                Text("You have no upcoming events.")
                    .foregroundColor(.secondary)
            } else {
                ForEach(events, id: \.eventIdentifier) { event in
                    EventRowView(event: event)
                }
            }
        }
        // Recommended for a good user experience on iOS
        .refreshable {
            // Here you would call fetchEvents again if it were directly accessible
        }
    }
}

// Individual design of each event cell
struct EventRowView: View {
    let event: EKEvent
    
    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            Text(event.title)
                .font(.headline)
            
            HStack {
                // Calendar color
                Circle()
                    .fill(Color(event.calendar.cgColor))
                    .frame(width: 10, height: 10)
                
                Text(formatDate(event.startDate))
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                
                Spacer()
                
                if let location = event.location, !location.isEmpty {
                    Image(systemName: "mappin.and.ellipse")
                        .foregroundColor(.red)
                        .font(.caption)
                }
            }
        }
        .padding(.vertical, 4)
    }
    
    private func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter.string(from: date)
    }
}

This implementation demonstrates the power of SwiftUI. With a few lines of code, we have managed asynchronous states and created a reactive interface. If the permission changes, the view will update automatically.


5. Adding Events Programmatically and Natively

A vital part for any iOS Developer is allowing the user to interact with the calendar by creating new events. There are two ways to do this when using EventKit in SwiftUI:

Option A: Direct programming in code (Background)

If your app needs to create an event automatically (for example, upon confirming a flight ticket purchase), you will do it purely in Swift:

extension CalendarManager {
    func addEvent(title: String, startDate: Date, endDate: Date) {
        let newEvent = EKEvent(eventStore: self.store)
        newEvent.title = title
        newEvent.startDate = startDate
        newEvent.endDate = endDate
        
        // Assign the user's default calendar
        newEvent.calendar = store.defaultCalendarForNewEvents
        
        do {
            try store.save(newEvent, span: .thisEvent)
            print("Event successfully saved")
            // Refresh the list to show the new event
            fetchEvents() 
        } catch {
            print("Error saving the event: \(error.localizedDescription)")
        }
    }
}

Option B: Using EKEventEditViewController (Recommended)

For a superior user experience, you should show the native iOS event creation screen. Since this is a UIKit view, we need a bridge to SwiftUI using UIViewControllerRepresentable.

import SwiftUI
import EventKitUI

struct EventEditViewController: UIViewControllerRepresentable {
    let eventStore: EKEventStore
    let event: EKEvent?
    @Environment(\.presentationMode) var presentationMode
    
    func makeUIViewController(context: Context) -> EKEventEditViewController {
        let controller = EKEventEditViewController()
        controller.eventStore = eventStore
        
        if let event = event {
            controller.event = event
        }
        
        controller.editViewDelegate = context.coordinator
        return controller
    }
    
    func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, EKEventEditViewDelegate {
        var parent: EventEditViewController
        
        init(_ parent: EventEditViewController) {
            self.parent = parent
        }
        
        func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
            parent.presentationMode.wrappedValue.dismiss()
            // Here you could notify your CalendarManager to call fetchEvents()
        }
    }
}

Now, in your main SwiftUI view, you can invoke this using a .sheet modifier:

// ... inside your ContentView
@State private var showingAddEvent = false

// ... on the navigation bar button:
.toolbar {
    Button(action: { showingAddEvent = true }) {
        Image(systemName: "plus")
    }
}
.sheet(isPresented: $showingAddEvent, onDismiss: {
    calendarManager.fetchEvents()
}) {
    EventEditViewController(eventStore: calendarManager.store, event: nil)
}

6. Cross-Platform Synchronization: iOS, macOS, and watchOS

The beauty of modern Swift programming is its portability. The code we have written to integrate EventKit in SwiftUI will work almost without modifications across the entire Apple ecosystem. However, there are key considerations that every iOS Developer should know when opening their Xcode project to other platforms:

Considerations for macOS

On macOS, using EventKit requires enabling a Capability in Xcode (App Sandbox). You must check the boxes corresponding to Calendar and Reminders in the “App Sandbox” -> “App Data” section within the target settings. Without this, your app on Mac will silently fail or throw Sandbox exceptions, regardless of what you put in the Info.plist.

Considerations for watchOS

On the Apple Watch, the EventKit framework is available, but the native EKEventEditViewController interface (which belongs to EventKitUI) is not.

To resolve this in a universal project, you must use conditional compilation (#if) in Swift:

// In your view file...
.toolbar {
    Button(action: { showingAddEvent = true }) {
        Image(systemName: "plus")
    }
}
#if os(iOS)
.sheet(isPresented: $showingAddEvent) {
    EventEditViewController(eventStore: calendarManager.store, event: nil)
}
#elseif os(watchOS)
.sheet(isPresented: $showingAddEvent) {
    // For watchOS, you must create your own SwiftUI form
    CustomWatchEventForm(manager: calendarManager)
}
#endif

Also, remember that watchOS is designed for quick interactions. Fetching very large date ranges on the watch will drain the battery. Limit your searches using predicateForEvents to a couple of days into the future.


7. Listening to External Changes (Advanced Observation)

A common mistake a beginner iOS Developer makes is assuming that the user will only modify their calendar through your app. What happens if the user opens the native Apple app, deletes an event, and returns to your app? Your SwiftUI interface will show outdated data.

To solve this, EventKit emits a notification every time the database changes. We must subscribe to it in our CalendarManager:

import Combine

class CalendarManager: ObservableObject {
    // ... previous properties ...
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // We listen for changes in the EventKit database
        NotificationCenter.default.publisher(for: .EKEventStoreChanged, object: store)
            // Ensure we receive the update on the main thread
            .receive(on: RunLoop.main) 
            .sink { [weak self] _ in
                // If the external database changes, we reload our list
                self?.fetchEvents()
            }
            .store(in: &cancellables)
    }
    // ... rest of the code ...
}

With this simple block, your application will magically stay synchronized with iCloud and other calendar apps in real-time.


Conclusion

Integrating EventKit in SwiftUI is a fundamental step to creating high-value applications in the Apple ecosystem. Throughout this guide, we have explored how to configure Xcode, request strict privacy permissions, and write clean, reactive code in Swift.

By mastering EKEventStore and using bridges like UIViewControllerRepresentable, you can combine the robustness of Apple’s underlying APIs with the incredible development speed of SwiftUI. Always remember to test your apps on physical devices, as the simulator often does not accurately reflect the real behavior of iCloud-synced calendars.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

TabView Bottom Accessory in SwiftUI

Next Article

CloudKit in SwiftUI

Related Posts