캘린더 ToDo 만들기: SwiftUI와 EventKit으로 캘린더 이벤트를 완료 리스트로 전환
출처: Dev.to
가끔은 내 캘린더를 되돌아보면서 내가 계획한 것뿐 아니라 실제로 했던 일도 알고 싶을 때가 있다.
- 공부 세션을 끝냈나요?
- 실제로 헬스장에 갔나요?
- 이번 주에 어떤 예정된 작업을 완료했나요?
- 일일 일정에 어떤 리듬이 있나요?
캘린더 앱은 미래 계획을 관리하는 데는 뛰어나지만, 그 계획이 실제로 완료됐는지는 잘 보여주지 못한다.
그래서 나는 Calendar ToDo 라는 iOS 앱을 만들기 시작했다 – 캘린더 이벤트를 ‘완료’로 표시하고 캘린더를 개인 실행 로그로 바꿔주는 앱이다.
Calendar ToDo란?
Calendar ToDo는 기존 캘린더 이벤트에 완료 기록을 추가해 주는 iOS 앱이다.
핵심 아이디어는 아주 단순하다.
캘린더에는 이미 계획이 들어 있다. Calendar ToDo는 그 계획을 완료 목록으로 바꾸는 일을 도와준다.
기능
- 오늘의 캘린더 이벤트 표시
- 이벤트를 완료로 표시
- 오늘의 완료율 표시
- 완료된 이벤트 타임라인 보관
- 히트맵으로 실행 리듬 시각화
- 이벤트 종료 시 알림 전송
- 알림에서 바로 이벤트를 완료로 표시 가능
- iCloud / CloudKit과 완료 기록 동기화
디자인 결정: Calendar ToDo는 원본 캘린더 이벤트를 편집하지 않는다. EventKit을 사용해 캘린더 데이터를 읽어온 뒤, 완료 기록은 앱 내부에 별도로 저장한다.
왜 캘린더 제목을 직접 편집하지 않을까?
완료된 이벤트를 관리하는 가장 간단한 방법은 제목 앞에 체크 표시를 붙이는 것이다. 예:
✅ Study SwiftUI
✅ Workout
✅ Write blog post
개인용 소규모 캘린더에서는 통하지만, 다음과 같은 단점이 있다.
| 문제 | Calendar ToDo | 제목 편집 |
|---|---|---|
| 원본 캘린더 무결성 | 캘린더를 읽기 전용으로 유지 | 원본 캘린더를 수정 |
| 계획과 결과의 분리 | 완료 기록을 별도 저장 | 계획과 결과가 뒤섞임 |
| 공유 캘린더 | 공유 데이터에 손대지 않음 | 공유 캘린더에서 위험 |
| 반복 이벤트 | 각 발생을 개별 추적 | 반복 이벤트에서 오류 발생 |
업무용 캘린더, 공유 캘린더, 반복 이벤트가 있는 경우 제목을 직접 바꾸는 것은 안전하지 않다. Calendar ToDo는 캘린더 위에 레이어처럼 작동한다: 캘린더는 계획된 이벤트의 원천이며, 앱은 실행 결과만 저장한다.
기본 아키텍처
앱은 SwiftUI, EventKit, Core Data, CloudKit으로 구성된다.
단순화된 데이터 흐름은 다음과 같다:
SwiftUI
↓
EventKit
↓
캘린더 이벤트 읽기
↓
Core Data / CloudKit
↓
완료 기록 저장
↓
알림 / 위젯 / 리포트
- EventKit – 캘린더 이벤트를 읽는다.
- Core Data + CloudKit – 앱 자체의 완료 기록을 저장·동기화한다.
핵심 분리
- 캘린더 이벤트 – EventKit에서 가져온(읽기 전용) 데이터
- 완료 기록 – Calendar ToDo가 별도로 저장
이렇게 하면 사용자의 캘린더가 안전하게 보호된다.
EventKit으로 이벤트 읽기
EventKit을 이용해 오늘의 이벤트를 가져오는 코드는 대략 다음과 같다:
let eventStore = EKEventStore()
let calendars = eventStore.calendars(for: .event)
let predicate = eventStore.predicateForEvents(
withStart: startOfDay,
end: endOfDay,
calendars: calendars
)
let events = eventStore.events(matching: predicate)
가져온 EKEvent 객체들은 SwiftUI에 표시되지만, 앱은 절대 직접 수정하지 않는다. 이 규칙은 캘린더에 업무 이벤트, 공유 이벤트, 반복 이벤트, 혹은 다른 앱이 만든 이벤트가 포함될 수 있기 때문에 중요하다.
완료 기록을 별도로 저장하기
EKEvent에 완료 상태를 다시 쓰는 대신, 앱은 자체적인 완료 기록을 만든다:
struct CompletionRecord {
let occurrenceKey: String
let status: CompletionStatus
let completedAt: Date
}
enum CompletionStatus {
case done
case skipped
case partial
}
실제 구현에는 더 많은 필드가 있지만 핵심 아이디어는 동일하다: 캘린더 이벤트를 바꾸지 말고 결과를 별도로 저장한다. 이렇게 하면 프라이버시가 보호되고 기존 캘린더와 함께 사용할 때도 안전하다.
어려운 부분: 이벤트와 기록 매칭하기
처음엔 event.eventIdentifier만으로도 완료 기록을 `EKEvent

