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:
@@ -9,12 +9,14 @@ import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@StateObject var appState = AppState()
|
||||
@StateObject var groceryList = GroceryList()
|
||||
@StateObject var groceryList = GroceryListManager()
|
||||
|
||||
// Tab ViewModels
|
||||
@StateObject var recipeViewModel = RecipeTabView.ViewModel()
|
||||
@StateObject var searchViewModel = SearchTabView.ViewModel()
|
||||
|
||||
@ObservedObject private var userSettings = UserSettings.shared
|
||||
|
||||
@State private var selectedTab: Tab = .recipes
|
||||
|
||||
enum Tab {
|
||||
@@ -37,13 +39,23 @@ struct MainView: View {
|
||||
.environmentObject(groceryList)
|
||||
}
|
||||
|
||||
SwiftUI.Tab("Grocery List", systemImage: "storefront", value: .groceryList) {
|
||||
GroceryListTabView()
|
||||
.environmentObject(groceryList)
|
||||
if userSettings.groceryListMode != GroceryListMode.appleReminders.rawValue {
|
||||
SwiftUI.Tab("Grocery List", systemImage: "storefront", value: .groceryList) {
|
||||
GroceryListTabView()
|
||||
.environmentObject(groceryList)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
.modifier(TabBarMinimizeModifier())
|
||||
.onChange(of: userSettings.groceryListMode) { _, newValue in
|
||||
if newValue == GroceryListMode.appleReminders.rawValue && selectedTab == .groceryList {
|
||||
selectedTab = .recipes
|
||||
}
|
||||
Task {
|
||||
await groceryList.load()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
recipeViewModel.presentLoadingIndicator = true
|
||||
await appState.getCategories()
|
||||
|
||||
@@ -7,7 +7,7 @@ import SwiftUI
|
||||
|
||||
struct AllRecipesListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@Binding var showEditView: Bool
|
||||
@State private var allRecipes: [Recipe] = []
|
||||
@State private var searchText: String = ""
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
|
||||
struct RecipeListView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@State var categoryName: String
|
||||
@State var searchText: String = ""
|
||||
@Binding var showEditView: Bool
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
// MARK: - RecipeView Ingredients Section
|
||||
|
||||
struct RecipeIngredientSection: View {
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
||||
|
||||
var body: some View {
|
||||
@@ -86,7 +86,7 @@ struct RecipeIngredientSection: View {
|
||||
// MARK: - RecipeIngredientSection List Item
|
||||
|
||||
fileprivate struct IngredientListItem: View {
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@Binding var ingredient: String
|
||||
@Binding var servings: Double
|
||||
@State var recipeYield: Double
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
|
||||
struct GroceryListTabView: View {
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
|
||||
struct RecipeTabView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var groceryList: GroceryListManager
|
||||
@EnvironmentObject var viewModel: RecipeTabView.ViewModel
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@@ -110,6 +110,7 @@ struct RecipeTabView: View {
|
||||
case .settings:
|
||||
SettingsView()
|
||||
.environmentObject(appState)
|
||||
.environmentObject(groceryList)
|
||||
case .newRecipe:
|
||||
RecipeView(viewModel: RecipeView.ViewModel())
|
||||
.environmentObject(appState)
|
||||
|
||||
Reference in New Issue
Block a user