From 6ec3eacb8d0bb2eb10ed4c75046c1f6d6fe51ce9 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 4 Jun 2024 22:57:18 -0400 Subject: [PATCH] feat: Begins equipment measurement form --- .../EquipmentMeasurementForm.swift | 337 ++++++++++++++++++ Sources/SharedModels/EquipmentType.swift | 4 +- Sources/Styleguide/TextField+Precision.swift | 14 + 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift diff --git a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift new file mode 100644 index 0000000..cd1d9ed --- /dev/null +++ b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift @@ -0,0 +1,337 @@ +import ComposableArchitecture +import SharedModels +import Styleguide +import SwiftUI + +@Reducer +public struct EquipmentMeasurementForm { + + public init() { } + + @Reducer(state: .equatable) + public enum Destination { + case infoView(InfoViewFeature) + } + + @ObservableState + public struct State: Equatable { + @Presents public var destination: Destination.State? + public var equipmentType: EquipmentType + public var focusedField: Field? + public var fields: FormFields + + public init( + destination: Destination.State? = nil, + equipmentType: EquipmentType = .airHandler, + focusedField: Field? = nil, + fields: FormFields = .init() + ) { + self.destination = destination + self.equipmentType = equipmentType + self.focusedField = focusedField + self.fields = fields + } + + public var equipmentMeasurement: EquipmentMeasurement { + fields.equipmentMeasurement(type: equipmentType) + } + + public var isValid: Bool { + fields.airflow != nil + && fields.returnPlenumPressure != nil + && fields.postFilterPressure != nil + && fields.coilPressure != nil + && fields.supplyPlenumPressure != nil + } + + public struct FormFields: Equatable { + public var airflow: Double? + public var returnPlenumPressure: Double? + public var postFilterPressure: Double? + public var coilPressure: Double? + public var supplyPlenumPressure: Double? + + public init( + airflow: Double? = nil, + returnPlenumPressure: Double? = nil, + postFilterPressure: Double? = nil, + coilPressure: Double? = nil, + supplyPlenumPressure: Double? = nil + ) { + self.airflow = airflow + self.returnPlenumPressure = returnPlenumPressure + self.postFilterPressure = postFilterPressure + self.coilPressure = coilPressure + self.supplyPlenumPressure = supplyPlenumPressure + } + + public init( + equipmentMeasurement: EquipmentMeasurement + ) { + switch equipmentMeasurement { + case let .airHandler(equipment): + self.init( + airflow: equipment.airflow, + returnPlenumPressure: equipment.returnPlenumPressure, + postFilterPressure: equipment.postFilterPressure, + coilPressure: equipment.postCoilPressure, + supplyPlenumPressure: equipment.supplyPlenumPressure + ) + case let .furnaceAndCoil(equipment): + self.init( + airflow: equipment.airflow, + returnPlenumPressure: equipment.returnPlenumPressure, + postFilterPressure: equipment.postFilterPressure, + coilPressure: equipment.preCoilPressure, + supplyPlenumPressure: equipment.supplyPlenumPressure + ) + } + } + + public func equipmentMeasurement(type: EquipmentType) -> EquipmentMeasurement { + switch type { + + case .airHandler: + return .airHandler(.init( + airflow: airflow, + returnPlenumPressure: returnPlenumPressure, + postFilterPressure: postFilterPressure, + postCoilPressure: coilPressure, + supplyPlenumPressure: supplyPlenumPressure + )) + + case .furnaceAndCoil: + return .furnaceAndCoil(.init( + airflow: airflow, + returnPlenumPressure: returnPlenumPressure, + postFilterPressure: postFilterPressure, + preCoilPressure: coilPressure, + supplyPlenumPressure: supplyPlenumPressure + )) + } + } + } + + public enum Field: Hashable, CaseIterable, FocusableField, Identifiable { + case returnPlenumPressure + case postFilterPressure + case coilPressure + case supplyPlenumPressure + case airflow + + public var id: Self { self } + + } + } + + public enum Action: BindableAction, ViewAction { + case binding(BindingAction) + case destination(PresentationAction) + case view(View) + + @CasePathable + public enum View { + case infoButtonTapped + case resetButtonTapped + case submitField + } + } + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + + case .binding: + return .none + + case .destination: + return .none + + + case let .view(action): + switch action { + + case .infoButtonTapped: + return .none + + case .resetButtonTapped: + state.fields = .init() + return .none + + case .submitField: + state.focusedField = state.focusedField?.next + return .none + + } + } + } + .ifLet(\.$destination, action: \.destination) + } +} + +fileprivate extension Store where State == EquipmentMeasurementForm.State { + + func prompt( + field: EquipmentMeasurementForm.State.Field + ) -> String { + let label = label(field: field) + guard field != .airflow else { return label } + return "\(label) Pressure" + } + + func label( + field: EquipmentMeasurementForm.State.Field + ) -> String { + switch field { + case .returnPlenumPressure: + return "Return" + case .postFilterPressure: + return "Post-Filter" + case .coilPressure: + switch state.equipmentType { + case .airHandler: + return "Post-Coil" + case .furnaceAndCoil: + return "Pre-Coil" + } + case .supplyPlenumPressure: + return "Supply" + case .airflow: + return "Airflow" + } + } + + var pressureFields: [EquipmentMeasurementForm.State.Field] { + EquipmentMeasurementForm.State.Field.allCases.filter { + $0 != .airflow + } + } +} + +@ViewAction(for: EquipmentMeasurementForm.self) +public struct EquipmentMeasurementFormView: View { + + @Bindable public var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + Form { + Section { + + } header: { + Text("Equipment Type") + } footer: { + Picker("Equipment Type", selection: $store.equipmentType) { + ForEach(EquipmentType.allCases) { + Text($0.description) + .tag($0) + } + } + .pickerStyle(.segmented) + .labelsHidden() + } + + Section { + Grid(alignment: .leading, horizontalSpacing: 40) { + ForEach(store.pressureFields) { field in + GridRow { + TextLabel(store.label(field: field)) + textField(for: field) + } + } + } + } header: { + HStack { + Text("Static Measurements") + Spacer() + InfoButton { send(.infoButtonTapped) } + } + } + + Section { + Grid(alignment: .leading, horizontalSpacing: 60) { + GridRow { + TextLabel(store.label(field: .airflow)) + textField(for: .airflow) + } + } + } footer: { + HStack { + Spacer() + ResetButton { send(.resetButtonTapped) } + .padding(.top) + Spacer() + } + } + } + .textLabelStyle(.boldSecondary) + .textFieldStyle(.roundedBorder) + } + + private func textField( + for field: EquipmentMeasurementForm.State.Field + ) -> some View { + let value: Binding + var fractionLength: Int = 2 + + switch field { + case .returnPlenumPressure: + value = $store.fields.returnPlenumPressure + case .postFilterPressure: + value = $store.fields.postFilterPressure + case .coilPressure: + value = $store.fields.coilPressure + case .supplyPlenumPressure: + value = $store.fields.supplyPlenumPressure + case .airflow: + value = $store.fields.airflow + fractionLength = 0 + } + + return textField( + title: store.prompt(field: field), + value: value, + fractionLength: fractionLength, + numberPad: field == .airflow + ) + } + + @ViewBuilder + private func textField( + title: String, + value: Binding, + fractionLength: Int, + numberPad: Bool + ) -> some View { + if numberPad { + TextField( + title, + value: value, + fractionLength: fractionLength, + prompt: Text(title) + ) + .numberPad() + } else { + TextField( + title, + value: value, + fractionLength: fractionLength, + prompt: Text(title) + ) + .decimalPad() + } + } + +} + +#Preview { + EquipmentMeasurementFormView( + store: Store(initialState: EquipmentMeasurementForm.State()) { + EquipmentMeasurementForm() + } + ) +} diff --git a/Sources/SharedModels/EquipmentType.swift b/Sources/SharedModels/EquipmentType.swift index 5665784..cb5e4c6 100644 --- a/Sources/SharedModels/EquipmentType.swift +++ b/Sources/SharedModels/EquipmentType.swift @@ -1,9 +1,11 @@ import Foundation -public enum EquipmentType: Equatable, CaseIterable, CustomStringConvertible { +public enum EquipmentType: Equatable, CaseIterable, CustomStringConvertible, Identifiable { case airHandler case furnaceAndCoil + public var id: Self { self } + public var description: String { switch self { case .airHandler: diff --git a/Sources/Styleguide/TextField+Precision.swift b/Sources/Styleguide/TextField+Precision.swift index 8bd688c..2fecd5d 100644 --- a/Sources/Styleguide/TextField+Precision.swift +++ b/Sources/Styleguide/TextField+Precision.swift @@ -15,5 +15,19 @@ extension TextField where Label == Text { prompt: prompt ) } + + public init( + _ titleKey: S, + value: Binding, + fractionLength: Int, + prompt: Text? = nil + ) { + self.init( + titleKey, + value: value, + format: .number.precision(.fractionLength(fractionLength)), + prompt: prompt + ) + } }