diff --git a/Features/DonutShop/Sources/DonutShop/Models/Donut.swift b/Features/DonutShop/Sources/DonutShop/Models/Donut.swift index 6fbf093..84aba99 100644 --- a/Features/DonutShop/Sources/DonutShop/Models/Donut.swift +++ b/Features/DonutShop/Sources/DonutShop/Models/Donut.swift @@ -203,8 +203,8 @@ public extension Donut { id: Donut.all.count, name: String(localized: "New Donut", comment: "New donut-placeholder name."), dough: .plain, - glaze: .chocolate, - topping: .sprinkles + glaze: .none, + topping: .none ) } diff --git a/Features/DonutShop/Sources/DonutShop/Models/FoodTruckModel.swift b/Features/DonutShop/Sources/DonutShop/Models/FoodTruckModel.swift index 7c9652d..fa6b13b 100644 --- a/Features/DonutShop/Sources/DonutShop/Models/FoodTruckModel.swift +++ b/Features/DonutShop/Sources/DonutShop/Models/FoodTruckModel.swift @@ -9,13 +9,7 @@ import Decide final class FoodTruckState: AtomicState { - @Property public var donuts = Donut.all - @Mutable @Property public var editorDonut: Donut = Donut( - id: Donut.all.count, - name: String(localized: "New Donut", comment: "New donut-placeholder name."), - dough: .plain, - glaze: .chocolate, - topping: .sprinkles - ) - + @Mutable @Property public var donuts = Donut.all + @Mutable @Property public var selectedDonut: Donut = Donut.cosmos + } diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutDetailsCell.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutDetailsCell.swift new file mode 100644 index 0000000..5f2ca91 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutDetailsCell.swift @@ -0,0 +1,36 @@ +// +// DonutDetailsCell.swift +// +// +// Created by Anton Kolchunov on 14.08.23. +// + +import SwiftUI +import UIKit + +class DonutDetailsCell: UITableViewCell { + + static var identifier = "DonutDetailsCell" + + var hostingController: UIHostingController? + + func configure(_ swiftUIView: T) { + let view = AnyView(swiftUIView) + hostingController = UIHostingController(rootView: view) + guard let hostingController = hostingController else { return } + contentView.addSubview(hostingController.view) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + + override func prepareForReuse() { + super.prepareForReuse() + hostingController?.view.removeFromSuperview() + hostingController = nil + } +} diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutIngredientCell.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutIngredientCell.swift new file mode 100644 index 0000000..0c2887d --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/Cells/DonutIngredientCell.swift @@ -0,0 +1,40 @@ +// +// DonutIngredientCell.swift +// +// +// Created by Anton Kolchunov on 14.08.23. +// + +import Foundation +import UIKit + +class DonutIngredientCell: UITableViewCell { + + static var identifier = "DonutIngredientCell" + + let nameLabel = UILabel() + let valueLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + nameLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(nameLabel) + contentView.addSubview(valueLabel) + valueLabel.textColor = .gray + + NSLayoutConstraint.activate([ + nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + nameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), + valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12), + valueLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/DonutDetails.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/DonutDetails.swift new file mode 100644 index 0000000..b5f0346 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Details/DonutDetails.swift @@ -0,0 +1,167 @@ +// +// DonutDetails.swift +// +// +// Created by Anton Kolchunov on 11.08.23. +// + +import SwiftUI +import Decide + +struct DonutDetailsView: View { + @State private var isPresentingEditor = false + @Bind(\FoodTruckState.$selectedDonut) var detailsDonut + + var body: some View { + DonutDetails() + .edgesIgnoringSafeArea(.all) + .navigationTitle(detailsDonut.name) + .navigationBarTitleDisplayMode(.inline) + .toolbarRole(.editor) + .toolbar { + ToolbarItemGroup { + Button { + isPresentingEditor = true + } label: { + Label("Edit", systemImage: "pencil") + } + } + } + .fullScreenCover(isPresented: $isPresentingEditor) { + NavigationView { + DetailsDonutEditor() + } + } + } +} + +struct DonutDetails: UIViewControllerRepresentable { + typealias UIViewControllerType = DonutDetailsTableViewController + + func makeUIViewController(context: Context) -> DonutDetailsTableViewController { + DonutDetailsTableViewController() + } + + func updateUIViewController(_ uiViewController: DonutDetailsTableViewController, context: Context) { + // Don't need to do anything here + } +} + +struct DonutDetailsPreview: PreviewProvider { + struct Preview: View { + var body: some View { + NavigationView { + DonutDetailsView() + } + } + } + + static var previews: some View { + Preview() + } +} + +class DonutDetailsTableViewController: UITableViewController, EnvironmentObservingObject { + + @DefaultEnvironment var environment + @DefaultBind(\FoodTruckState.$selectedDonut) var donut + + func environmentDidUpdate() { + _ = donut // we need to read property in order to subscribe. + tableView.reloadData() + } + + let sections = ["", "Flavor profile", "Ingredients"] + let ingredientsTitles = ["Dough", "Glaze", "Topping"] + + init() { + super.init(style: .insetGrouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + environmentDidUpdate() + + tableView.register(DonutDetailsCell.self, forCellReuseIdentifier: DonutDetailsCell.identifier) + tableView.register(DonutIngredientCell.self, forCellReuseIdentifier: DonutIngredientCell.identifier) + tableView.contentInsetAdjustmentBehavior = .always + + self.tableView.isScrollEnabled = false + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 2 { + return 3 + } + return 1 + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section] + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 || indexPath.section == 1 { + let cell = tableView.dequeueReusableCell( + withIdentifier: DonutDetailsCell.identifier, + for: indexPath + ) as! DonutDetailsCell + let view = indexPath.section == 0 ? buildDonutView() : buildFlavorDetailsView() + cell.configure(view) + return cell + } + + let cell = buildIngredientsCell(for: indexPath) + return cell + } + + private func buildDonutView() -> any View { + return DonutView(donut: donut) + .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity) + .listRowInsets(.init()) + .padding(.horizontal, 20) + .padding(.vertical) + .background() + } + + private func buildFlavorDetailsView() -> any View { + return DonatFlavorDetailsView( + mostPotentFlavor: donut.flavors.mostPotent, + flavors: donut.flavors + ).padding(20) + } + + private func buildIngredientsCell(for indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell( + withIdentifier: DonutIngredientCell.identifier, + for: indexPath + ) as! DonutIngredientCell + let ingredient = getIngredient(for: indexPath) + cell.nameLabel.text = ingredientsTitles[indexPath.row] + cell.valueLabel.text = ingredient?.name ?? "None" + cell.selectionStyle = .none + return cell + } + + private func getIngredient(for indexPath: IndexPath) -> (any Ingredient)? { + switch indexPath.row { + case 0: + return donut.dough + case 1: + return donut.glaze + case 2: + return donut.topping + default: + fatalError() + } + } +} diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DetailsDonutEditor.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DetailsDonutEditor.swift new file mode 100644 index 0000000..13fd892 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DetailsDonutEditor.swift @@ -0,0 +1,34 @@ +// +// DetailsDonutEditor.swift +// +// +// Created by Anton Kolchunov on 15.08.23. +// + +import SwiftUI + +struct DetailsDonutEditor: View { + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationView { + DonutEditor() + } + .navigationViewStyle(StackNavigationViewStyle()) + .navigationBarTitle("Edit Donut") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: Button("Close") { + dismiss() + }) + } +} + +struct DetailsDonutEditor_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + DetailsDonutEditor() + } + } +} + + diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DonutEditor.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DonutEditor.swift index 3225243..10cafbf 100644 --- a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DonutEditor.swift +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DonutEditor.swift @@ -10,41 +10,27 @@ import Decide struct DonutEditor: View { - @Bind(\FoodTruckState.$editorDonut) var donut + @Bind(\FoodTruckState.$selectedDonut) var donut var body: some View { - ZStack { - WidthThresholdReader { proxy in - if proxy.isCompact { + WidthThresholdReader { proxy in + if proxy.isCompact { + Form { + donutViewer + editorContent + } + } else { + HStack(spacing: 0) { + donutViewer + Divider().ignoresSafeArea() Form { - donutViewer editorContent } - } else { - HStack(spacing: 0) { - donutViewer - Divider().ignoresSafeArea() - Form { - editorContent - } - .formStyle(.grouped) - .frame(width: 350) - } - } - } - } - .toolbar { - ToolbarTitleMenu { - Button { - - } label: { - Label("My Action", systemImage: "star") + .formStyle(.grouped) + .frame(width: 350) } } } - .navigationTitle(donut.name) - .navigationBarTitleDisplayMode(.inline) - .toolbarRole(.editor) } var donutViewer: some View { @@ -61,84 +47,20 @@ struct DonutEditor: View { Section("Donut") { TextField("Name", text: $donut.name, prompt: Text("Donut Name")) } - - Section("Flavor Profile") { - Grid { - let (topFlavor, topFlavorValue) = donut.flavors.mostPotent - ForEach(Flavor.allCases) { flavor in - let isTopFlavor = topFlavor == flavor - let flavorValue = max(donut.flavors[flavor], 0) - GridRow { - flavor.image - .foregroundStyle(isTopFlavor ? .primary : .secondary) - - Text(flavor.name) - .gridCellAnchor(.leading) - .foregroundStyle(isTopFlavor ? .primary : .secondary) - - Gauge(value: Double(flavorValue), in: 0...Double(topFlavorValue)) { - EmptyView() - } - .tint(isTopFlavor ? Color.accentColor : Color.secondary) - .labelsHidden() - - Text(flavorValue.formatted()) - .gridCellAnchor(.trailing) - .foregroundStyle(isTopFlavor ? .primary : .secondary) - } - } - } + + Section("Flavor profile") { + DonatFlavorDetailsView( + mostPotentFlavor: donut.flavors.mostPotent, + flavors: donut.flavors + ) } Section("Ingredients") { - Picker("Dough", selection: $donut.dough) { - ForEach(Donut.Dough.all) { dough in - Text(dough.name) - .tag(dough) - } - } - - Picker("Glaze", selection: $donut.glaze) { - Section { - Text("None") - .tag(nil as Donut.Glaze?) - } - ForEach(Donut.Glaze.all) { glaze in - Text(glaze.name) - .tag(glaze as Donut.Glaze?) - } - } - - Picker("Topping", selection: $donut.topping) { - Section { - Text("None") - .tag(nil as Donut.Topping?) - } - Section { - ForEach(Donut.Topping.other) { topping in - Text(topping.name) - .tag(topping as Donut.Topping?) - } - } - Section { - ForEach(Donut.Topping.lattices) { topping in - Text(topping.name) - .tag(topping as Donut.Topping?) - } - } - Section { - ForEach(Donut.Topping.lines) { topping in - Text(topping.name) - .tag(topping as Donut.Topping?) - } - } - Section { - ForEach(Donut.Topping.drizzles) { topping in - Text(topping.name) - .tag(topping as Donut.Topping?) - } - } - } + DoughPicker(dough: $donut.dough) + + GlazePicker(glaze: $donut.glaze) + + ToppingPicker(topping: $donut.topping) } } } @@ -146,13 +68,13 @@ struct DonutEditor: View { struct DonutEditor_Previews: PreviewProvider { struct Preview: View { var body: some View { - DonutEditor() + NavigationView { + DonutEditor() + } } } static var previews: some View { - NavigationStack { - Preview() - } + Preview() } } diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DoughPicker.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DoughPicker.swift new file mode 100644 index 0000000..aa62424 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/DoughPicker.swift @@ -0,0 +1,39 @@ +// +// DoughPicker.swift +// +// +// Created by Anton Kolchunov on 15.08.23. +// + +import SwiftUI + +struct DoughPicker: View { + + @Binding var dough: Donut.Dough + + var body: some View { + Picker("Dough", selection: $dough) { + ForEach(Donut.Dough.all) { dough in + Text(dough.name) + .tag(dough) + } + } + } +} + +struct DoughPicker_Previews: PreviewProvider { + struct Preview: View { + var body: some View { + Form() { + Section { + DoughPicker(dough: .constant(.plain)) + } + } + } + } + + static var previews: some View { + Preview() + } +} + diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/GlazePicker.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/GlazePicker.swift new file mode 100644 index 0000000..3f84445 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/GlazePicker.swift @@ -0,0 +1,44 @@ +// +// GlazePicker.swift +// +// +// Created by Anton Kolchunov on 15.08.23. +// + +import SwiftUI + +struct GlazePicker: View { + + @Binding var glaze: Donut.Glaze? + + var body: some View { + Picker("Glaze", selection: $glaze) { + Section { + Text("None") + .tag(nil as Donut.Glaze?) + } + ForEach(Donut.Glaze.all) { glaze in + Text(glaze.name) + .tag(glaze as Donut.Glaze?) + } + } + } +} + +struct GlazePicker_Previews: PreviewProvider { + + struct Preview: View { + var body: some View { + Form() { + Section { + GlazePicker(glaze: .constant(.blueberry)) + } + } + } + } + + static var previews: some View { + Preview() + } +} + diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/NewDonutEditor.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/NewDonutEditor.swift new file mode 100644 index 0000000..8ab7e6c --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/NewDonutEditor.swift @@ -0,0 +1,44 @@ +// +// NewDonutEditor.swift +// +// +// Created by Anton Kolchunov on 15.08.23. +// + +import Decide +import SwiftUI + +struct NewDonutEditor: View { + + @Observe(\FoodTruckState.$selectedDonut) var donut + + var body: some View { + DonutEditor() + .toolbar { + ToolbarTitleMenu { + Button { + + } label: { + Label("My Action", systemImage: "star") + } + } + } + .navigationTitle(donut.name) + .navigationBarTitleDisplayMode(.inline) + .toolbarRole(.editor) + } +} + +struct NewDonutEditor_Previews: PreviewProvider { + struct Preview: View { + var body: some View { + NavigationView { + NewDonutEditor() + } + } + } + + static var previews: some View { + Preview() + } +} diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/ToppingPicker.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/ToppingPicker.swift new file mode 100644 index 0000000..5760216 --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Editor/ToppingPicker.swift @@ -0,0 +1,63 @@ +// +// ToppingPicker.swift +// +// +// Created by Anton Kolchunov on 15.08.23. +// + +import SwiftUI + +struct ToppingPicker: View { + + @Binding var topping: Donut.Topping? + + var body: some View { + Picker("Topping", selection: $topping) { + Section { + Text("None") + .tag(nil as Donut.Topping?) + } + Section { + ForEach(Donut.Topping.other) { topping in + Text(topping.name) + .tag(topping as Donut.Topping?) + } + } + Section { + ForEach(Donut.Topping.lattices) { topping in + Text(topping.name) + .tag(topping as Donut.Topping?) + } + } + Section { + ForEach(Donut.Topping.lines) { topping in + Text(topping.name) + .tag(topping as Donut.Topping?) + } + } + Section { + ForEach(Donut.Topping.drizzles) { topping in + Text(topping.name) + .tag(topping as Donut.Topping?) + } + } + } + } +} + +struct ToppingPicker_Previews: PreviewProvider { + struct Preview: View { + var body: some View { + Form() { + Section { + ToppingPicker(topping: .constant(.blueberryDrizzle)) + } + } + } + } + + static var previews: some View { + Preview() + } +} + diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Gallery/DonutGallery.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Gallery/DonutGallery.swift index 2d232b8..7add67f 100644 --- a/Features/DonutShop/Sources/DonutShop/Views/Donut/Gallery/DonutGallery.swift +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Gallery/DonutGallery.swift @@ -10,8 +10,8 @@ import Decide struct DonutGallery: View { @Observe(\FoodTruckState.$donuts) var donuts - @Bind(\FoodTruckState.$editorDonut) var selectedDonut - + @Bind(\FoodTruckState.$selectedDonut) var selectedDonut + @State private var layout = BrowserLayout.grid @State private var selection = Set() @@ -43,7 +43,7 @@ struct DonutGallery: View { // .searchable(text: $searchText) .navigationTitle("Donuts") .navigationDestination(for: Donut.self) { donut in - DonutEditor() + DonutDetailsView() .onAppear { selectedDonut = donut } diff --git a/Features/DonutShop/Sources/DonutShop/Views/Donut/Render/DonatFlavorDetailsView.swift b/Features/DonutShop/Sources/DonutShop/Views/Donut/Render/DonatFlavorDetailsView.swift new file mode 100644 index 0000000..c0bddce --- /dev/null +++ b/Features/DonutShop/Sources/DonutShop/Views/Donut/Render/DonatFlavorDetailsView.swift @@ -0,0 +1,61 @@ +// +// DonatFlavorDetailsView.swift +// +// +// Created by Anton Kolchunov on 11.08.23. +// + +import Foundation +import SwiftUI + +struct DonatFlavorDetailsView: View { + + var mostPotentFlavor: (Flavor, Int) + var flavors: FlavorProfile + + var body: some View { + Grid { + let (topFlavor, topFlavorValue) = mostPotentFlavor + ForEach(Flavor.allCases) { flavor in + let isTopFlavor = topFlavor == flavor + let flavorValue = max(flavors[flavor], 0) + GridRow { + flavor.image + .foregroundStyle(isTopFlavor ? .primary : .secondary) + + Text(flavor.name) + .gridCellAnchor(.leading) + .foregroundStyle(isTopFlavor ? .primary : .secondary) + + Gauge(value: Double(flavorValue), in: 0...Double(topFlavorValue)) { + EmptyView() + } + .tint(isTopFlavor ? Color.accentColor : Color.secondary) + .labelsHidden() + + Text(flavorValue.formatted()) + .gridCellAnchor(.trailing) + .foregroundStyle(isTopFlavor ? .primary : .secondary) + } + } + } + } +} + +struct DonutFlavorDetailsPreview: PreviewProvider { + static var previews: some View { + Form { + DonatFlavorDetailsView( + mostPotentFlavor: (.sweet, 10), + flavors: FlavorProfile( + salty: 1, + sweet: 10, + bitter: 2, + sour: 3, + savory: 4, + spicy: 5 + ) + ) + } + } +}