Реалізація календаря та планувальника в мобільній програмі
Вбудувати «просто календар» — одне з тих побажань, яке перетворюється в двотижневу задачу. Часові пояси, локалізація дат, повторювані події, синхронізація з системним календарем — кожен пункт додає нетривіальну складність.
Архітектурні рішення
Використовувати готову бібліотеку або писати з нуля
Для більшості програм — готова бібліотека. На iOS: FSCalendar — одна з найзрілих, підтримує кастомізацію через appearance API та delegate-методи, працює на UIKit. Для SwiftUI — swift-calendar або власна реалізація через LazyVGrid з Calendar API.
На Android: kizitonwose/calendar-library (CalendarView та WeekCalendarView) — добре документована, Compose-сумісна. Material Design 3 містить DatePicker та DateRangePicker, але тільки для вибору дат, не для відображення подій.
У Flutter: table_calendar — де-факто стандарт з 2000+ stars, підтримує event markers, locale, формати відображення.
Писати з нуля оправдано тільки при дуже нестандартному дизайні або специфічних вимогах до продуктивності при тисячах подій у місяці.
Робота з датами та часовими поясами
Найчастіший баг: подія, створена в Москві (UTC+3), відображається в неправильному дні у користувача в Берліні (UTC+2). Причина — зберігання та порівняння дат без урахування часової зони.
Правило: в API завжди передаємо UTC timestamp (ISO 8601: 2024-03-15T14:30:00Z), конвертуємо в локальний час тільки для відображення. На iOS — TimeZone.current при ініціалізації Calendar та DateFormatter. Calendar(identifier: .gregorian) з calendar.timeZone = TimeZone(identifier: "Europe/Moscow")! для подій з фіксованою зоною (наприклад, конференція в Москві має відображатися в московському часі у всіх користувачів).
На Android — java.time.ZonedDateTime (API 26+) або бібліотека ThreeTenBP для старіших. LocalDate.ofInstant(instant, ZoneId.of("Europe/Berlin")) — безопасний перевід. Date та Calendar з java.util вважаються застарілими; якщо унаслідкований код використовує їх — плануй міграцію.
Повторювані события
Повторювані события (recurrence rules) — це RFC 5545 стандарт (iCalendar). RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20241231T235959Z — подія кожні понеділок, середу, п'ятницю до кінця 2024 року.
На iOS — фреймворк EventKit повністю підтримує RRULE через EKRecurrenceRule. Інтегруючи з системним календарем — використовуй EKEventStore та EKEvent.recurrenceRules. Для кастомного сховища — потрібен парсер RRULE. Готова реалізація: ical4j на JVM, rrule.js якщо є серверний JavaScript, на Swift — невеликі відкриті бібліотеки типу RRuleSwift.
Не зберігай в базі кожен екземпляр повторювального события — тільки правило та виключення (EXDATE). Генеруй екземпляри на льоту для відображуваного діапазону дат.
Синхронізація з системним календарем
iOS — EventKit з дозволом EKEntityType.event. Створення, читання, оновлення подій у системному календарі через EKEventStore. Користувач може вибрати в який із своїх календарів додати подію — показуємо UIAlertController зі списком з eventStore.calendars(for: .event).
Android — CalendarProvider ContentProvider. Доступ через ContentResolver з URI CalendarContract.Events.CONTENT_URI. Потребує дозволів READ_CALENDAR та WRITE_CALENDAR. Складніше ніж EventKit, але аналогічно по можливостям.
Важливо: синхронізація в обидві сторони. Якщо користувач видалив подію в системному календарі — приложення має це виявити. EKEventStore.reset() інвалідує кешовані дані; слухаємо EKEventStoreChangedNotification.
Продуктивність при великій кількості подій
FSCalendar та аналоги справляються з сотнями добре. При тисячах — потрібна віртуалізація: завантажуємо события тільки для видимого місяця + 1 місяць вперед/назад. NSFetchedResultsController на iOS або Room + Flow на Android для реактивного оновлення при зміні даних.
Event markers (точки під датою) — не рисуй UIView для кожної точки. CALayer або кастомна drawRect: з UIGraphicsGetCurrentContext() в тисячи разів ефективніше для ячейок з 5+ подіями.
Види відображення
Місячний, тижневий, денний вид — кожен потребує окремої верстки. Переключення між видами через UIPageViewController або горизонтальний UICollectionView з paging. Плавний перехід: анімація crossDissolve при зміні режиму, збереження вибраної дати як anchor.
Термін: базовий місячний календар з подіями — 1 тиждень. Повноцінний планувальник з тижневим/денним видом, повторюваними подіями, синхронізацією з системним календарем та offline-підтримкою — 2 тижні.







