๐ Mastering EventKit
Integrate calendars and reminders! Build scheduling features with EventKit.
โญ Difficulty: โญโญ
โฑ๏ธ Est. Time: 1-2h
๐ App Services
โจ EventKit is?
EventKit is a framework for accessing iOS calendar and reminder data. From creating, editing, and deleting events to setting alarms and recurring schedules, all calendar features are available. It auto-syncs with iCloud for consistent data across all devices.
๐ฆ Key Features
Feature Overview
โ
Create/Edit/Delete calendar events
โ
Create/Edit/Delete reminders
โ
Alarm settings (time-based, location-based)
โ
๋ฐ๋ณต ์ผ์ (๋งค์ผ, ๋งค์ฃผ, ๋งค์...)
โ
Calendar search and filtering
โ
Attendee management
โ
iCloud ์๋ ๋๊ธฐํ๐ Permission Request
PermissionManager.swift
import EventKit class EventKitManager { static let shared = EventKitManager() let eventStore = EKEventStore() // Calendar Permission Request func requestCalendarAccess() async -> Bool { do { return try await eventStore.requestFullAccessToEvents() } catch { print("Calendar access error: \(error)") return false } } // ๋ฆฌ๋ง์ธ๋ Permission Request func requestReminderAccess() async -> Bool { do { return try await eventStore.requestFullAccessToReminders() } catch { print("Reminder access error: \(error)") return false } } // Permission status ํ์ธ func checkAuthorizationStatus() -> EKAuthorizationStatus { return EKEventStore.authorizationStatus(for: .event) } } // Usage Example let hasAccess = await EventKitManager.shared.requestCalendarAccess() if hasAccess { print("์บ๋ฆฐ๋ ์ ๊ทผ ํ์ฉ๋จ") }
๐ Creating Events
CreateEvent.swift
import EventKit extension EventKitManager { // Default ์ด๋ฒคํธ ์์ฑ func createEvent( title: String, startDate: Date, endDate: Date, notes: String? = nil, location: String? = nil ) throws -> String { // New ์ด๋ฒคํธ ์์ฑ let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.notes = notes event.location = location // Default ์บ๋ฆฐ๋์ ์ ์ฅ event.calendar = eventStore.defaultCalendarForNewEvents // Save try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } // Alarm ํฌํจ ์ด๋ฒคํธ func createEventWithAlarm( title: String, startDate: Date, endDate: Date, alarmMinutesBefore: Int ) throws -> String { let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.calendar = eventStore.defaultCalendarForNewEvents // Alarm ์ถ๊ฐ (10๋ถ ์ ) let alarm = EKAlarm(relativeOffset: TimeInterval(-alarmMinutesBefore * 60)) event.addAlarm(alarm) try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } } // Usage Example let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! let oneHourLater = Calendar.current.date(byAdding: .hour, value: 1, to: tomorrow)! let eventID = try EventKitManager.shared.createEvent( title: "ํ ๋ฏธํ ", startDate: tomorrow, endDate: oneHourLater, notes: "Q1 ๊ฒฐ๊ณผ ๋ฆฌ๋ทฐ", location: "ํ์์ค A" ) print("Event created: \(eventID)")
๐ Creating Recurring Events
RecurringEvent.swift
import EventKit extension EventKitManager { // ๋งค์ฃผ ๋ฐ๋ณต ์ด๋ฒคํธ func createWeeklyRecurringEvent( title: String, startDate: Date, endDate: Date ) throws { let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.calendar = eventStore.defaultCalendarForNewEvents // Set recurrence rule (weekly) let recurrenceRule = EKRecurrenceRule( recurrenceWith: .weekly, interval: 1, end: nil // ๋ฌดํ ๋ฐ๋ณต ) event.addRecurrenceRule(recurrenceRule) try eventStore.save(event, span: .thisEvent) } // ๋งค์ผ ๋ฐ๋ณต (10ํ) func createDailyRecurringEvent( title: String, startDate: Date, endDate: Date, occurrenceCount: Int ) throws { let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.calendar = eventStore.defaultCalendarForNewEvents // 10ํ ๋ฐ๋ณต ํ ์ข ๋ฃ let recurrenceEnd = EKRecurrenceEnd(occurrenceCount: occurrenceCount) let recurrenceRule = EKRecurrenceRule( recurrenceWith: .daily, interval: 1, end: recurrenceEnd ) event.addRecurrenceRule(recurrenceRule) try eventStore.save(event, span: .thisEvent) } // ํน์ ์์ผ๋ง ๋ฐ๋ณต (์, ์, ๊ธ) func createCustomRecurringEvent( title: String, startDate: Date, endDate: Date ) throws { let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.calendar = eventStore.defaultCalendarForNewEvents // ์, ์, ๊ธ ๋ฐ๋ณต let daysOfWeek = [ EKRecurrenceDayOfWeek(.monday), EKRecurrenceDayOfWeek(.wednesday), EKRecurrenceDayOfWeek(.friday) ] let recurrenceRule = EKRecurrenceRule( recurrenceWith: .weekly, interval: 1, daysOfTheWeek: daysOfWeek, daysOfTheMonth: nil, monthsOfTheYear: nil, weeksOfTheYear: nil, daysOfTheYear: nil, setPositions: nil, end: nil ) event.addRecurrenceRule(recurrenceRule) try eventStore.save(event, span: .thisEvent) } }
๐ Searching Events
SearchEvents.swift
import EventKit extension EventKitManager { // Search events in specific period func fetchEvents(from startDate: Date, to endDate: Date) -> [EKEvent] { let calendars = eventStore.calendars(for: .event) // Create predicate let predicate = eventStore.predicateForEvents( withStart: startDate, end: endDate, calendars: calendars ) // Event Fetch let events = eventStore.events(matching: predicate) return events } // ์ค๋์ ์ด๋ฒคํธ func fetchTodayEvents() -> [EKEvent] { let calendar = Calendar.current let startOfDay = calendar.startOfDay(for: Date()) let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)! return fetchEvents(from: startOfDay, to: endOfDay) } // ์ด๋ฒ ์ฃผ ์ด๋ฒคํธ func fetchThisWeekEvents() -> [EKEvent] { let calendar = Calendar.current let today = Date() guard let weekStart = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: today)), let weekEnd = calendar.date(byAdding: .weekOfYear, value: 1, to: weekStart) else { return [] } return fetchEvents(from: weekStart, to: weekEnd) } // ํน์ ์บ๋ฆฐ๋์ ์ด๋ฒคํธ๋ง func fetchEventsFromCalendar(calendarID: String, from: Date, to: Date) -> [EKEvent] { guard let calendar = eventStore.calendar(withIdentifier: calendarID) else { return [] } let predicate = eventStore.predicateForEvents( withStart: from, end: to, calendars: [calendar] ) return eventStore.events(matching: predicate) } } // Usage Example let todayEvents = EventKitManager.shared.fetchTodayEvents() for event in todayEvents { print("\(event.title ?? "์ ๋ชฉ ์์") - \(event.startDate ?? Date())") }
โ๏ธ Edit & Delete Events
ModifyEvent.swift
import EventKit extension EventKitManager { // Event Fetch func getEvent(identifier: String) -> EKEvent? { return eventStore.event(withIdentifier: identifier) } // Event ์์ func updateEvent(identifier: String, newTitle: String) throws { guard let event = getEvent(identifier: identifier) else { throw NSError(domain: "Event not found", code: 404) } event.title = newTitle try eventStore.save(event, span: .thisEvent) } // Event ์ญ์ func deleteEvent(identifier: String) throws { guard let event = getEvent(identifier: identifier) else { throw NSError(domain: "Event not found", code: 404) } try eventStore.remove(event, span: .thisEvent) } // ๋ฐ๋ณต ์ด๋ฒคํธ ์ญ์ (all ๋ฐ๋ณต ํฌํจ) func deleteRecurringEvent(identifier: String) throws { guard let event = getEvent(identifier: identifier) else { throw NSError(domain: "Event not found", code: 404) } // .futureEvents: ์ด ์ด๋ฒคํธ์ ๋ฏธ๋ ๋ฐ๋ณต ๋ชจ๋ ์ญ์ try eventStore.remove(event, span: .futureEvents) } }
๐ฑ SwiftUI Integration
CalendarView.swift
import SwiftUI import EventKit struct CalendarView: View { @State private var events: [EKEvent] = [] @State private var hasAccess = false @State private var showingAddEvent = false let manager = EventKitManager.shared var body: some View { NavigationStack { Group { if hasAccess { List { ForEach(events, id: \.eventIdentifier) { event in VStack(alignment: .leading, spacing: 4) { Text(event.title ?? "์ ๋ชฉ ์์") .font(.headline) HStack { Image(systemName: "clock") Text(event.startDate ?? Date(), style: .time) } .font(.caption) .foregroundStyle(.secondary) if let location = event.location { HStack { Image(systemName: "location") Text(location) } .font(.caption) .foregroundStyle(.secondary) } } .padding(.vertical, 4) } .onDelete(deleteEvent) } } else { VStack(spacing: 20) { Image(systemName: "calendar.badge.exclamationmark") .font(.system(size: 60)) .foregroundStyle(.gray) Text("์บ๋ฆฐ๋ ์ ๊ทผ ๊ถํ ํ์") .font(.title2) .bold() Button("Permission Request") { Task { await requestAccess() } } .buttonStyle(.borderedProminent) } } } .navigationTitle("์ค๋์ ์ผ์ ") .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { showingAddEvent = true }) { Image(systemName: "plus") } } } .sheet(isPresented: $showingAddEvent) { AddEventView() } } .task { await requestAccess() } } func requestAccess() async { hasAccess = await manager.requestCalendarAccess() if hasAccess { loadEvents() } } func loadEvents() { events = manager.fetchTodayEvents() } func deleteEvent(at offsets: IndexSet) { for index in offsets { let event = events[index] try? manager.deleteEvent(identifier: event.eventIdentifier) } loadEvents() } } // Event ์ถ๊ฐ ํ๋ฉด struct AddEventView: View { @Environment(\.dismiss) var dismiss @State private var title = "" @State private var startDate = Date() @State private var endDate = Date().addingTimeInterval(3600) @State private var location = "" @State private var notes = "" var body: some View { NavigationStack { Form { Section("๊ธฐ๋ณธ ์ ๋ณด") { TextField("์ ๋ชฉ", text: $title) TextField("์์น", text: $location) } Section("์๊ฐ") { DatePicker("Start", selection: $startDate) DatePicker("์ข ๋ฃ", selection: $endDate) } Section("๋ฉ๋ชจ") { TextEditor(text: $notes) .frame(height: 100) } } .navigationTitle("์ ์ด๋ฒคํธ") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { saveEvent() } .disabled(title.isEmpty) } } } } func saveEvent() { do { _ = try EventKitManager.shared.createEvent( title: title, startDate: startDate, endDate: endDate, notes: notes.isEmpty ? nil : notes, location: location.isEmpty ? nil : location ) dismiss() } catch { print("์ด๋ฒคํธ Save failed: \(error)") } } }
โ Reminders
Reminders.swift
import EventKit extension EventKitManager { // Create reminder func createReminder( title: String, dueDate: Date? = nil, priority: Int = 0 ) throws -> String { let reminder = EKReminder(eventStore: eventStore) reminder.title = title reminder.calendar = eventStore.defaultCalendarForNewReminders() // ๊ธฐํ ์ค์ if let dueDate = dueDate { reminder.dueDateComponents = Calendar.current.dateComponents( [.year, .month, .day, .hour, .minute], from: dueDate ) } // ์ฐ์ ์์ (0: ์์, 1-4: ๋์, 5: ๋ณดํต, 6-9: ๋ฎ์) reminder.priority = priority try eventStore.save(reminder, commit: true) return reminder.calendarItemIdentifier } // Complete๋์ง ์์ ๋ฆฌ๋ง์ธ๋ Fetch func fetchIncompleteReminders() async -> [EKReminder] { let predicate = eventStore.predicateForIncompleteReminders( withDueDateStarting: nil, ending: nil, calendars: nil ) return await withCheckedContinuation { continuation in eventStore.fetchReminders(matching: predicate) { reminders in continuation.resume(returning: reminders ?? []) } } } // ๋ฆฌ๋ง์ธ๋ ์๋ฃ ํ์ func completeReminder(identifier: String) throws { guard let reminder = eventStore.calendarItem(withIdentifier: identifier) as? EKReminder else { throw NSError(domain: "Reminder not found", code: 404) } reminder.isCompleted = true reminder.completionDate = Date() try eventStore.save(reminder, commit: true) } } // Usage Example let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! let reminderID = try EventKitManager.shared.createReminder( title: "์ฐ์ ์ฌ๊ธฐ", dueDate: tomorrow, priority: 1 ) // ๋ฏธ์๋ฃ ๋ฆฌ๋ง์ธ๋ Fetch let reminders = await EventKitManager.shared.fetchIncompleteReminders() print("ํ ์ผ: \(reminders.count)๊ฐ")
๐ Location-based Alarms
LocationAlarm.swift
import EventKit import CoreLocation extension EventKitManager { // Location ๊ธฐ๋ฐ ๋ฆฌ๋ง์ธ๋ func createLocationBasedReminder( title: String, latitude: Double, longitude: Double, radius: Double = 100 // ๋ฏธํฐ ) throws { let reminder = EKReminder(eventStore: eventStore) reminder.title = title reminder.calendar = eventStore.defaultCalendarForNewReminders() // Location ์๋ ์์ฑ let location = CLLocation(latitude: latitude, longitude: longitude) let structuredLocation = EKStructuredLocation(title: "์๋ฆผ ์์น") structuredLocation.geoLocation = location structuredLocation.radius = radius // ๋์ฐฉ/์ถ๋ฐ ์๋ฆผ let alarm = EKAlarm() alarm.structuredLocation = structuredLocation alarm.proximity = .enter // .enter (๋์ฐฉ), .leave (์ถ๋ฐ) reminder.addAlarm(alarm) try eventStore.save(reminder, commit: true) } } // Usage Example (์ง ๊ทผ์ฒ ๋์ฐฉ ์ ์๋ฆผ) try EventKitManager.shared.createLocationBasedReminder( title: "ํ๊ด๋ฌธ ์ด์ ํ์ธํ๊ธฐ", latitude: 37.5665, longitude: 126.9780, radius: 200 )
๐ก HIG Guidelines
HIG Recommendations
โ
DO
1. Permission Request ์ ์ด์ ์ค๋ช
- "์ผ์ ์ ์บ๋ฆฐ๋์ ์ ์ฅํ๋ ค๋ฉด ์ ๊ทผ Permission required"
2. ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ์ฌ์ฉ
- defaultCalendarForNewEvents ์ฌ์ฉ
- ์ฌ์ฉ์๊ฐ ์ ํํ ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ์กด์ค
3. ๋ช
ํํ ์ด๋ฒคํธ ์ ๋ณด
- ์ ๋ชฉ, ์๊ฐ, ์์น๋ฅผ ์ ํํ๊ฒ
- notes added to ์ ๋ณด ์ ๊ณต
4. ๋ฐ๋ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฃผ์
- .thisEvent vs .futureEvents ๊ตฌ๋ถ
- ์ฌ์ฉ์์๊ฒ ์ ํ ์ต์
์ ๊ณต
โ DON'T
1. ๊ถํ ์์ด ์บ๋ฆฐ๋ ์ ๊ทผ ์๋
2. ์ฌ์ฉ์ ๋์ ์์ด ์ด๋ฒคํธ ์์ฑ
3. ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ๋ฌด์ํ๊ณ ์์ ์บ๋ฆฐ๋์ ์ ์ฅ
4. ์๋ฆผ ์์ด existing ์ด๋ฒคํธ ์ญ์ ๐ง Practical Tips
Real-World Patterns
import EventKit // 1. ์บ๋ฆฐ๋ ๋ณ๊ฒฝ ๊ฐ์ง class CalendarObserver { let eventStore = EKEventStore() func observeChanges() { NotificationCenter.default.addObserver( forName: .EKEventStoreChanged, object: eventStore, queue: .main ) { _ in print("์บ๋ฆฐ๋ ๋ฐ์ดํฐ ๋ณ๊ฒฝ๋จ") // UI ์ ๋ฐ์ดํธ } } } // 2. ์ฐธ์์ ์ถ๊ฐ func createEventWithAttendees( title: String, startDate: Date, endDate: Date, attendeeEmails: [String] ) throws { let event = EKEvent(eventStore: EventKitManager.shared.eventStore) event.title = title event.startDate = startDate event.endDate = endDate // ์ฐธ์์๋ iOS์์ ์ง์ ์ถ๊ฐ ๋ถ๊ฐ (์ฝ๊ธฐ๋ง ๊ฐ๋ฅ) // Exchange ์๋ฒ๋ iCloud๋ฅผ through์๋ง ๊ฐ๋ฅ try EventKitManager.shared.eventStore.save(event, span: .thisEvent) } // 3. ์บ๋ฆฐ๋ ์์ Fetch func getCalendarColor(event: EKEvent) -> UIColor { return UIColor(cgColor: event.calendar.cgColor) }
๐ Info.plist Configuration
Info.plist
<!-- ์บ๋ฆฐ๋ ๊ถํ --> <key>NSCalendarsUsageDescription</key> <string>์ฑ์์ ์ผ์ ์ ์บ๋ฆฐ๋์ ์ ์ฅํ๋ ค๋ฉด Permission required</string> <!-- ๋ฆฌ๋ง์ธ๋ ๊ถํ --> <key>NSRemindersUsageDescription</key> <string>ํ ์ผ์ ๋ฆฌ๋ง์ธ๋ added toํ๋ ค๋ฉด Permission required</string>
๐ก EventKit ํต์ฌ
โ
์์คํ
์บ๋ฆฐ๋/๋ฆฌ๋ง์ธ๋ ์๋ฒฝ ํตํฉ
โ
iCloud ์๋ ๋๊ธฐํ
โ
๋ฐ๋ณต ์ผ์ Supported
โ
์์น ๊ธฐ๋ฐ ์๋
โ
all ์บ๋ฆฐ๋ ์ฑ๊ณผ ํธํ