WIP - Complete App refactoring

This commit is contained in:
VincentMeilinger
2025-05-26 15:52:24 +02:00
parent 29fd3c668b
commit 5acf3b9c4f
49 changed files with 1996 additions and 543 deletions

View File

@@ -9,17 +9,17 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Duration Section
/*
struct RecipeDurationSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
@State var presentPopover: Bool = false
var body: some View {
VStack(alignment: .leading) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
DurationView(time: viewModel.recipe.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.recipe.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.recipe.totalTime, title: LocalizedStringKey("Total time"))
}
if viewModel.editMode {
Button {
@@ -34,9 +34,9 @@ struct RecipeDurationSection: View {
.padding()
.popover(isPresented: $presentPopover) {
EditableDurationView(
prepTime: viewModel.observableRecipeDetail.prepTime,
cookTime: viewModel.observableRecipeDetail.cookTime,
totalTime: viewModel.observableRecipeDetail.totalTime
prepTime: viewModel.recipe.prepTime,
cookTime: viewModel.recipe.cookTime,
totalTime: viewModel.recipe.totalTime
)
}
}
@@ -94,10 +94,10 @@ fileprivate struct EditableDurationView: View {
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
}
.padding()
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
.onChange(of: prepTime.hourComponent) { updateTotalTime() }
.onChange(of: prepTime.minuteComponent) { updateTotalTime() }
.onChange(of: cookTime.hourComponent) { updateTotalTime() }
.onChange(of: cookTime.minuteComponent) { updateTotalTime() }
}
}
@@ -142,3 +142,5 @@ fileprivate struct TimePickerView: View {
.padding()
}
}
*/

View File

@@ -10,9 +10,9 @@ import SwiftUI
// MARK: - RecipeView Import Section
/*
struct RecipeImportSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var importRecipe: (String) async -> UserAlert?
var body: some View {
@@ -49,4 +49,4 @@ struct RecipeImportSection: View {
.padding(.top, 5)
}
}
*/

View File

@@ -9,23 +9,23 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Ingredients Section
/*
struct RecipeIngredientSection: View {
@EnvironmentObject var groceryList: GroceryList
@ObservedObject var viewModel: RecipeView.ViewModel
@Environment(CookbookState.self) var cookbookState
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
HStack {
Button {
withAnimation {
if groceryList.containsRecipe(viewModel.observableRecipeDetail.id) {
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
if cookbookState.groceryList.containsRecipe(viewModel.recipe.id) {
cookbookState.groceryList.deleteGroceryRecipe(viewModel.recipe.id)
} else {
groceryList.addItems(
viewModel.observableRecipeDetail.recipeIngredient,
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
cookbookState.groceryList.addItems(
viewModel.recipe.recipeIngredient,
toRecipe: viewModel.recipe.id,
recipeName: viewModel.recipe.name
)
}
}
@@ -45,26 +45,26 @@ struct RecipeIngredientSection: View {
.foregroundStyle(.secondary)
.bold()
ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier)
ServingPickerView(selectedServingSize: $viewModel.recipe.ingredientMultiplier)
}
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
ForEach(0..<viewModel.recipe.recipeIngredient.count, id: \.self) { ix in
IngredientListItem(
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
servings: $viewModel.observableRecipeDetail.ingredientMultiplier,
recipeYield: Double(viewModel.observableRecipeDetail.recipeYield),
recipeId: viewModel.observableRecipeDetail.id
ingredient: $viewModel.recipe.recipeIngredient[ix],
servings: $viewModel.recipe.ingredientMultiplier,
recipeYield: Double(viewModel.recipe.recipeYield),
recipeId: viewModel.recipe.id
) {
groceryList.addItem(
viewModel.observableRecipeDetail.recipeIngredient[ix],
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
cookbookState.groceryList.addItem(
viewModel.recipe.recipeIngredient[ix],
toRecipe: viewModel.recipe.id,
recipeName: viewModel.recipe.name
)
}
.padding(4)
}
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
if viewModel.recipe.ingredientMultiplier != Double(viewModel.recipe.recipeYield) {
HStack() {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.secondary)
@@ -83,14 +83,14 @@ struct RecipeIngredientSection: View {
}
}
.padding()
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
.animation(.easeInOut, value: viewModel.recipe.ingredientMultiplier)
}
}
// MARK: - RecipeIngredientSection List Item
fileprivate struct IngredientListItem: View {
@EnvironmentObject var groceryList: GroceryList
@Environment(CookbookState.self) var cookbookState
@Binding var ingredient: String
@Binding var servings: Double
@State var recipeYield: Double
@@ -110,7 +110,7 @@ fileprivate struct IngredientListItem: View {
var body: some View {
HStack(alignment: .top) {
if groceryList.containsItem(at: recipeId, item: ingredient) {
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
if #available(iOS 17.0, *) {
Image(systemName: "storefront")
.foregroundStyle(Color.green)
@@ -140,11 +140,11 @@ fileprivate struct IngredientListItem: View {
}
Spacer()
}
.onChange(of: servings) { newServings in
.onChange(of: servings) { _, newServings in
if recipeYield == 0 {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings)
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings)
} else {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield)
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings/recipeYield)
}
}
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
@@ -168,8 +168,8 @@ fileprivate struct IngredientListItem: View {
.onEnded { gesture in
withAnimation {
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
if groceryList.containsItem(at: recipeId, item: ingredient) {
groceryList.deleteItem(ingredient, fromRecipe: recipeId)
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
cookbookState.groceryList.deleteItem(ingredient, fromRecipe: recipeId)
} else {
addToGroceryListAction()
}
@@ -209,9 +209,12 @@ struct ServingPickerView: View {
.bold()
}
}
.onChange(of: selectedServingSize) { newValue in
.onChange(of: selectedServingSize) { _, newValue in
if newValue < 0 { selectedServingSize = 0 }
else if newValue > 100 { selectedServingSize = 100 }
}
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Instructions Section
/*
struct RecipeInstructionSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
@@ -19,8 +19,8 @@ struct RecipeInstructionSection: View {
SecondaryLabel(text: LocalizedStringKey("Instructions"))
Spacer()
}
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
ForEach(viewModel.recipe.recipeInstructions.indices, id: \.self) { ix in
RecipeInstructionListItem(instruction: $viewModel.recipe.recipeInstructions[ix], index: ix+1)
}
if viewModel.editMode {
Button {
@@ -56,4 +56,4 @@ fileprivate struct RecipeInstructionListItem: View {
.animation(.easeInOut, value: isSelected)
}
}
*/

View File

@@ -9,16 +9,16 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Keyword Section
/*
struct RecipeKeywordSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
let columns: [GridItem] = [ GridItem(.flexible(minimum: 50, maximum: 200), spacing: 5) ]
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
Group {
if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: $viewModel.observableRecipeDetail.keywords)
if !viewModel.recipe.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: $viewModel.recipe.keywords)
} else {
Text(LocalizedStringKey("No keywords."))
}
@@ -189,3 +189,4 @@ struct KeywordPickerView_Previews: PreviewProvider {
}
}
*/

View File

@@ -9,14 +9,14 @@ import Foundation
import SwiftUI
// MARK: - Recipe Metadata Section
/*
struct RecipeMetadataSection: View {
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel
@Environment(CookbookState.self) var cookbookState
@State var viewModel: RecipeView.ViewModel
@State var keywords: [RecipeKeyword] = []
var categories: [String] {
appState.categories.map({ category in category.name })
cookbookState.selectedAccountState.categories.map({ category in category.name })
}
@State var presentKeywordSheet: Bool = false
@@ -28,11 +28,11 @@ struct RecipeMetadataSection: View {
// Category
SecondaryLabel(text: "Category")
HStack {
TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory)
TextField("Category", text: $viewModel.recipe.recipeCategory)
.lineLimit(1)
.textFieldStyle(.roundedBorder)
Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
Picker("Choose", selection: $viewModel.recipe.recipeCategory) {
Text("").tag("")
ForEach(categories, id: \.self) { item in
Text(item)
@@ -45,10 +45,10 @@ struct RecipeMetadataSection: View {
// Keywords
SecondaryLabel(text: "Keywords")
if !viewModel.observableRecipeDetail.keywords.isEmpty {
if !viewModel.recipe.keywords.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
ForEach(viewModel.recipe.keywords, id: \.self) { keyword in
Text(keyword)
.padding(5)
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
@@ -70,11 +70,11 @@ struct RecipeMetadataSection: View {
Button {
presentServingsPopover.toggle()
} label: {
Text("\(viewModel.observableRecipeDetail.recipeYield) Serving(s)")
Text("\(viewModel.recipe.recipeYield) Serving(s)")
.lineLimit(1)
}
.popover(isPresented: $presentServingsPopover) {
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.observableRecipeDetail.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.recipe.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
}
}
}
@@ -82,7 +82,7 @@ struct RecipeMetadataSection: View {
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
.padding([.horizontal, .bottom], 5)
.sheet(isPresented: $presentKeywordSheet) {
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
KeywordPickerView(title: "Keywords", searchSuggestions: cookbookState.selectedAccountState.keywords, selection: $viewModel.recipe.keywords)
}
}
}
@@ -126,22 +126,22 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
// MARK: - RecipeView More Information Section
struct MoreInformationSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
VStack(alignment: .leading) {
if let dateCreated = viewModel.recipeDetail.dateCreated {
if let dateCreated = viewModel.recipe.dateCreated {
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: dateCreated) ?? "")")
}
if let dateModified = viewModel.recipeDetail.dateModified {
if let dateModified = viewModel.recipe.dateModified {
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: dateModified) ?? "")")
}
if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) {
if viewModel.recipe.url != "", let url = URL(string: viewModel.recipe.url ?? "") {
HStack(alignment: .top) {
Text("URL:")
Link(destination: url) {
Text(viewModel.observableRecipeDetail.url)
Text(viewModel.recipe.url ?? "")
}
}
}
@@ -157,3 +157,5 @@ struct MoreInformationSection: View {
.padding()
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Nutrition Section
/*
struct RecipeNutritionSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
@@ -28,7 +28,7 @@ struct RecipeNutritionSection: View {
} else if !nutritionEmpty() {
VStack(alignment: .leading) {
ForEach(Nutrition.allCases, id: \.self) { nutrition in
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
if let value = viewModel.recipe.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
HStack(alignment: .top) {
Text("\(nutrition.localizedDescription): \(value)")
.multilineTextAlignment(.leading)
@@ -43,7 +43,7 @@ struct RecipeNutritionSection: View {
}
} title: {
HStack {
if let servingSize = viewModel.observableRecipeDetail.nutrition["servingSize"] {
if let servingSize = viewModel.recipe.nutrition["servingSize"] {
SecondaryLabel(text: "Nutrition (\(servingSize))")
} else {
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
@@ -56,17 +56,19 @@ struct RecipeNutritionSection: View {
func binding(for key: String) -> Binding<String> {
Binding(
get: { viewModel.observableRecipeDetail.nutrition[key, default: ""] },
set: { viewModel.observableRecipeDetail.nutrition[key] = $0 }
get: { viewModel.recipe.nutrition[key, default: ""] },
set: { viewModel.recipe.nutrition[key] = $0 }
)
}
func nutritionEmpty() -> Bool {
for nutrition in Nutrition.allCases {
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
if let value = viewModel.recipe.nutrition[nutrition.dictKey] {
return false
}
}
return true
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Tool Section
/*
struct RecipeToolSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
@@ -20,7 +20,7 @@ struct RecipeToolSection: View {
Spacer()
}
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
RecipeListSection(list: $viewModel.recipe.tool)
if viewModel.editMode {
Button {
@@ -35,3 +35,5 @@ struct RecipeToolSection: View {
}
*/