Fix settings page dismissing immediately by replacing multiple isPresented navigation destinations with value-based NavigationPath

The settings view was being popped immediately after push because multiple
navigationDestination(isPresented:) modifiers on the same view caused SwiftUI
to reset bindings when appState published changes. Replaced with a single
navigationDestination(for: SidebarDestination.self) using an explicit
NavigationStack(path:). Also fixed @ObservedObject -> @StateObject on
SettingsView.ViewModel, added AllRecipesListView/AllRecipesCategoryCardView,
and added translations for new strings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 02:20:38 +01:00
parent c8ddb098d1
commit 6824dbea6b
6 changed files with 439 additions and 83 deletions

View File

@@ -18,96 +18,130 @@ struct RecipeTabView: View {
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
private var showEditViewBinding: Binding<Bool> {
Binding(
get: { false },
set: { if $0 { viewModel.navigateToNewRecipe() } }
)
}
private var nonEmptyCategories: [Category] {
appState.categories.filter { $0.recipe_count > 0 }
}
private var totalRecipeCount: Int {
appState.categories.reduce(0) { $0 + $1.recipe_count }
}
var body: some View {
NavigationSplitView {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Recently Viewed
if !appState.recentRecipes.isEmpty {
RecentRecipesSection()
}
// Categories header
if !appState.categories.isEmpty {
Text("Categories")
.font(.title2)
.bold()
.padding(.horizontal)
}
// Category grid
if appState.categories.isEmpty {
VStack(spacing: 12) {
Image(systemName: "book.closed")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("No cookbooks found")
.font(.headline)
.foregroundStyle(.secondary)
Text("Pull to refresh or check your server connection.")
.font(.subheadline)
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
NavigationStack(path: $viewModel.sidebarPath) {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Recently Viewed
if !appState.recentRecipes.isEmpty {
RecentRecipesSection()
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
LazyVGrid(columns: gridColumns, spacing: 12) {
ForEach(appState.categories) { category in
Button {
viewModel.selectedCategory = category
if horizontalSizeClass == .compact {
viewModel.navigateToCategory = true
}
} label: {
CategoryCardView(
category: category,
isSelected: viewModel.selectedCategory?.name == category.name
)
}
.buttonStyle(.plain)
// Categories header
if !nonEmptyCategories.isEmpty {
Text("Categories")
.font(.title2)
.bold()
.padding(.horizontal)
}
// Category grid
if nonEmptyCategories.isEmpty {
VStack(spacing: 12) {
Image(systemName: "book.closed")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("No cookbooks found")
.font(.headline)
.foregroundStyle(.secondary)
Text("Pull to refresh or check your server connection.")
.font(.subheadline)
.foregroundStyle(.tertiary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
LazyVGrid(columns: gridColumns, spacing: 12) {
// All Recipes card
if totalRecipeCount > 0 {
Button {
viewModel.navigateToAllRecipes()
} label: {
AllRecipesCategoryCardView()
}
.buttonStyle(.plain)
}
ForEach(nonEmptyCategories) { category in
Button {
if horizontalSizeClass == .compact {
viewModel.navigateToCategory(category)
} else {
viewModel.selectedCategory = category
viewModel.showAllRecipesInDetail = false
}
} label: {
CategoryCardView(
category: category,
isSelected: !viewModel.showAllRecipesInDetail && viewModel.selectedCategory?.name == category.name
)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
}
.padding(.horizontal)
}
.padding(.vertical)
}
.navigationTitle("Recipes")
.toolbar {
RecipeTabViewToolBar()
}
.navigationDestination(for: SidebarDestination.self) { destination in
switch destination {
case .settings:
SettingsView()
.environmentObject(appState)
case .newRecipe:
RecipeView(viewModel: RecipeView.ViewModel())
.environmentObject(appState)
.environmentObject(groceryList)
case .category(let category):
RecipeListView(
categoryName: category.name,
showEditView: showEditViewBinding
)
.id(category.id)
.environmentObject(appState)
.environmentObject(groceryList)
case .allRecipes:
AllRecipesListView(showEditView: showEditViewBinding)
.environmentObject(appState)
.environmentObject(groceryList)
}
}
.padding(.vertical)
}
.navigationTitle("Recipes")
.toolbar {
RecipeTabViewToolBar()
}
.navigationDestination(isPresented: $viewModel.presentSettingsView) {
SettingsView()
.environmentObject(appState)
}
.navigationDestination(isPresented: $viewModel.presentEditView) {
RecipeView(viewModel: RecipeView.ViewModel())
.environmentObject(appState)
.environmentObject(groceryList)
}
.navigationDestination(for: Recipe.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState)
.environmentObject(groceryList)
}
.navigationDestination(isPresented: $viewModel.navigateToCategory) {
if let category = viewModel.selectedCategory {
RecipeListView(
categoryName: category.name,
showEditView: $viewModel.presentEditView
)
.id(category.id)
.environmentObject(appState)
.environmentObject(groceryList)
.navigationDestination(for: Recipe.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState)
.environmentObject(groceryList)
}
}
} detail: {
NavigationStack {
if let category = viewModel.selectedCategory {
if viewModel.showAllRecipesInDetail {
AllRecipesListView(showEditView: showEditViewBinding)
} else if let category = viewModel.selectedCategory {
RecipeListView(
categoryName: category.name,
showEditView: $viewModel.presentEditView
showEditView: showEditViewBinding
)
.id(category.id)
}
@@ -133,16 +167,42 @@ struct RecipeTabView: View {
}
}
enum SidebarDestination: Hashable {
case settings
case newRecipe
case category(Category)
case allRecipes
}
class ViewModel: ObservableObject {
@Published var presentEditView: Bool = false
@Published var presentSettingsView: Bool = false
@Published var navigateToCategory: Bool = false
@Published var sidebarPath = NavigationPath()
@Published var presentLoadingIndicator: Bool = false
@Published var presentConnectionPopover: Bool = false
@Published var serverConnection: Bool = false
@Published var selectedCategory: Category? = nil
@Published var showAllRecipesInDetail: Bool = false
func navigateToSettings() {
sidebarPath.append(SidebarDestination.settings)
}
func navigateToNewRecipe() {
sidebarPath.append(SidebarDestination.newRecipe)
}
func navigateToCategory(_ category: Category) {
selectedCategory = category
showAllRecipesInDetail = false
sidebarPath.append(SidebarDestination.category(category))
}
func navigateToAllRecipes() {
selectedCategory = nil
showAllRecipesInDetail = true
sidebarPath.append(SidebarDestination.allRecipes)
}
}
}
@@ -173,7 +233,7 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
}
Button {
viewModel.presentSettingsView = true
viewModel.navigateToSettings()
} label: {
Text("Settings")
Image(systemName: "gearshape")
@@ -214,7 +274,7 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
ToolbarItem(placement: .topBarTrailing) {
Button {
Logger.view.debug("Add new recipe")
viewModel.presentEditView = true
viewModel.navigateToNewRecipe()
} label: {
Image(systemName: "plus.circle.fill")
}