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

@@ -22,6 +22,8 @@
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; }; A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; };
B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE012CF0000100000001 /* CategoryCardView.swift */; }; B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE012CF0000100000001 /* CategoryCardView.swift */; };
B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */; }; B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */; };
B1C0DE062CF0000300000003 /* AllRecipesCategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */; };
B1C0DE082CF0000400000004 /* AllRecipesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C0DE072CF0000400000004 /* AllRecipesListView.swift */; };
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; }; A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; };
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; }; A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; };
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; }; A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; };
@@ -105,6 +107,8 @@
A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = "<group>"; }; A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = "<group>"; };
B1C0DE012CF0000100000001 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = "<group>"; }; B1C0DE012CF0000100000001 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = "<group>"; };
B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRecipesSection.swift; sourceTree = "<group>"; }; B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentRecipesSection.swift; sourceTree = "<group>"; };
B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesCategoryCardView.swift; sourceTree = "<group>"; };
B1C0DE072CF0000400000004 /* AllRecipesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllRecipesListView.swift; sourceTree = "<group>"; };
A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; }; A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; }; A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; }; A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
@@ -386,6 +390,8 @@
A70171BD2AB4987900064C43 /* RecipeListView.swift */, A70171BD2AB4987900064C43 /* RecipeListView.swift */,
A70171C12AB498C600064C43 /* RecipeCardView.swift */, A70171C12AB498C600064C43 /* RecipeCardView.swift */,
B1C0DE012CF0000100000001 /* CategoryCardView.swift */, B1C0DE012CF0000100000001 /* CategoryCardView.swift */,
B1C0DE052CF0000300000003 /* AllRecipesCategoryCardView.swift */,
B1C0DE072CF0000400000004 /* AllRecipesListView.swift */,
B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */, B1C0DE032CF0000200000002 /* RecentRecipesSection.swift */,
A70171BF2AB498A900064C43 /* RecipeView.swift */, A70171BF2AB498A900064C43 /* RecipeView.swift */,
A97506112B920D8100E86029 /* RecipeViewSections */, A97506112B920D8100E86029 /* RecipeViewSections */,
@@ -604,6 +610,8 @@
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */, A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */, B1C0DE022CF0000100000001 /* CategoryCardView.swift in Sources */,
B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */, B1C0DE042CF0000200000002 /* RecentRecipesSection.swift in Sources */,
B1C0DE062CF0000300000003 /* AllRecipesCategoryCardView.swift in Sources */,
B1C0DE082CF0000400000004 /* AllRecipesListView.swift in Sources */,
A70171842AA8E71900064C43 /* MainView.swift in Sources */, A70171842AA8E71900064C43 /* MainView.swift in Sources */,
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */, A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */,
A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */, A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */,

View File

@@ -583,6 +583,28 @@
} }
} }
}, },
"All Recipes" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alle Rezepte"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Todas las recetas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Toutes les recettes"
}
}
}
},
"An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites." : { "An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -2577,6 +2599,28 @@
} }
} }
}, },
"No recipes found" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Keine Rezepte gefunden"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "No se encontraron recetas"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aucune recette trouvée"
}
}
}
},
"No results found" : { "No results found" : {
"comment" : "A message indicating that no recipes were found for the current search query.", "comment" : "A message indicating that no recipes were found for the current search query.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,

View File

@@ -0,0 +1,152 @@
//
// AllRecipesCategoryCardView.swift
// Nextcloud Cookbook iOS Client
//
import SwiftUI
struct AllRecipesCategoryCardView: View {
@EnvironmentObject var appState: AppState
@State private var mosaicImages: [UIImage] = []
private var totalRecipeCount: Int {
appState.categories.reduce(0) { $0 + $1.recipe_count }
}
var body: some View {
ZStack(alignment: .bottomLeading) {
// 2x2 image mosaic or gradient fallback
if mosaicImages.count >= 4 {
mosaicGrid
} else {
gradientFallback
}
// Bottom scrim with text
VStack(alignment: .leading, spacing: 2) {
Spacer()
LinearGradient(
colors: [.clear, .black.opacity(0.95)],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 60)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: 2) {
Text("All Recipes")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(.white)
.lineLimit(1)
Text("\(totalRecipeCount) recipes")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white.opacity(0.85))
}
.padding(.horizontal, 10)
.padding(.bottom, 8)
}
}
}
.frame(height: 140)
.clipShape(RoundedRectangle(cornerRadius: 17))
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
.task {
await loadMosaicImages()
}
}
private func loadMosaicImages() async {
// Ensure recipes are loaded for each category (they may not be yet)
for category in appState.categories {
if appState.recipes[category.name] == nil || appState.recipes[category.name]!.isEmpty {
await appState.getCategory(named: category.name, fetchMode: .preferLocal)
}
}
// Collect all recipes across categories, shuffled for variety
var allRecipes: [Recipe] = []
for category in appState.categories {
if let recipes = appState.recipes[category.name] {
allRecipes.append(contentsOf: recipes)
}
}
allRecipes.shuffle()
// Filter to recipes that have an image URL, then pick 4
var candidates: [Recipe] = []
var seenIds: Set<Int> = []
for recipe in allRecipes {
guard let url = recipe.imageUrl, !url.isEmpty else { continue }
guard !seenIds.contains(recipe.recipe_id) else { continue }
seenIds.insert(recipe.recipe_id)
candidates.append(recipe)
if candidates.count >= 4 { break }
}
var images: [UIImage] = []
for recipe in candidates {
if let image = await appState.getImage(
id: recipe.recipe_id,
size: .THUMB,
fetchMode: UserSettings.shared.storeThumb ? .preferLocal : .onlyServer
) {
images.append(image)
}
}
guard !images.isEmpty else { return }
// Cycle to fill 4 slots if fewer than 4 unique images
var filled: [UIImage] = []
for i in 0..<4 {
filled.append(images[i % images.count])
}
mosaicImages = filled
}
private var mosaicGrid: some View {
VStack(spacing: 1) {
HStack(spacing: 1) {
imageCell(mosaicImages[safe: 0])
imageCell(mosaicImages[safe: 1])
}
HStack(spacing: 1) {
imageCell(mosaicImages[safe: 2])
imageCell(mosaicImages[safe: 3])
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 140, maxHeight: 140)
.clipped()
}
private func imageCell(_ image: UIImage?) -> some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.clipped()
} else {
Color.gray
}
}
}
private var gradientFallback: some View {
LinearGradient(
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 140, maxHeight: 140)
.overlay(alignment: .center) {
Image(systemName: "square.grid.2x2.fill")
.font(.system(size: 36))
.foregroundStyle(.white.opacity(0.5))
}
}
}
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@@ -0,0 +1,92 @@
//
// AllRecipesListView.swift
// Nextcloud Cookbook iOS Client
//
import SwiftUI
struct AllRecipesListView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryList
@Binding var showEditView: Bool
@State private var allRecipes: [Recipe] = []
@State private var searchText: String = ""
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
var body: some View {
Group {
let recipes = recipesFiltered()
if !recipes.isEmpty {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
Text("\(recipes.count) recipes")
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(.horizontal)
LazyVGrid(columns: gridColumns, spacing: 12) {
ForEach(recipes, id: \.recipe_id) { recipe in
NavigationLink(value: recipe) {
RecipeCardView(recipe: recipe)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal)
}
.padding(.vertical)
}
} else {
VStack(spacing: 16) {
Image(systemName: "fork.knife")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("No recipes found")
.font(.headline)
.foregroundStyle(.secondary)
Button {
Task {
allRecipes = await appState.getRecipes()
}
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
.bold()
}
.buttonStyle(.bordered)
.tint(.nextcloudBlue)
}.padding()
}
}
.searchable(text: $searchText, prompt: "Search recipes/keywords")
.navigationTitle(String(localized: "All Recipes"))
.navigationDestination(for: Recipe.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState)
.environmentObject(groceryList)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showEditView = true
} label: {
Image(systemName: "plus.circle.fill")
}
}
}
.task {
allRecipes = await appState.getRecipes()
}
.refreshable {
allRecipes = await appState.getRecipes()
}
}
private func recipesFiltered() -> [Recipe] {
guard !searchText.isEmpty else { return allRecipes }
return allRecipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased()) ||
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased()))
}
}
}

View File

@@ -14,7 +14,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@ObservedObject var userSettings = UserSettings.shared @ObservedObject var userSettings = UserSettings.shared
@ObservedObject var viewModel = ViewModel() @StateObject var viewModel = ViewModel()
var body: some View { var body: some View {
Form { Form {

View File

@@ -18,96 +18,130 @@ struct RecipeTabView: View {
private let gridColumns = [GridItem(.adaptive(minimum: 160), spacing: 12)] 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 { var body: some View {
NavigationSplitView { NavigationSplitView {
ScrollView { NavigationStack(path: $viewModel.sidebarPath) {
VStack(alignment: .leading, spacing: 20) { ScrollView {
// Recently Viewed VStack(alignment: .leading, spacing: 20) {
if !appState.recentRecipes.isEmpty { // Recently Viewed
RecentRecipesSection() 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)
} }
.frame(maxWidth: .infinity)
.padding(.top, 40) // Categories header
} else { if !nonEmptyCategories.isEmpty {
LazyVGrid(columns: gridColumns, spacing: 12) { Text("Categories")
ForEach(appState.categories) { category in .font(.title2)
Button { .bold()
viewModel.selectedCategory = category .padding(.horizontal)
if horizontalSizeClass == .compact { }
viewModel.navigateToCategory = true
} // Category grid
} label: { if nonEmptyCategories.isEmpty {
CategoryCardView( VStack(spacing: 12) {
category: category, Image(systemName: "book.closed")
isSelected: viewModel.selectedCategory?.name == category.name .font(.system(size: 48))
) .foregroundStyle(.secondary)
} Text("No cookbooks found")
.buttonStyle(.plain) .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) .navigationDestination(for: Recipe.self) { recipe in
} RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.navigationTitle("Recipes") .environmentObject(appState)
.toolbar { .environmentObject(groceryList)
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)
} }
} }
} detail: { } detail: {
NavigationStack { NavigationStack {
if let category = viewModel.selectedCategory { if viewModel.showAllRecipesInDetail {
AllRecipesListView(showEditView: showEditViewBinding)
} else if let category = viewModel.selectedCategory {
RecipeListView( RecipeListView(
categoryName: category.name, categoryName: category.name,
showEditView: $viewModel.presentEditView showEditView: showEditViewBinding
) )
.id(category.id) .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 { class ViewModel: ObservableObject {
@Published var presentEditView: Bool = false @Published var sidebarPath = NavigationPath()
@Published var presentSettingsView: Bool = false
@Published var navigateToCategory: Bool = false
@Published var presentLoadingIndicator: Bool = false @Published var presentLoadingIndicator: Bool = false
@Published var presentConnectionPopover: Bool = false @Published var presentConnectionPopover: Bool = false
@Published var serverConnection: Bool = false @Published var serverConnection: Bool = false
@Published var selectedCategory: Category? = nil @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 { Button {
viewModel.presentSettingsView = true viewModel.navigateToSettings()
} label: { } label: {
Text("Settings") Text("Settings")
Image(systemName: "gearshape") Image(systemName: "gearshape")
@@ -214,7 +274,7 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
Logger.view.debug("Add new recipe") Logger.view.debug("Add new recipe")
viewModel.presentEditView = true viewModel.navigateToNewRecipe()
} label: { } label: {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
} }