Fix meal plan not populating on first login and add pull-to-refresh sync

Rewrite MealPlanSyncManager.performSync() (renamed from performInitialSync) to
discover _mealPlanAssignment metadata from all server recipes, not just locally-
known ones. On first sync all recipes are checked; on subsequent syncs only
recipes modified since lastMealPlanSyncDate are fetched (max 5 concurrent).

Trigger meal plan sync from pull-to-refresh on both the recipe and meal plan
tabs, and from the "Refresh all" toolbar button.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 11:40:31 +01:00
parent c8d9ab7397
commit 1f7f19c74b
9 changed files with 108 additions and 36 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store .DS_Store
Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/hendrik.hogertz.xcuserdatad/UserInterfaceState.xcuserstate Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/hendrik.hogertz.xcuserdatad/UserInterfaceState.xcuserstate
Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/hendrik.hogertz.xcuserdatad/xcschemes/xcschememanagement.plist Nextcloud Cookbook iOS Client.xcodeproj/xcuserdata/hendrik.hogertz.xcuserdatad/xcschemes/xcschememanagement.plist
Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/hendrik.hogertz.xcuserdatad/IDEFindNavigatorScopes.plist

View File

@@ -933,13 +933,14 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Nextcloud-Cookbook-iOS-Client-Info.plist"; INFOPLIST_FILE = "Nextcloud-Cookbook-iOS-Client-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cookbook; INFOPLIST_KEY_CFBundleDisplayName = Cookbook;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
INFOPLIST_KEY_NSRemindersFullAccessUsageDescription = "This app uses Reminders to save your grocery list items.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -955,7 +956,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.10.1; MARKETING_VERSION = 1.10.1;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -977,13 +978,14 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Nextcloud-Cookbook-iOS-Client-Info.plist"; INFOPLIST_FILE = "Nextcloud-Cookbook-iOS-Client-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cookbook; INFOPLIST_KEY_CFBundleDisplayName = Cookbook;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
INFOPLIST_KEY_NSRemindersFullAccessUsageDescription = "This app uses Reminders to save your grocery list items.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -999,7 +1001,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.10.1; MARKETING_VERSION = 1.10.1;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -1017,12 +1019,12 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-ClientTests"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
@@ -1040,12 +1042,12 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-ClientTests"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
@@ -1062,12 +1064,12 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-ClientUITests"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
@@ -1084,12 +1086,12 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-ClientUITests"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
@@ -1107,7 +1109,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
@@ -1121,7 +1123,7 @@
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook.shareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1144,7 +1146,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = EF2ABA36D9; DEVELOPMENT_TEAM = JGFU6788BP;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
@@ -1158,7 +1160,7 @@
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = eu.hogertz.cookbook.shareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@@ -101,7 +101,7 @@ class GroceryStateSyncManager {
// MARK: - Initial Sync // MARK: - Initial Sync
/// Pushes any local-only items and reconciles server items on app launch. /// Pushes any local-only items and reconciles server items on app launch.
func performInitialSync() async { func performSync() async {
guard let appState, let groceryManager else { return } guard let appState, let groceryManager else { return }
await loadPendingRemovals() await loadPendingRemovals()
@@ -208,7 +208,7 @@ class GroceryStateSyncManager {
// MARK: - Pending Removal Tracking // MARK: - Pending Removal Tracking
/// Records a recipe ID whose grocery items were fully removed, so that /// Records a recipe ID whose grocery items were fully removed, so that
/// `performInitialSync` can push the deletion even after the key disappears /// `performSync` can push the deletion even after the key disappears
/// from `groceryDict`. /// from `groceryDict`.
func trackPendingRemoval(recipeId: String) { func trackPendingRemoval(recipeId: String) {
pendingRemovalRecipeIds.insert(recipeId) pendingRemovalRecipeIds.insert(recipeId)

View File

@@ -60,28 +60,76 @@ class MealPlanSyncManager {
mealPlanManager.reconcileFromServer(serverAssignment: serverAssignment, recipeId: recipeId, recipeName: recipeName) mealPlanManager.reconcileFromServer(serverAssignment: serverAssignment, recipeId: recipeId, recipeName: recipeName)
} }
// MARK: - Initial Sync // MARK: - Full Sync
func performInitialSync() async { func performSync() async {
guard let appState, let mealPlanManager else { return } guard let appState, let mealPlanManager else { return }
let recipeIds = Array(mealPlanManager.entriesByDate.values.flatMap { $0 }.map(\.recipeId)) // Phase 1: Push locally-known meal plan state
let uniqueIds = Array(Set(recipeIds)) let localRecipeIds = Array(Set(
mealPlanManager.entriesByDate.values.flatMap { $0 }.map(\.recipeId)
for recipeId in uniqueIds { ))
guard let recipeIdInt = Int(recipeId) else { continue } for recipeId in localRecipeIds {
await pushMealPlanState(forRecipeId: recipeId) await pushMealPlanState(forRecipeId: recipeId)
}
if let serverRecipe = await appState.getRecipe(id: recipeIdInt, fetchMode: .onlyServer) { // Phase 2: Discover meal plan assignments from server
reconcileFromServer( let allRecipes = await appState.getRecipes()
serverAssignment: serverRecipe.mealPlanAssignment, let lastSync = UserSettings.shared.lastMealPlanSyncDate
// Filter to recipes modified since last sync
let recipesToCheck: [Recipe]
if let lastSync {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
recipesToCheck = allRecipes.filter { recipe in
guard let dateStr = recipe.dateModified,
let date = formatter.date(from: dateStr) else { return true }
return date > lastSync
}
} else {
recipesToCheck = allRecipes // First sync: check all
}
// Fetch details concurrently (max 5 parallel)
await withTaskGroup(of: (String, String, MealPlanAssignment?)?.self) { group in
var iterator = recipesToCheck.makeIterator()
let maxConcurrent = 5
var active = 0
while active < maxConcurrent, let recipe = iterator.next() {
active += 1
group.addTask {
guard let detail = await appState.getRecipe(
id: recipe.recipe_id, fetchMode: .onlyServer
) else { return nil }
return (String(recipe.recipe_id), detail.name, detail.mealPlanAssignment)
}
}
for await result in group {
if let (recipeId, recipeName, assignment) = result,
let assignment, !assignment.dates.isEmpty {
mealPlanManager.reconcileFromServer(
serverAssignment: assignment,
recipeId: recipeId, recipeId: recipeId,
recipeName: serverRecipe.name recipeName: recipeName
) )
} }
if let recipe = iterator.next() {
group.addTask {
guard let detail = await appState.getRecipe(
id: recipe.recipe_id, fetchMode: .onlyServer
) else { return nil }
return (String(recipe.recipe_id), detail.name, detail.mealPlanAssignment)
} }
} }
}
}
UserSettings.shared.lastMealPlanSyncDate = Date()
}
// MARK: - Merge Logic // MARK: - Merge Logic

View File

@@ -175,6 +175,12 @@ class UserSettings: ObservableObject {
} }
} }
@Published var lastMealPlanSyncDate: Date? {
didSet {
UserDefaults.standard.set(lastMealPlanSyncDate, forKey: "lastMealPlanSyncDate")
}
}
init() { init() {
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? "" self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
@@ -203,6 +209,7 @@ class UserSettings: ObservableObject {
self.categorySortAscending = UserDefaults.standard.object(forKey: "categorySortAscending") as? Bool ?? true self.categorySortAscending = UserDefaults.standard.object(forKey: "categorySortAscending") as? Bool ?? true
self.recipeSortMode = UserDefaults.standard.object(forKey: "recipeSortMode") as? String ?? RecipeSortMode.recentlyAdded.rawValue self.recipeSortMode = UserDefaults.standard.object(forKey: "recipeSortMode") as? String ?? RecipeSortMode.recentlyAdded.rawValue
self.recipeSortAscending = UserDefaults.standard.object(forKey: "recipeSortAscending") as? Bool ?? true self.recipeSortAscending = UserDefaults.standard.object(forKey: "recipeSortAscending") as? Bool ?? true
self.lastMealPlanSyncDate = UserDefaults.standard.object(forKey: "lastMealPlanSyncDate") as? Date
if authString == "" { if authString == "" {
if token != "" && username != "" { if token != "" && username != "" {

View File

@@ -99,12 +99,12 @@ struct MainView: View {
await groceryList.load() await groceryList.load()
groceryList.configureSyncManager(appState: appState) groceryList.configureSyncManager(appState: appState)
if UserSettings.shared.grocerySyncEnabled { if UserSettings.shared.grocerySyncEnabled {
await groceryList.syncManager?.performInitialSync() await groceryList.syncManager?.performSync()
} }
await mealPlan.load() await mealPlan.load()
mealPlan.configureSyncManager(appState: appState) mealPlan.configureSyncManager(appState: appState)
if UserSettings.shared.mealPlanSyncEnabled { if UserSettings.shared.mealPlanSyncEnabled {
await mealPlan.syncManager?.performInitialSync() await mealPlan.syncManager?.performSync()
} }
recipeViewModel.presentLoadingIndicator = false recipeViewModel.presentLoadingIndicator = false
} }

View File

@@ -73,6 +73,15 @@ struct MealPlanTabView: View {
} }
} }
.navigationTitle("Meal Plan") .navigationTitle("Meal Plan")
.refreshable {
await appState.getCategories()
for category in appState.categories {
await appState.getCategory(named: category.name, fetchMode: .preferServer)
}
if UserSettings.shared.mealPlanSyncEnabled {
await mealPlan.syncManager?.performSync()
}
}
.navigationDestination(for: Recipe.self) { recipe in .navigationDestination(for: Recipe.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe)) RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState) .environmentObject(appState)

View File

@@ -286,6 +286,9 @@ struct RecipeTabView: View {
await appState.getCategory(named: category.name, fetchMode: .preferServer) await appState.getCategory(named: category.name, fetchMode: .preferServer)
await appState.getCategoryImage(for: category.name) await appState.getCategoryImage(for: category.name)
} }
if UserSettings.shared.mealPlanSyncEnabled {
await mealPlan.syncManager?.performSync()
}
} }
} }
@@ -342,6 +345,7 @@ struct RecipeTabView: View {
fileprivate struct RecipeTabViewToolBar: ToolbarContent { fileprivate struct RecipeTabViewToolBar: ToolbarContent {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var viewModel: RecipeTabView.ViewModel @EnvironmentObject var viewModel: RecipeTabView.ViewModel
@EnvironmentObject var mealPlan: MealPlanManager
var body: some ToolbarContent { var body: some ToolbarContent {
// Top left menu toolbar item // Top left menu toolbar item
@@ -356,6 +360,9 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
await appState.getCategory(named: category.name, fetchMode: .preferServer) await appState.getCategory(named: category.name, fetchMode: .preferServer)
} }
await appState.updateAllRecipeDetails() await appState.updateAllRecipeDetails()
if UserSettings.shared.mealPlanSyncEnabled {
await mealPlan.syncManager?.performSync()
}
viewModel.presentLoadingIndicator = false viewModel.presentLoadingIndicator = false
} }
} label: { } label: {

View File

@@ -13,7 +13,5 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>NSRemindersFullAccessUsageDescription</key>
<string>This app uses Reminders to save your grocery list items.</string>
</dict> </dict>
</plist> </plist>