Updated RecipeView

This commit is contained in:
VincentMeilinger
2024-03-01 14:17:24 +01:00
parent d3e0366ce6
commit 744ea76a34
41 changed files with 588 additions and 383 deletions

View File

@@ -10,71 +10,85 @@ import SwiftUI
struct RecipeView: View {
@ObservedObject var appState: AppState
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var appState: AppState
@StateObject var viewModel: ViewModel
@State var imageHeight: CGFloat = 350
private enum CoordinateSpaces {
case scrollView
}
var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading) {
ZStack {
VStack(spacing: 0) {
ParallaxHeader(
coordinateSpace: CoordinateSpaces.scrollView,
defaultHeight: imageHeight
) {
if let recipeImage = viewModel.recipeImage {
Image(uiImage: recipeImage)
.resizable()
.scaledToFill()
.frame(maxHeight: 300)
.clipped()
}
}.animation(.easeInOut, value: viewModel.recipeImage)
}
LazyVStack (alignment: .leading) {
VStack(alignment: .leading) {
HStack {
EditableText(text: $viewModel.recipeDetail.name, editMode: $viewModel.editMode)
EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
.font(.title)
.bold()
.padding()
.onDisappear {
viewModel.showTitle = true
}
.onAppear {
viewModel.showTitle = false
}
Spacer()
if let isDownloaded = viewModel.isDownloaded {
Spacer()
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
.foregroundColor(.secondary)
.padding()
}
}
}.padding([.top, .horizontal])
if viewModel.recipeDetail.description != "" || viewModel.editMode {
EditableText(text: $viewModel.recipeDetail.description, editMode: $viewModel.editMode, lineLimit: 0...10, axis: .vertical)
if viewModel.observableRecipeDetail.description != "" || viewModel.editMode {
EditableText(text: $viewModel.observableRecipeDetail.description, editMode: $viewModel.editMode, titleKey: "Description", lineLimit: 0...5, axis: .vertical)
.padding([.bottom, .horizontal])
}
// Recipe Body Section
RecipeDurationSection(viewModel: viewModel)
Divider()
RecipeDurationSection(viewModel: appState, recipeDetail: viewModel.recipeDetail)
if viewModel.editMode {
RecipeMetadataSection(viewModel: viewModel)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if(!viewModel.recipeDetail.recipeIngredient.isEmpty || viewModel.editMode) {
if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) {
RecipeIngredientSection(viewModel: viewModel)
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(.ultraThinMaterial))
.padding(5)
}
if(!viewModel.recipeDetail.recipeInstructions.isEmpty || viewModel.editMode) {
if(!viewModel.observableRecipeDetail.recipeInstructions.isEmpty || viewModel.editMode) {
RecipeInstructionSection(viewModel: viewModel)
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(.ultraThinMaterial))
.padding(5)
}
if(!viewModel.recipeDetail.tool.isEmpty || viewModel.editMode) {
if(!viewModel.observableRecipeDetail.tool.isEmpty || viewModel.editMode) {
RecipeToolSection(viewModel: viewModel)
}
RecipeNutritionSection(viewModel: viewModel)
RecipeKeywordSection(viewModel: viewModel)
MoreInformationSection(recipeDetail: viewModel.recipeDetail)
if !viewModel.editMode {
RecipeKeywordSection(viewModel: viewModel)
}
MoreInformationSection(viewModel: viewModel)
}
}.padding(.horizontal, 5)
}
.padding(.horizontal, 5)
.background(Rectangle().foregroundStyle(.background).shadow(radius: 5).mask(Rectangle().padding(.top, -20)))
}
}
.coordinateSpace(name: CoordinateSpaces.scrollView)
.ignoresSafeArea(.container, edges: .top)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar {
@@ -87,7 +101,11 @@ struct RecipeView: View {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
// TODO: POST edited recipe
viewModel.editMode = false
if viewModel.newRecipe {
presentationMode.wrappedValue.dismiss()
} else {
viewModel.editMode = false
}
}
}
} else {
@@ -116,36 +134,44 @@ struct RecipeView: View {
}
}
.sheet(isPresented: $viewModel.presentShareSheet) {
ShareView(recipeDetail: viewModel.recipeDetail,
ShareView(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(),
recipeImage: viewModel.recipeImage,
presentShareSheet: $viewModel.presentShareSheet)
}
.task {
viewModel.recipeDetail = await appState.getRecipe(
id: viewModel.recipe.recipe_id,
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
) ?? RecipeDetail.error
viewModel.recipeImage = await appState.getImage(
id: viewModel.recipe.recipe_id,
size: .FULL,
fetchMode: UserSettings.shared.storeImages ? .preferLocal : .onlyServer
)
if viewModel.recipe.storedLocally == nil {
viewModel.recipe.storedLocally = appState.recipeDetailExists(recipeId: viewModel.recipe.recipe_id)
// Load recipe detail
if !viewModel.newRecipe {
// For existing recipes, load the recipeDetail and image
let recipeDetail = await appState.getRecipe(
id: viewModel.recipe.recipe_id,
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
) ?? RecipeDetail.error
viewModel.setupView(recipeDetail: recipeDetail)
// Show download badge
if viewModel.recipe.storedLocally == nil {
viewModel.recipe.storedLocally = appState.recipeDetailExists(recipeId: viewModel.recipe.recipe_id)
}
viewModel.isDownloaded = viewModel.recipe.storedLocally
// Load recipe image
viewModel.recipeImage = await appState.getImage(
id: viewModel.recipe.recipe_id,
size: .FULL,
fetchMode: UserSettings.shared.storeImages ? .preferLocal : .onlyServer
)
if let image = viewModel.recipeImage {
imageHeight = image.size.height < 350 ? image.size.height : 350
} else {
imageHeight = 100
}
} else {
// Prepare view for a new recipe
viewModel.setupView(recipeDetail: RecipeDetail())
viewModel.editMode = true
viewModel.isDownloaded = false
}
viewModel.isDownloaded = viewModel.recipe.storedLocally
}
.refreshable {
viewModel.recipeDetail = await appState.getRecipe(
id: viewModel.recipe.recipe_id,
fetchMode: UserSettings.shared.storeRecipes ? .preferServer : .onlyServer
) ?? RecipeDetail.error
viewModel.recipeImage = await appState.getImage(
id: viewModel.recipe.recipe_id,
size: .FULL,
fetchMode: UserSettings.shared.storeImages ? .preferServer : .onlyServer
)
}
.onAppear {
if UserSettings.shared.keepScreenAwake {
@@ -155,21 +181,29 @@ struct RecipeView: View {
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
}
.onChange(of: viewModel.editMode) { newValue in
if newValue && appState.allKeywords.isEmpty {
Task {
appState.allKeywords = await appState.getKeywords(fetchMode: .preferServer).sorted(by: { a, b in
a.recipe_count > b.recipe_count
})
}
}
}
}
// MARK: - RecipeView ViewModel
class ViewModel: ObservableObject {
@Published var observableRecipeDetail: ObservableRecipeDetail = ObservableRecipeDetail()
@Published var recipeDetail: RecipeDetail = RecipeDetail.error
@Published var recipeImage: UIImage? = nil
@Published var editMode: Bool = false
@Published var presentShareSheet: Bool = false
@Published var showTitle: Bool = false
@Published var isDownloaded: Bool? = nil
@Published var keywords: [String] = []
@Published var nutrition: [String] = []
var newRecipe: Bool = false
var recipe: Recipe
var sharedURL: URL? = nil
@@ -179,64 +213,154 @@ struct RecipeView: View {
self.recipe = recipe
}
func setupView(recipeDetail: RecipeDetail) {
self.keywords = recipeDetail.keywords.components(separatedBy: ",")
init() {
self.newRecipe = true
self.recipe = Recipe(
name: String(localized: "New Recipe"),
keywords: "",
dateCreated: "",
dateModified: "",
imageUrl: "",
imagePlaceholderUrl: "",
recipe_id: 0)
}
func setupView(recipeDetail: RecipeDetail) {
self.recipeDetail = recipeDetail
self.observableRecipeDetail = ObservableRecipeDetail(recipeDetail)
}
}
}
// MARK: - Duration Section
// MARK: - Recipe Metadata Section
fileprivate struct RecipeDurationSection: View {
@ObservedObject var viewModel: AppState
@State var recipeDetail: RecipeDetail
struct RecipeMetadataSection: View {
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel
@State var categories: [String] = []
@State var keywords: [RecipeKeyword] = []
@State var presentKeywordPopover: Bool = false
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 250), alignment: .leading)]) {
if let prepTime = recipeDetail.prepTime, let time = DurationComponents.ptToText(prepTime) {
VStack(alignment: .leading) {
HStack {
SecondaryLabel(text: LocalizedStringKey("Preparation"))
Spacer()
}
Text(time)
.lineLimit(1)
}.padding()
}
/*
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
TimerView(timer: viewModel.getTimer(forRecipe: recipeDetail.id, duration: DurationComponents.fromPTString(cookTime)))
.padding()
}
*/
VStack(alignment: .leading) {
CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category")
if let cookTime = recipeDetail.cookTime, let time = DurationComponents.ptToText(cookTime) {
VStack(alignment: .leading) {
HStack {
SecondaryLabel(text: LocalizedStringKey("Cooking"))
Spacer()
}
Text(time)
.lineLimit(1)
}.padding()
}
SecondaryLabel(text: "Keywords")
.padding()
if let totalTime = recipeDetail.totalTime, let time = DurationComponents.ptToText(totalTime) {
VStack(alignment: .leading) {
HStack {
SecondaryLabel(text: LocalizedStringKey("Total time"))
Spacer()
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
Text(keyword)
}
Text(time)
.lineLimit(1)
}.padding()
}
}.padding(.horizontal)
Button {
presentKeywordPopover.toggle()
} label: {
Text("Edit keywords")
Image(systemName: "chevron.right")
}
.padding(.horizontal)
}
.task {
categories = appState.categories.map({ category in category.name })
}
.sheet(isPresented: $presentKeywordPopover) {
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
}
}
}
struct CategoryPickerView: View {
@Binding var items: [String]
@Binding var input: String
@State private var pickerChoice: String = ""
var titleKey: LocalizedStringKey
var body: some View {
VStack(alignment: .leading) {
SecondaryLabel(text: "Category")
.padding([.top, .horizontal])
HStack {
TextField(titleKey, text: $input)
.lineLimit(1)
.textFieldStyle(.roundedBorder)
.padding()
.onSubmit {
pickerChoice = ""
}
Picker("Select Item", selection: $pickerChoice) {
Text("").tag("")
ForEach(items, id: \.self) { item in
Text(item)
}
}
.pickerStyle(.menu)
.padding()
.onChange(of: pickerChoice) { newValue in
if pickerChoice != "" {
input = newValue
}
}
}
}
.onAppear {
pickerChoice = input
}
}
}
// MARK: - Duration Section
fileprivate struct RecipeDurationSection: View {
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel
var body: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
DurationView(time: viewModel.observableRecipeDetail.prepTime.displayString, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.observableRecipeDetail.cookTime.displayString, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime.displayString, title: LocalizedStringKey("Total time"))
}
}
}
struct DurationView: View {
@State var time: String
@State var title: LocalizedStringKey
var body: some View {
VStack(alignment: .leading) {
HStack {
SecondaryLabel(text: title)
Spacer()
}
HStack {
Image(systemName: "clock")
.foregroundStyle(.secondary)
Text(time)
.lineLimit(1)
}
}
.padding()
}
}
// MARK: - Nutrition Section
fileprivate struct RecipeNutritionSection: View {
@@ -244,7 +368,7 @@ fileprivate struct RecipeNutritionSection: View {
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
Group {
VStack(alignment: .leading) {
if viewModel.editMode {
ForEach(Nutrition.allCases, id: \.self) { nutrition in
HStack {
@@ -254,29 +378,25 @@ fileprivate struct RecipeNutritionSection: View {
.lineLimit(1)
}
}
} else {
if !viewModel.recipeDetail.nutrition.isEmpty {
VStack(alignment: .leading) {
ForEach(Nutrition.allCases, id: \.self) { nutrition in
if let value = viewModel.recipeDetail.nutrition[nutrition.dictKey] {
HStack(alignment: .top) {
Text(nutrition.localizedDescription)
Text(":")
Text(value)
.multilineTextAlignment(.leading)
}
.padding(4)
} 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 {
HStack(alignment: .top) {
Text("\(nutrition.localizedDescription): \(value)")
.multilineTextAlignment(.leading)
}
.padding(4)
}
}
} else {
Text(LocalizedStringKey("No nutritional information."))
}
} else {
Text(LocalizedStringKey("No nutritional information."))
}
}
} title: {
HStack {
if let servingSize = viewModel.recipeDetail.nutrition["servingSize"] {
if let servingSize = viewModel.observableRecipeDetail.nutrition["servingSize"] {
SecondaryLabel(text: "Nutrition (\(servingSize))")
} else {
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
@@ -289,10 +409,19 @@ fileprivate struct RecipeNutritionSection: View {
func binding(for key: String) -> Binding<String> {
Binding(
get: { viewModel.recipeDetail.nutrition[key, default: ""] },
set: { viewModel.recipeDetail.nutrition[key] = $0 }
get: { viewModel.observableRecipeDetail.nutrition[key, default: ""] },
set: { viewModel.observableRecipeDetail.nutrition[key] = $0 }
)
}
func nutritionEmpty() -> Bool {
for nutrition in Nutrition.allCases {
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
return false
}
}
return true
}
}
@@ -300,26 +429,17 @@ fileprivate struct RecipeNutritionSection: View {
fileprivate struct RecipeKeywordSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var keywords: [String] = []
let columns: [GridItem] = [ GridItem(.flexible(minimum: 50, maximum: 200), spacing: 5) ]
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
Group {
if !keywords.isEmpty || viewModel.editMode {
//RecipeListSection(list: keywords)
EditableStringList(items: $keywords, editMode: $viewModel.editMode, titleKey: "Keyword", lineLimit: 0...1, axis: .horizontal) {
RecipeListSection(list: keywords)
}
if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: viewModel.observableRecipeDetail.keywords)
} else {
Text(LocalizedStringKey("No keywords."))
}
}
.onAppear {
self.keywords = viewModel.recipeDetail.keywords.components(separatedBy: ",")
}
.onDisappear {
viewModel.recipeDetail.keywords = keywords.joined(separator: ",")
}
} title: {
HStack {
SecondaryLabel(text: LocalizedStringKey("Keywords"))
@@ -334,18 +454,18 @@ fileprivate struct RecipeKeywordSection: View {
// MARK: - More Information Section
fileprivate struct MoreInformationSection: View {
let recipeDetail: RecipeDetail
@ObservedObject var viewModel: RecipeView.ViewModel
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
VStack(alignment: .leading) {
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateCreated) ?? "")")
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: recipeDetail.dateModified) ?? "")")
if recipeDetail.url != "", let url = URL(string: recipeDetail.url) {
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateCreated) ?? "")")
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: viewModel.recipeDetail.dateModified) ?? "")")
if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) {
HStack() {
Text("URL:")
Link(destination: url) {
Text(recipeDetail.url)
Text(viewModel.observableRecipeDetail.url)
}
}
}
@@ -402,23 +522,23 @@ fileprivate struct RecipeIngredientSection: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
if viewModel.recipeDetail.recipeYield == 0 {
if viewModel.observableRecipeDetail.recipeYield == 0 {
SecondaryLabel(text: LocalizedStringKey("Ingredients"))
} else if viewModel.recipeDetail.recipeYield == 1 {
} else if viewModel.observableRecipeDetail.recipeYield == 1 {
SecondaryLabel(text: LocalizedStringKey("Ingredients per serving"))
} else {
SecondaryLabel(text: LocalizedStringKey("Ingredients for \(viewModel.recipeDetail.recipeYield) servings"))
SecondaryLabel(text: LocalizedStringKey("Ingredients for \(viewModel.observableRecipeDetail.recipeYield) servings"))
}
Spacer()
Button {
withAnimation {
if groceryList.containsRecipe(viewModel.recipeDetail.id) {
groceryList.deleteGroceryRecipe(viewModel.recipeDetail.id)
if groceryList.containsRecipe(viewModel.observableRecipeDetail.id) {
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
} else {
groceryList.addItems(
viewModel.recipeDetail.recipeIngredient,
toRecipe: viewModel.recipeDetail.id,
recipeName: viewModel.recipeDetail.name
ReorderableItem.items(viewModel.observableRecipeDetail.recipeIngredient),
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
)
}
}
@@ -431,13 +551,13 @@ fileprivate struct RecipeIngredientSection: View {
}
}
EditableStringList(items: $viewModel.recipeDetail.recipeIngredient, editMode: $viewModel.editMode, titleKey: "Ingredient", lineLimit: 0...1, axis: .horizontal) {
ForEach(0..<viewModel.recipeDetail.recipeIngredient.count, id: \.self) { ix in
IngredientListItem(ingredient: viewModel.recipeDetail.recipeIngredient[ix], recipeId: viewModel.recipeDetail.id) {
EditableStringList(items: $viewModel.observableRecipeDetail.recipeIngredient, editMode: $viewModel.editMode, titleKey: "Ingredient", lineLimit: 0...1, axis: .horizontal) {
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
IngredientListItem(ingredient: viewModel.observableRecipeDetail.recipeIngredient[ix], recipeId: viewModel.observableRecipeDetail.id) {
groceryList.addItem(
viewModel.recipeDetail.recipeIngredient[ix],
toRecipe: viewModel.recipeDetail.id,
recipeName: viewModel.recipeDetail.name
viewModel.observableRecipeDetail.recipeIngredient[ix].item,
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
)
}
.padding(4)
@@ -449,7 +569,7 @@ fileprivate struct RecipeIngredientSection: View {
fileprivate struct IngredientListItem: View {
@EnvironmentObject var groceryList: GroceryList
@State var ingredient: String
@State var ingredient: ReorderableItem<String>
@State var recipeId: String
let addToGroceryListAction: () -> Void
@State var isSelected: Bool = false
@@ -461,7 +581,7 @@ fileprivate struct IngredientListItem: View {
var body: some View {
HStack(alignment: .top) {
if groceryList.containsItem(at: recipeId, item: ingredient) {
if groceryList.containsItem(at: recipeId, item: ingredient.item) {
if #available(iOS 17.0, *) {
Image(systemName: "storefront")
.foregroundStyle(Color.green)
@@ -476,7 +596,7 @@ fileprivate struct IngredientListItem: View {
Image(systemName: "circle")
}
Text("\(ingredient)")
Text("\(ingredient.item)")
.multilineTextAlignment(.leading)
.lineLimit(5)
Spacer()
@@ -502,8 +622,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 groceryList.containsItem(at: recipeId, item: ingredient.item) {
groceryList.deleteItem(ingredient.item, fromRecipe: recipeId)
} else {
addToGroceryListAction()
}
@@ -531,9 +651,9 @@ fileprivate struct RecipeInstructionSection: View {
SecondaryLabel(text: LocalizedStringKey("Instructions"))
Spacer()
}
EditableStringList(items: $viewModel.recipeDetail.recipeInstructions, editMode: $viewModel.editMode, titleKey: "Instruction", lineLimit: 0...15, axis: .vertical) {
ForEach(0..<viewModel.recipeDetail.recipeInstructions.count, id: \.self) { ix in
RecipeInstructionListItem(instruction: viewModel.recipeDetail.recipeInstructions[ix], index: ix+1)
EditableStringList(items: $viewModel.observableRecipeDetail.recipeInstructions, editMode: $viewModel.editMode, titleKey: "Instruction", lineLimit: 0...15, axis: .vertical) {
ForEach(0..<viewModel.observableRecipeDetail.recipeInstructions.count, id: \.self) { ix in
RecipeInstructionListItem(instruction: viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
}
}
}.padding()
@@ -541,7 +661,7 @@ fileprivate struct RecipeInstructionSection: View {
}
fileprivate struct RecipeInstructionListItem: View {
@State var instruction: String
@State var instruction: ReorderableItem<String>
@State var index: Int
@State var isSelected: Bool = false
@@ -549,7 +669,7 @@ fileprivate struct RecipeInstructionListItem: View {
HStack(alignment: .top) {
Text("\(index)")
.monospaced()
Text(instruction)
Text(instruction.item)
}.padding(4)
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
.onTapGesture {
@@ -571,8 +691,8 @@ fileprivate struct RecipeToolSection: View {
SecondaryLabel(text: "Tools")
Spacer()
}
EditableStringList(items: $viewModel.recipeDetail.tool, editMode: $viewModel.editMode, titleKey: "Tool", lineLimit: 0...1, axis: .horizontal) {
RecipeListSection(list: viewModel.recipeDetail.tool)
EditableStringList(items: $viewModel.observableRecipeDetail.tool, editMode: $viewModel.editMode, titleKey: "Tool", lineLimit: 0...1, axis: .horizontal) {
RecipeListSection(list: ReorderableItem.items(viewModel.observableRecipeDetail.tool))
}
}.padding()
}
@@ -601,31 +721,24 @@ fileprivate struct EditableText: View {
fileprivate struct EditableStringList<Content: View>: View {
@Binding var items: [String]
@Binding var items: [ReorderableItem<String>]
@Binding var editMode: Bool
@State var titleKey: LocalizedStringKey = ""
@State var lineLimit: ClosedRange<Int> = 0...50
@State var axis: Axis = .vertical
@State var editableItems: [ReorderableItem<String>] = []
var content: () -> Content
var body: some View {
if editMode {
VStack {
ReorderableForEach(items: $editableItems, defaultItem: ReorderableItem(item: "")) { ix, item in
TextField("", text: $editableItems[ix].item, axis: axis)
ReorderableForEach(items: $items, defaultItem: ReorderableItem(item: "")) { ix, item in
TextField("", text: $items[ix].item, axis: axis)
.textFieldStyle(.roundedBorder)
.lineLimit(lineLimit)
}
}
.onAppear {
editableItems = ReorderableItem.list(items: items)
}
.onDisappear {
items = ReorderableItem.items(editableItems)
}
.transition(.slide)
} else {
content()
}