import ComposableArchitecture import SharedModels import Styleguide import SwiftUI @Reducer public struct EstimationForm { public init() { } @ObservableState public struct State: Equatable, Sendable { @SharedReader public var existingMeasurement: EquipmentMeasurement? public let id: SharedPressureEstimationState.FlaggedEstimationContainer.ID? public var airflowSelection: AirflowSelection public var cfmTextField: Int? public var coolingCapacity: EquipmentMetadata.CoolingCapacity public var filterPressureDrop: Double? public var name: String public init( id: SharedPressureEstimationState.FlaggedEstimationContainer.ID? = nil, existingMeasurement: SharedReader, airflowSelection: AirflowSelection = .cfmPerTon, cfmTextField: Int? = nil, coolingCapacity: EquipmentMetadata.CoolingCapacity = .default, filterPressureDrop: Double? = nil, name: String = "" ) { self.id = id self._existingMeasurement = existingMeasurement self.airflowSelection = airflowSelection self.cfmTextField = cfmTextField self.filterPressureDrop = filterPressureDrop self.coolingCapacity = coolingCapacity self.name = name } public var airflow: Double { let cfmTextField = Double(self.cfmTextField ?? 0) switch airflowSelection { case .cfmPerTon: return Double(cfmTextField) * coolingCapacity.rawValue case .cfm: return Double(cfmTextField) } } public var isValid: Bool { !name.isEmpty && cfmTextField != nil } // Note: Keep in display order of the picker. public enum AirflowSelection: Hashable, CaseIterable, Identifiable, CustomStringConvertible { case cfmPerTon case cfm init( _ container: SharedPressureEstimationState.FlaggedEstimationContainer.EstimationState.CFMContainer ) { switch container { case .cfm: self = .cfm case .cfmPerTon: self = .cfmPerTon } } public var id: Self { self } public var description: String { switch self { case .cfm: return "CFM" case .cfmPerTon: return "CFM / Ton" } } } } public enum Action: BindableAction { case binding(BindingAction) } public var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding: return .none } } } } public struct EstimationFormView: View { @Bindable public var store: StoreOf public var body: some View { Form { Section("Estimation Name") { HStack { TextLabel("Name") .padding(.trailing, 40) TextField( "Name", text: $store.name, prompt: Text("Required") ) } } Section("Airflow") { VStack { CaseIterablePicker( "Airflow Type", selection: $store.airflowSelection ) .pickerStyle(.segmented) Grid(alignment: .leading, horizontalSpacing: 40) { if store.airflowSelection == .cfmPerTon { GridRow { HStack { TextLabel("Capacity") Spacer() CoolingCapacityPicker( selection: $store.coolingCapacity ) } .gridCellColumns(2) } } GridRow { HStack { TextLabel(store.airflowSelection.description) Spacer() TextField( "CFM / Ton", value: $store.cfmTextField, format: .number, prompt: Text("CFM") ) .frame(width: 100) .numberPad() } .gridCellColumns(2) } } } } if let existingsMeasurement = store.existingMeasurement, existingsMeasurement.hasFilterDrop { Section("Filter Pressure Drop") { HStack { TextLabel("Pressure Drop") Spacer() TextField( "Filter Drop", value: $store.filterPressureDrop, fractionLength: 2, prompt: Text("Optional") ) .frame(width: 100) .decimalPad() } } } } .applyFormStyle() } } fileprivate extension EquipmentMeasurement { var hasFilterDrop: Bool { switch self { case let .airHandler(airHandler): return airHandler.postFilterPressure > 0 case let .furnaceAndCoil(furnace): return furnace.postFilterPressure > 0 } } } #Preview { EstimationFormView( store: Store( initialState: EstimationForm.State( existingMeasurement: SharedReader( Shared(EquipmentMeasurement.mock(type: .airHandler)) ) ) ) { EstimationForm() } ) }