Реализация календаря и планировщика в мобильном приложении
Встроить «просто календарь» — одно из тех пожеланий, которое превращается в двухнедельную задачу. Часовые пояса, локализация дат, повторяющиеся события, синхронизация с системным календарём — каждый пункт добавляет нетривиальную сложность.
Архитектурные решения
Использовать готовую библиотеку или писать с нуля
Для большинства приложений — готовая библиотека. На 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 framework полностью поддерживает 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 недели.







