EventKit для Swift: Полное руководство по работе с календарём и напоминаниями
EventKit — это мощный фреймворк от Apple, предоставляющий разработчикам Swift унифицированный интерфейс для взаимодействия с календарными базами данных и напоминаниями на устройствах iOS, iPadOS, macOS и watchOS. Он позволяет не только читать события и задачи, но и создавать, редактировать и удалять их, а также подписываться на уведомления об изменениях. Эта библиотека является ключевым инструментом для создания приложений-планировщиков, трекеров задач, корпоративных календарей и любых других программ, где требуется управление временем пользователя.
Зачем нужен EventKit? В современном мире управление временем — критически важная задача. EventKit даёт разработчику возможность интегрировать своё приложение с родными календарями и напоминаниями iOS/macOS, что избавляет пользователя от необходимости переключаться между разными программами. Вы можете автоматически добавлять встречи из вашего сервиса в календарь пользователя, синхронизировать дедлайны задач с напоминаниями или анализировать занятость. Без EventKit каждое приложение было бы изолированным "островом" данных, а с ним — становится частью единой экосистемы Apple.
Важно понимать, что EventKit — это не сторонняя библиотека, а системный фреймворк. Он встроен в SDK Apple и не требует установки через менеджеры пакетов для базового использования. Однако, для удобства работы и добавления дополнительных функций (например, реактивных обновлений) разработчики часто используют обёртки, такие как RxEventKit или EventKitUI. В этой статье мы сфокусируемся на чистом EventKit, так как это основа.
Установка (Подключение к проекту)
Поскольку EventKit является частью системного фреймворка Apple, его "установка" сводится к импорту модуля в ваш код. Команды pip или npm здесь не применимы. Для подключения выполните следующие шаги:
- Импорт в файле Swift: В начале вашего файла добавьте
import EventKit. - Добавление фреймворка в проект (если требуется): Обычно Xcode автоматически линкует системные фреймворки. Если нет — перейдите в Target > General > Frameworks, Libraries, and Embedded Content, нажмите "+" и добавьте
EventKit.framework. - Настройка Info.plist: Для доступа к календарю и напоминаниям необходимо добавить строки конфиденциальности:
NSCalendarsUsageDescription— строка, объясняющая, зачем приложению нужен доступ к календарю (например, "Чтобы добавлять встречи").NSRemindersUsageDescription— строка для доступа к напоминаниям.
// Установка не требуется, просто импортируем фреймворк
import EventKit
import UIKit
class ViewController: UIViewController {
// Ваш код
}
Быстрый старт: минимальный рабочий пример
Давайте создадим простое приложение, которое запрашивает доступ к календарю и создаёт одно событие "Тестовая встреча" на завтра.
import EventKit
import UIKit
class QuickStartViewController: UIViewController {
let eventStore = EKEventStore()
override func viewDidLoad() {
super.viewDidLoad()
requestAccessAndCreateEvent()
}
func requestAccessAndCreateEvent() {
// 1. Запрашиваем доступ к календарю
eventStore.requestAccess(to: .event) { [weak self] (granted, error) in
guard let self = self else { return }
if granted && error == nil {
// 2. Создаём событие
let event = EKEvent(eventStore: self.eventStore)
event.title = "Тестовая встреча"
event.startDate = Date().addingTimeInterval(24 * 60 * 60) // Завтра
event.endDate = event.startDate.addingTimeInterval(60 * 60) // Через час
event.calendar = self.eventStore.defaultCalendarForNewEvents
do {
try self.eventStore.save(event, span: .thisEvent, commit: true)
print("Событие создано: \(event.eventIdentifier ?? "Нет ID")")
} catch {
print("Ошибка сохранения: \(error.localizedDescription)")
}
} else {
print("Доступ запрещён или ошибка: \(error?.localizedDescription ?? "Неизвестная ошибка")")
}
}
}
}
Этот пример демонстрирует три ключевых шага: запрос разрешения, создание объекта EKEvent и его сохранение в календарь. Обратите внимание, что все операции с EventKit должны выполняться на фоновом потоке, чтобы не блокировать UI.
Основные классы и методы EventKit
Ниже представлен разбор 8 ключевых компонентов фреймворка. Каждый элемент включает сигнатуру, описание и рабочий пример.
1. EKEventStore — Хранилище событий
Сигнатура: class EKEventStore : NSObject
Описание: Основной класс для доступа к календарным базам данных. Через него выполняются все операции: чтение, запись, удаление событий и напоминаний.
Пример:
let eventStore = EKEventStore()
// Инициализация должна быть единственной на время жизни приложения (синглтон).
// Используйте один экземпляр для всех операций.
2. requestAccess(to:completion:) — Запрос доступа
Сигнатура: func requestAccess(to entityType: EKEntityType, completion: @escaping EKEventStoreRequestAccessCompletionHandler)
Описание: Запрашивает у пользователя разрешение на доступ к календарю (.event) или напоминаниям (.reminder). Вызывается один раз; последующие вызовы возвращают текущий статус.
Пример:
eventStore.requestAccess(to: .reminder) { granted, error in
if granted {
print("Доступ к напоминаниям разрешён")
} else {
// Покажите пользователю объяснение, почему нужен доступ
print("Доступ запрещён: \(error?.localizedDescription ?? "")")
}
}
3. EKEvent — Событие календаря
Сигнатура: class EKEvent : EKCalendarItem
Описание: Представляет собой одно событие в календаре. Содержит свойства: title, startDate, endDate, calendar, location, notes и другие.
Пример:
let newEvent = EKEvent(eventStore: eventStore)
newEvent.title = "Встреча с командой"
newEvent.startDate = Date()
newEvent.endDate = Date().addingTimeInterval(3600)
newEvent.notes = "Обсудить спринт"
newEvent.calendar = eventStore.defaultCalendarForNewEvents
4. save(_:span:commit:) — Сохранение события
Сигнатура: func save(_ event: EKEvent, span: EKSpan, commit: Bool) throws
Описание: Сохраняет новое или обновляет существующее событие. Параметр span определяет, применяются ли изменения к одному событию (.thisEvent) или ко всей серии повторяющихся событий (.futureEvents).
Пример:
do {
try eventStore.save(newEvent, span: .thisEvent, commit: true)
print("Событие сохранено с ID: \(newEvent.eventIdentifier ?? "нет")")
} catch {
print("Ошибка сохранения: \(error)")
}
5. predicateForEvents(withStart:end:calendars:) — Поиск событий
Сигнатура: func predicateForEvents(withStart startDate: Date, end endDate: Date, calendars: [EKCalendar]?) -> NSPredicate
Описание: Создаёт предикат для поиска событий в заданном диапазоне дат. Можно ограничить поиск конкретными календарями.
Пример:
let startDate = Date()
let endDate = Date().addingTimeInterval(7 * 24 * 3600) // Неделя
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
let events = eventStore.events(matching: predicate)
for event in events {
print("Найдено событие: \(event.title) в \(event.startDate)")
}
6. EKReminder — Напоминание
Сигнатура: class EKReminder : EKCalendarItem
Описание: Представляет задачу или напоминание. В отличие от EKEvent, напоминание не привязано ко времени начала/окончания, но может иметь дату выполнения (dueDateComponents) и приоритет.
Пример:
let reminder = EKReminder(eventStore: eventStore)
reminder.title = "Купить молоко"
reminder.calendar = eventStore.defaultCalendarForNewReminders()
let dueDate = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: Date().addingTimeInterval(86400))
reminder.dueDateComponents = dueDate
reminder.priority = 1 // Высокий приоритет
7. fetchReminders(matching:completion:) — Получение напоминаний
Сигнатура: func fetchReminders(matching predicate: NSPredicate, completion: @escaping ([EKReminder]?) -> Void)
Описание: Асинхронно загружает напоминания, соответствующие предикату. Предикат создаётся методами predicateForReminders(in:) или predicateForIncompleteReminders(withDueDateStarting:ending:calendars:).
Пример:
let predicate = eventStore.predicateForIncompleteReminders(withDueDateStarting: nil, ending: Date(), calendars: nil)
eventStore.fetchReminders(matching: predicate) { reminders in
guard let reminders = reminders else { return }
for reminder in reminders {
print("Просроченное напоминание: \(reminder.title)")
}
}
8. remove(_:span:commit:) — Удаление событий/напоминаний
Сигнатура: func remove(_ event: EKEvent, span: EKSpan, commit: Bool) throws (для событий)
func remove(_ reminder: EKReminder, commit: Bool) throws (для напоминаний)
Описание: Удаляет указанное событие или напоминание из календаря. Для повторяющихся событий можно удалить только одно или все будущие.
Пример:
// Удаление первого найденного события
if let eventToDelete = events.first {
do {
try eventStore.remove(eventToDelete, span: .thisEvent, commit: true)
print("Событие удалено")
} catch {
print("Ошибка удаления: \(error)")
}
}
Реальный практический пример: Менеджер встреч и задач
Создадим класс CalendarManager, который объединяет работу с календарём и напоминаниями. Он будет уметь создавать встречи, добавлять к ним напоминания за 15 минут и выводить список всех событий на сегодня.
import EventKit
import Foundation
class CalendarManager {
static let shared = CalendarManager()
private let eventStore = EKEventStore()
private init() {}
// MARK: - Запрос доступа ко всему сразу
func requestFullAccess(completion: @escaping (Bool) -> Void) {
let group = DispatchGroup()
var calendarGranted = false
var reminderGranted = false
group.enter()
eventStore.requestAccess(to: .event) { granted, _ in
calendarGranted = granted
group.leave()
}
group.enter()
eventStore.requestAccess(to: .reminder) { granted, _ in
reminderGranted = granted
group.leave()
}
group.notify(queue: .main) {
completion(calendarGranted && reminderGranted)
}
}
// MARK: - Создание встречи с напоминанием
func createMeetingWithReminder(title: String, startDate: Date, durationInMinutes: Int, completion: @escaping (Bool) -> Void) {
// 1. Создаём событие
let event = EKEvent(eventStore: eventStore)
event.title = title
event.startDate = startDate
event.endDate = startDate.addingTimeInterval(TimeInterval(durationInMinutes * 60))
event.calendar = eventStore.defaultCalendarForNewEvents
// 2. Добавляем напоминание за 15 минут (алерт)
let alarm = EKAlarm(relativeOffset: -15 * 60) // -900 секунд
event.addAlarm(alarm)
// 3. Сохраняем событие
do {
try eventStore.save(event, span: .thisEvent, commit: true)
print("Встреча '\(title)' создана")
// 4. Создаём связанное напоминание (задачу) в списке напоминаний
let reminder = EKReminder(eventStore: eventStore)
reminder.title = "Подготовиться к: \(title)"
reminder.calendar = eventStore.defaultCalendarForNewReminders()
// Устанавливаем дедлайн за 30 минут до встречи
let deadline = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: startDate.addingTimeInterval(-30 * 60))
reminder.dueDateComponents = deadline
let reminderAlarm = EKAlarm(relativeOffset: -60 * 60) // За час до дедлайна
reminder.addAlarm(reminderAlarm)
try eventStore.save(rem