๐ EventKit ์์ ์ ๋ณต
์บ๋ฆฐ๋์ ๋ฆฌ๋ง์ธ๋๋ฅผ ์ฑ์ ํตํฉ! EventKit์ผ๋ก ์ผ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
โจ EventKit์ด๋?
EventKit์ iOS์ ์บ๋ฆฐ๋ ๋ฐ ๋ฆฌ๋ง์ธ๋ ๋ฐ์ดํฐ์ ์ ๊ทผํ๋ ํ๋ ์์ํฌ์ ๋๋ค. ์ด๋ฒคํธ ์์ฑ, ์์ , ์ญ์ ๋ถํฐ ์๋ ์ค์ , ๋ฐ๋ณต ์ผ์ ๊น์ง ๋ชจ๋ ์บ๋ฆฐ๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. iCloud์ ์๋ ๋๊ธฐํ๋์ด ๋ชจ๋ ๊ธฐ๊ธฐ์์ ์ผ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํฉ๋๋ค.
๐ฆ ์ฃผ์ ๊ธฐ๋ฅ
๊ธฐ๋ฅ ๊ฐ์
โ
์บ๋ฆฐ๋ ์ด๋ฒคํธ ์์ฑ/์์ /์ญ์
โ
๋ฆฌ๋ง์ธ๋ ์์ฑ/์์ /์ญ์
โ
์๋ ์ค์ (์๊ฐ ๊ธฐ๋ฐ, ์์น ๊ธฐ๋ฐ)
โ
๋ฐ๋ณต ์ผ์ (๋งค์ผ, ๋งค์ฃผ, ๋งค์...)
โ
์บ๋ฆฐ๋ ๊ฒ์ ๋ฐ ํํฐ๋ง
โ
์ฐธ์์ ๊ด๋ฆฌ
โ
iCloud ์๋ ๋๊ธฐํ๐ ๊ถํ ์์ฒญ
PermissionManager.swift
import EventKit class EventKitManager { static let shared = EventKitManager() let eventStore = EKEventStore() // ์บ๋ฆฐ๋ ๊ถํ ์์ฒญ func requestCalendarAccess() async -> Bool { do { return try await eventStore.requestFullAccessToEvents() } catch { print("Calendar access error: \(error)") return false } } // ๋ฆฌ๋ง์ธ๋ ๊ถํ ์์ฒญ func requestReminderAccess() async -> Bool { do { return try await eventStore.requestFullAccessToReminders() } catch { print("Reminder access error: \(error)") return false } } // ๊ถํ ์ํ ํ์ธ func checkAuthorizationStatus() -> EKAuthorizationStatus { return EKEventStore.authorizationStatus(for: .event) } } // ์ฌ์ฉ ์์ let hasAccess = await EventKitManager.shared.requestCalendarAccess() if hasAccess { print("์บ๋ฆฐ๋ ์ ๊ทผ ํ์ฉ๋จ") }
๐ ์ด๋ฒคํธ ์์ฑํ๊ธฐ
CreateEvent.swift
import EventKit extension EventKitManager { // ๊ธฐ๋ณธ ์ด๋ฒคํธ ์์ฑ func createEvent( title: String, startDate: Date, endDate: Date, notes: String? = nil, location: String? = nil ) throws -> String { // ์ ์ด๋ฒคํธ ์์ฑ let event = EKEvent(eventStore: eventStore) event.title = title event.startDate = startDate event.endDate = endDate event.notes = notes event.location = location // ๊ธฐ๋ณธ ์บ๋ฆฐ๋์ ์ ์ฅ event.calendar = eventStore.defaultCalendarForNewEvents // ์ ์ฅ try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } // ์๋ ํฌํจ ์ด๋ฒคํธ 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 // ์๋ ์ถ๊ฐ (10๋ถ ์ ) let alarm = EKAlarm(relativeOffset: TimeInterval(-alarmMinutesBefore * 60)) event.addAlarm(alarm) try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } } // ์ฌ์ฉ ์์ 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("์ด๋ฒคํธ ์์ฑ๋จ: \(eventID)")
๐ ๋ฐ๋ณต ์ด๋ฒคํธ ์์ฑ
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 // ๋ฐ๋ณต ๊ท์น ์ค์ (๋งค์ฃผ ๋ฐ๋ณต) 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) } }
๐ ์ด๋ฒคํธ ๊ฒ์ํ๊ธฐ
SearchEvents.swift
import EventKit extension EventKitManager { // ํน์ ๊ธฐ๊ฐ์ ์ด๋ฒคํธ ๊ฒ์ func fetchEvents(from startDate: Date, to endDate: Date) -> [EKEvent] { let calendars = eventStore.calendars(for: .event) // Predicate ์์ฑ let predicate = eventStore.predicateForEvents( withStart: startDate, end: endDate, calendars: calendars ) // ์ด๋ฒคํธ ๊ฐ์ ธ์ค๊ธฐ 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) } } // ์ฌ์ฉ ์์ let todayEvents = EventKitManager.shared.fetchTodayEvents() for event in todayEvents { print("\(event.title ?? "์ ๋ชฉ ์์") - \(event.startDate ?? Date())") }
โ๏ธ ์ด๋ฒคํธ ์์ ๋ฐ ์ญ์
ModifyEvent.swift
import EventKit extension EventKitManager { // ์ด๋ฒคํธ ๊ฐ์ ธ์ค๊ธฐ func getEvent(identifier: String) -> EKEvent? { return eventStore.event(withIdentifier: identifier) } // ์ด๋ฒคํธ ์์ 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) } // ์ด๋ฒคํธ ์ญ์ 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) } // ๋ฐ๋ณต ์ด๋ฒคํธ ์ญ์ (๋ชจ๋ ๋ฐ๋ณต ํฌํจ) 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 ํตํฉ
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("๊ถํ ์์ฒญ") { 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() } } // ์ด๋ฒคํธ ์ถ๊ฐ ํ๋ฉด 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("์์", selection: $startDate) DatePicker("์ข ๋ฃ", selection: $endDate) } Section("๋ฉ๋ชจ") { TextEditor(text: $notes) .frame(height: 100) } } .navigationTitle("์ ์ด๋ฒคํธ") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("์ทจ์") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("์ ์ฅ") { 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("์ด๋ฒคํธ ์ ์ฅ ์คํจ: \(error)") } } }
โ ๋ฆฌ๋ง์ธ๋ (Reminders)
Reminders.swift
import EventKit extension EventKitManager { // ๋ฆฌ๋ง์ธ๋ ์์ฑ 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 } // ์๋ฃ๋์ง ์์ ๋ฆฌ๋ง์ธ๋ ๊ฐ์ ธ์ค๊ธฐ 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) } } // ์ฌ์ฉ ์์ let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! let reminderID = try EventKitManager.shared.createReminder( title: "์ฐ์ ์ฌ๊ธฐ", dueDate: tomorrow, priority: 1 ) // ๋ฏธ์๋ฃ ๋ฆฌ๋ง์ธ๋ ๊ฐ์ ธ์ค๊ธฐ let reminders = await EventKitManager.shared.fetchIncompleteReminders() print("ํ ์ผ: \(reminders.count)๊ฐ")
๐ ์์น ๊ธฐ๋ฐ ์๋
LocationAlarm.swift
import EventKit import CoreLocation extension EventKitManager { // ์์น ๊ธฐ๋ฐ ๋ฆฌ๋ง์ธ๋ func createLocationBasedReminder( title: String, latitude: Double, longitude: Double, radius: Double = 100 // ๋ฏธํฐ ) throws { let reminder = EKReminder(eventStore: eventStore) reminder.title = title reminder.calendar = eventStore.defaultCalendarForNewReminders() // ์์น ์๋ ์์ฑ 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) } } // ์ฌ์ฉ ์์ (์ง ๊ทผ์ฒ ๋์ฐฉ ์ ์๋ฆผ) try EventKitManager.shared.createLocationBasedReminder( title: "ํ๊ด๋ฌธ ์ด์ ํ์ธํ๊ธฐ", latitude: 37.5665, longitude: 126.9780, radius: 200 )
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
HIG ๊ถ์ฅ์ฌํญ
โ
DO
1. ๊ถํ ์์ฒญ ์ ์ด์ ์ค๋ช
- "์ผ์ ์ ์บ๋ฆฐ๋์ ์ ์ฅํ๋ ค๋ฉด ์ ๊ทผ ๊ถํ์ด ํ์ํฉ๋๋ค"
2. ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ์ฌ์ฉ
- defaultCalendarForNewEvents ์ฌ์ฉ
- ์ฌ์ฉ์๊ฐ ์ ํํ ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ์กด์ค
3. ๋ช
ํํ ์ด๋ฒคํธ ์ ๋ณด
- ์ ๋ชฉ, ์๊ฐ, ์์น๋ฅผ ์ ํํ๊ฒ
- notes์ ์ถ๊ฐ ์ ๋ณด ์ ๊ณต
4. ๋ฐ๋ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ฃผ์
- .thisEvent vs .futureEvents ๊ตฌ๋ถ
- ์ฌ์ฉ์์๊ฒ ์ ํ ์ต์
์ ๊ณต
โ DON'T
1. ๊ถํ ์์ด ์บ๋ฆฐ๋ ์ ๊ทผ ์๋
2. ์ฌ์ฉ์ ๋์ ์์ด ์ด๋ฒคํธ ์์ฑ
3. ๊ธฐ๋ณธ ์บ๋ฆฐ๋ ๋ฌด์ํ๊ณ ์์ ์บ๋ฆฐ๋์ ์ ์ฅ
4. ์๋ฆผ ์์ด ๊ธฐ์กด ์ด๋ฒคํธ ์ญ์ ๐ง ์ค๋ฌด ํ์ฉ ํ
์ค์ ํจํด
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๋ฅผ ํตํด์๋ง ๊ฐ๋ฅ try EventKitManager.shared.eventStore.save(event, span: .thisEvent) } // 3. ์บ๋ฆฐ๋ ์์ ๊ฐ์ ธ์ค๊ธฐ func getCalendarColor(event: EKEvent) -> UIColor { return UIColor(cgColor: event.calendar.cgColor) }
๐ Info.plist ์ค์
Info.plist
<!-- ์บ๋ฆฐ๋ ๊ถํ --> <key>NSCalendarsUsageDescription</key> <string>์ฑ์์ ์ผ์ ์ ์บ๋ฆฐ๋์ ์ ์ฅํ๋ ค๋ฉด ๊ถํ์ด ํ์ํฉ๋๋ค</string> <!-- ๋ฆฌ๋ง์ธ๋ ๊ถํ --> <key>NSRemindersUsageDescription</key> <string>ํ ์ผ์ ๋ฆฌ๋ง์ธ๋์ ์ถ๊ฐํ๋ ค๋ฉด ๊ถํ์ด ํ์ํฉ๋๋ค</string>
๐ก EventKit ํต์ฌ
โ
์์คํ
์บ๋ฆฐ๋/๋ฆฌ๋ง์ธ๋ ์๋ฒฝ ํตํฉ
โ
iCloud ์๋ ๋๊ธฐํ
โ
๋ฐ๋ณต ์ผ์ ์ง์
โ
์์น ๊ธฐ๋ฐ ์๋
โ
๋ชจ๋ ์บ๋ฆฐ๋ ์ฑ๊ณผ ํธํ