Add Apple Reminders integration for grocery list with local mapping persistence

Introduce a GroceryListManager facade that delegates to either the existing
in-app GroceryList or a new RemindersGroceryStore backed by EventKit. Users
choose the mode in Settings; when Reminders mode is active the Grocery List
tab is hidden. Recipe-to-reminder grouping uses a local mapping file
(reminder_mappings.data) instead of polluting the reminder's notes field,
with automatic pruning when reminders are deleted externally.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 02:54:52 +01:00
parent 6824dbea6b
commit 98c82dc537
15 changed files with 800 additions and 12 deletions

View File

@@ -5,6 +5,7 @@
// Created by Vincent Meilinger on 15.09.23.
//
import EventKit
import Foundation
import OSLog
import SwiftUI
@@ -13,8 +14,11 @@ import SwiftUI
struct SettingsView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryListManager: GroceryListManager
@ObservedObject var userSettings = UserSettings.shared
@StateObject var viewModel = ViewModel()
@State private var reminderLists: [EKCalendar] = []
@State private var remindersPermission: EKAuthorizationStatus = .notDetermined
var body: some View {
Form {
@@ -54,6 +58,50 @@ struct SettingsView: View {
} footer: {
Text("The selected cookbook will open on app launch by default.")
}
Section {
Picker("Grocery list storage", selection: $userSettings.groceryListMode) {
ForEach(GroceryListMode.allValues, id: \.self) { mode in
Text(mode.descriptor()).tag(mode.rawValue)
}
}
if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
if remindersPermission == .notDetermined {
Button("Grant Reminders Access") {
Task {
let granted = await groceryListManager.requestRemindersAccess()
remindersPermission = groceryListManager.remindersPermissionStatus
if granted {
reminderLists = groceryListManager.availableReminderLists()
}
}
}
} else if remindersPermission == .denied || remindersPermission == .restricted {
Text("Reminders access was denied. Please enable it in System Settings to use this feature.")
.foregroundStyle(.secondary)
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
} else if remindersPermission == .fullAccess {
Picker("Reminders list", selection: $userSettings.remindersListIdentifier) {
ForEach(reminderLists, id: \.calendarIdentifier) { list in
Text(list.title).tag(list.calendarIdentifier)
}
}
}
}
} header: {
Text("Grocery List")
} footer: {
if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
Text("Grocery items will be saved to Apple Reminders. The Grocery List tab will be hidden since you can manage items directly in the Reminders app.")
} else {
Text("Grocery items are stored locally on this device.")
}
}
Section {
Toggle(isOn: $userSettings.expandNutritionSection) {
@@ -187,6 +235,16 @@ struct SettingsView: View {
}
.task {
await viewModel.getUserData()
remindersPermission = groceryListManager.remindersPermissionStatus
if remindersPermission == .fullAccess {
reminderLists = groceryListManager.availableReminderLists()
}
}
.onChange(of: userSettings.groceryListMode) { _, _ in
remindersPermission = groceryListManager.remindersPermissionStatus
if remindersPermission == .fullAccess {
reminderLists = groceryListManager.availableReminderLists()
}
}
}