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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user