Files
FamilyMealPlanner/MealMood/Services/CalendarService.swift
T
alexandrev-tibco 6c7e12b41f 1.0.4: Smarter auto-assign, quick dish entry, calendar fix, frictionless onboarding
- AutocompleteEngine: no-repeat rule now applies to all dishes (not just tagless ones); fallback still allows repeats when no alternative exists
- CalendarService: update existing events instead of delete+create to prevent duplicates; fix weekStartDate using plan date instead of currentWeekStart
- DishFormView: form stays open after saving in create mode for quick multi-dish entry; Done button to close; saved feedback banner
- OnboardingViewModel/FirstDishesStepView: dishes are now optional during onboarding; Finish button always enabled
- AnalyticsService: new events — onboarding_step_viewed(step) and auto_assign_used(filled, unfilled)
- Localization: dish_done and first_dishes_optional_hint added in 6 languages; release notes updated in all 6 languages
- Version bump: 1.0.3 → 1.0.4 (build 19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 11:35:50 +02:00

144 lines
5.0 KiB
Swift

import EventKit
import Foundation
@MainActor
final class CalendarService {
static let shared = CalendarService()
private let eventStore = EKEventStore()
private init() {}
func requestAccess() async -> Bool {
do {
return try await eventStore.requestFullAccessToEvents()
} catch {
print("Calendar access error: \(error)")
return false
}
}
func availableCalendars() -> [EKCalendar] {
eventStore.calendars(for: .event)
}
func createEvent(
slot: MealSlot,
dishName: String,
dishDescription: String?,
weekStartDate: Date,
settings: AppSettings
) -> String? {
guard let calendar = resolvedCalendar(for: settings) else {
print("Calendar sync skipped: no writable calendar available")
return nil
}
let event = EKEvent(eventStore: eventStore)
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
event.title = "\(prefix)\(mealName): \(dishName)"
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
event.notes = dishDescription
event.calendar = calendar
if let reminder = settings.reminderMinutesBefore {
event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60)))
}
do {
try eventStore.save(event, span: .thisEvent)
return event.eventIdentifier
} catch {
print("Error saving event: \(error)")
return nil
}
}
func updateEvent(
eventId: String,
slot: MealSlot,
dishName: String,
dishDescription: String?,
weekStartDate: Date,
settings: AppSettings
) -> String? {
guard let event = eventStore.event(withIdentifier: eventId) else {
// Event was deleted externally create a fresh one
return createEvent(
slot: slot,
dishName: dishName,
dishDescription: dishDescription,
weekStartDate: weekStartDate,
settings: settings
)
}
let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) "
let mealNameKey = slot.mealType == "lunch" ? "lunch" : "dinner"
let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved())
event.title = "\(prefix)\(mealName): \(dishName)"
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
let slotTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
event.startDate = combineDateAndTime(date: slotDate, time: slotTime)
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
event.notes = dishDescription
event.alarms?.forEach { event.removeAlarm($0) }
if let reminder = settings.reminderMinutesBefore {
event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60)))
}
do {
try eventStore.save(event, span: .thisEvent)
return event.eventIdentifier
} catch {
print("Error updating event: \(error)")
return nil
}
}
func deleteEvent(eventId: String) {
guard let event = eventStore.event(withIdentifier: eventId) else { return }
do {
try eventStore.remove(event, span: .thisEvent)
} catch {
print("Error deleting event: \(error)")
}
}
func updateEventsTime(slots: [MealSlot], weekStartDate: Date, settings: AppSettings) {
for slot in slots {
guard let eventId = slot.calendarEventId,
let event = eventStore.event(withIdentifier: eventId),
event.startDate >= Date() else { continue }
let slotDate = weekStartDate.addingDays(slot.dayOfWeek)
let newTime = slot.mealType == "lunch" ? settings.lunchTime : settings.dinnerTime
event.startDate = combineDateAndTime(date: slotDate, time: newTime)
event.endDate = event.startDate.addingMinutes(settings.eventDuration)
try? eventStore.save(event, span: .thisEvent)
}
}
private func resolvedCalendar(for settings: AppSettings) -> EKCalendar? {
if let calendarId = settings.calendarId,
let calendar = eventStore.calendar(withIdentifier: calendarId) {
return calendar
}
if let defaultCalendar = eventStore.defaultCalendarForNewEvents {
return defaultCalendar
}
return eventStore.calendars(for: .event).first
}
}