Creating reminder lists with EventKit from your app
Create new calendars in EventKit to organize reminders or events under custom lists
To create custom reminder lists in your app, use the EventKit framework to organize reminders by context, project, or priority. This allows users to better structure their tasks through custom lists that integrate directly with the system’s Reminders app.
Interaction with the reminder system
Create a class in a Swift file to implement logic for interacting with system reminders.
Represent the gateway to access and modify reminders with a constant that stores an instance of the EKEventStore class, a list of EKCalendar objects representing reminder lists, and the available EKSource locations where reminders can be stored.
import EventKit
@MainActor
class ViewModel: ObservableObject {
private let eventStore = EKEventStore()
@Published var reminderLists: [EKCalendar] = []
@Published var availableSources: [EKSource] = []
}
In the class, create a method that asynchronously requests permission to access the user’s reminders. If permission is granted, another function is called to fetch the reminder lists from the system.
func requestAccessAndLoadLists() async {
do {
let permission = try await eventStore.requestFullAccessToReminders()
guard permission else { return }
loadReminderLists()
} catch {
print("Reminders access error:", error.localizedDescription)
}
}
func loadReminderLists() {
let allLists = eventStore.calendars(for: .reminder)
reminderLists = allLists
availableSources = eventStore.sources
}
The calendars(for:) method returns all reminder lists available in the system. The sources property provides the locations where reminders can be created, such as iCloud or local storage. Both properties, reminderLists and availableSources, are marked with @Published, which automatically updates the UI when their values change.
Before running this code, add the Privacy - Reminders Full Access Usage Description key to your Info.plist file to explain why your app needs access to reminders.
Now let's write a function to create a new reminder list with a custom name.
func createCustomList(named title: String) throws -> EKCalendar {
let newCalendar = EKCalendar(for: .reminder, eventStore: eventStore)
newCalendar.title = title
guard let source = eventStore.sources.first(where: { $0.sourceType == .local || $0.sourceType == .calDAV }) ??
eventStore.defaultCalendarForNewReminders()?.source else {
throw NSError(domain: "Reminder", code: 1, userInfo: [NSLocalizedDescriptionKey: "No valid source"])
}
newCalendar.source = source
try eventStore.saveCalendar(newCalendar, commit: true)
loadReminderLists()
return newCalendar
}
This function creates a new EKCalendar configured for reminders. It attempts to find a valid source, prioritizing local or CalDAV sources. If no valid source is found, it throws an error. Once created, the calendar is saved to the system and the lists are reloaded to reflect the change.
The final method adds individual reminders to a selected list.
func addReminder(title: String, to calendar: EKCalendar, due: Date?) throws {
let reminder = EKReminder(eventStore: eventStore)
reminder.title = title
reminder.calendar = calendar
if let due = due {
reminder.dueDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: due)
}
try eventStore.save(reminder, commit: true)
}
This creates a new EKReminder with the specified title and assigns it to the selected calendar. If a due date is provided, it converts the date to DateComponents before saving the reminder to the system.
Here is what it looks like once you are done:
import EventKit
@MainActor
class ViewModel: ObservableObject {
private let eventStore = EKEventStore()
@Published var reminderLists: [EKCalendar] = []
@Published var availableSources: [EKSource] = []
func requestAccessAndLoadLists() async {
do {
let permission = try await eventStore.requestFullAccessToReminders()
guard permission else { return }
loadReminderLists()
} catch {
print("Reminders access error:", error.localizedDescription)
}
}
func loadReminderLists() {
let allLists = eventStore.calendars(for: .reminder)
reminderLists = allLists
availableSources = eventStore.sources
}
func createCustomList(named title: String) throws -> EKCalendar {
let newCalendar = EKCalendar(for: .reminder, eventStore: eventStore)
newCalendar.title = title
guard let source = eventStore.sources.first(where: { $0.sourceType == .local || $0.sourceType == .calDAV }) ??
eventStore.defaultCalendarForNewReminders()?.source else {
throw NSError(domain: "Reminder", code: 1, userInfo: [NSLocalizedDescriptionKey: "No valid source"])
}
newCalendar.source = source
try eventStore.saveCalendar(newCalendar, commit: true)
loadReminderLists()
return newCalendar
}
func addReminder(title: String, to calendar: EKCalendar, due: Date?) throws {
let reminder = EKReminder(eventStore: eventStore)
reminder.title = title
reminder.calendar = calendar
if let due = due {
reminder.dueDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: due)
}
try eventStore.save(reminder, commit: true)
}
}
Creating the UI
Now, using the reminder-handling logic from the topics above, combine SwiftUI with EventKit to display existing lists, create new custom lists, and add reminders with titles and due dates.
struct ContentView: View {
@StateObject private var listManager = ViewModel()
@State private var selectedList: EKCalendar?
@State private var newListName = ""
@State private var reminderTitle = ""
@State private var dueDate = Date()
@State private var showAlert = false
@State private var alertMessage = ""
}
You need a ViewModel instance to manage list loading and creation, as well as properties to select a list, input text values, and control alert presentation.
Now, let's create a form section to display and select existing reminder lists.
Section(header: Text("Available Reminder Lists")) {
Picker("Select a List", selection: $selectedList) {
ForEach(listManager.reminderLists, id: \.calendarIdentifier) { calendar in
Text(calendar.title)
.tag(Optional(calendar))
}
}
}
The Picker displays all available reminder lists loaded from the view model. Each item shows the calendar title. The .tag(Optional(calendar)) modifier is necessary because selectedList is an optional value.
Next, add a section that allows users to create new custom lists.
Section(header: Text("Create New List")) {
TextField("List Name", text: $newListName)
Button("Create List") {
do {
let newCalendar = try listManager.createCustomList(named: newListName)
selectedList = newCalendar
newListName = ""
} catch {
alertMessage = "Failed to create list: \(error.localizedDescription)"
showAlert = true
}
}
.disabled(newListName.isEmpty)
}
This section provides a text field for entering the list name and a button that calls the createCustomList(named:) method. If successful, it sets the newly created list as selected and clears the input field. If an error occurs, an alert displays the error message.
Now, let's add the section for creating individual reminders.
Section(header: Text("Add Reminder")) {
TextField("Reminder Title", text: $reminderTitle)
DatePicker("Due Date", selection: $dueDate, displayedComponents: [.date, .hourAndMinute])
Button("Add Reminder") {
guard let selected = selectedList else {
alertMessage = "Please select a list first."
showAlert = true
return
}
do {
try listManager.addReminder(title: reminderTitle, to: selected, due: dueDate)
reminderTitle = ""
alertMessage = "Reminder added successfully!"
showAlert = true
} catch {
alertMessage = "Failed to add reminder: \(error.localizedDescription)"
showAlert = true
}
}
.disabled(reminderTitle.isEmpty)
}
The DatePicker allows users to set the due date. The button validates that a list is selected before creating the reminder. Success or failure messages are presented through alerts.
Add a section to display available sources for diagnostic purposes.
Section(header: Text("Available Sources")) {
ForEach(listManager.availableSources, id: \.title) { source in
Text("\(source.title) (\(source.sourceType.rawValue))")
}
}
This section lists all available sources, such as iCloud or local storage. This helps users understand where their reminders are stored and can be useful for diagnosing source-related errors.
Now let's set up the function to load the existing lists when the view appears.
.task {
await listManager.requestAccessAndLoadLists()
selectedList = listManager.reminderLists.first
}
The task(priority:_:) modifier is invoked when the view is loaded and asynchronously requests permission and fetches the reminder lists. It also selects the first list by default if one exists.
Finally, add an alert modifier to display messages to the user.
.alert("Info", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
Here is the complete code of the ContentView:
import SwiftUI
import EventKit
struct ContentView: View {
@StateObject private var listManager = ViewModel()
@State private var selectedList: EKCalendar?
@State private var newListName = ""
@State private var reminderTitle = ""
@State private var dueDate = Date()
@State private var showAlert = false
@State private var alertMessage = ""
var body: some View {
NavigationStack {
Form {
Section(header: Text("Available Reminder Lists")) {
Picker("Select a List", selection: $selectedList) {
ForEach(listManager.reminderLists, id: \.calendarIdentifier) { calendar in
Text(calendar.title)
.tag(Optional(calendar))
}
}
}
Section(header: Text("Create New List")) {
TextField("List Name", text: $newListName)
Button("Create List") {
do {
let newCalendar = try listManager.createCustomList(named: newListName)
selectedList = newCalendar
newListName = ""
} catch {
alertMessage = "Failed to create list: \(error.localizedDescription)"
showAlert = true
}
}
.disabled(newListName.isEmpty)
}
Section(header: Text("Add Reminder")) {
TextField("Reminder Title", text: $reminderTitle)
DatePicker("Due Date", selection: $dueDate, displayedComponents: [.date, .hourAndMinute])
Button("Add Reminder") {
guard let selected = selectedList else {
alertMessage = "Please select a list first."
showAlert = true
return
}
do {
try listManager.addReminder(title: reminderTitle, to: selected, due: dueDate)
reminderTitle = ""
alertMessage = "Reminder added successfully!"
showAlert = true
} catch {
alertMessage = "Failed to add reminder: \(error.localizedDescription)"
showAlert = true
}
}
.disabled(reminderTitle.isEmpty)
}
Section(header: Text("Available Sources")) {
ForEach(listManager.availableSources, id: \.title) { source in
Text("\(source.title) (\(source.sourceType.rawValue))")
}
}
}
.navigationTitle("Reminders Setup")
.task {
await listManager.requestAccessAndLoadLists()
selectedList = listManager.reminderLists.first
}
.alert("Info", isPresented: $showAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
}
}
}
When running the code in the Xcode simulator or on a physical device, you can create custom reminder lists, add reminders with due dates, and see all available storage sources where your reminders are saved.