import ComposableArchitecture import DependenciesAdditions import EstimatedPressureDependency import SharedModels import Styleguide import SwiftUI import TCAExtras @Reducer public struct AirHandlerMeasurementForm { @ObservableState public struct State: Equatable { public var calculatedMeasurement: EquipmentMeasurement.AirHandler? public var focusedField: Field? public var measurement: EquipmentMeasurement.AirHandler public var updatedAirflow: Double? public var fanType: FanType public var ratedStaticPressures = RatedStaticPressures() public init( calculatedMeasurement: EquipmentMeasurement.AirHandler? = nil, focusedField: Field? = nil, measurement: EquipmentMeasurement.AirHandler = .init(), updatedAirflow: Double? = nil, fanType: FanType = .constantSpeed ) { self.calculatedMeasurement = calculatedMeasurement self.focusedField = focusedField self.measurement = measurement self.updatedAirflow = updatedAirflow self.fanType = fanType } public var isValid: Bool { return measurement.returnPlenumPressure != nil && measurement.postFilterPressure != nil && measurement.postCoilPressure != nil && measurement.supplyPlenumPressure != nil && measurement.airflow != nil && updatedAirflow != nil } public enum Field: String, Equatable, CaseIterable, FocusableField { case returnPlenumPressure case postFilterPressure case postCoilPressure case supplyPlenumPressure case airflow case updatedAirflow } } public enum Action: BindableAction, ViewAction { case binding(BindingAction) case receive(TaskResult) case view(View) @CasePathable public enum View { case infoButtonTapped case resetButtonTapped case submitField } } @Dependency(\.estimatedPressuresClient) var estimatedPressuresClient @Dependency(\.logger["\(Self.self)"]) var logger public var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding: return .none case let .receive(.success(calculatedMeasurement)): state.calculatedMeasurement = calculatedMeasurement return .none case .receive: return .none case let .view(action): switch action { case .infoButtonTapped: #warning("Fix me.") return .none case .resetButtonTapped: state.measurement = .init() state.updatedAirflow = nil return .none case .submitField: state.focusedField = state.focusedField?.next guard state.isValid else { return .none } return calculateEstimates(state: state) } } } .onFailure(case: \.receive, .log(logger: logger)) } private func calculateEstimates(state: State) -> Effect { .receive(action: \.receive) { let result = try await estimatedPressuresClient.estimatedPressure( for: .airHandler(state.measurement), at: state.updatedAirflow ?? 0 ) guard case let .airHandler(airHandler) = result else { return .none } return airHandler } } } @ViewAction(for: AirHandlerMeasurementForm.self) public struct AirHandlerMeasurementFormView: View { @FocusState private var focusedField: AirHandlerMeasurementForm.State.Field? @Bindable public var store: StoreOf public init( store: StoreOf ) { self.store = store self.focusedField = store.focusedField } public var body: some View { Form { Section { textField( "Return Plenum Pressure", value: $store.measurement.returnPlenumPressure ) .focused($focusedField, equals: .returnPlenumPressure) .onSubmit { send(.submitField) } .decimalPad() textField( "Post-Filter Pressure", value: $store.measurement.postFilterPressure ) .focused($focusedField, equals: .postFilterPressure) .onSubmit { send(.submitField) } .decimalPad() textField( "Post-Coil Pressure", value: $store.measurement.postCoilPressure ) .focused($focusedField, equals: .postCoilPressure) .onSubmit { send(.submitField) } .decimalPad() textField( "Supply Plenum Pressure", value: $store.measurement.supplyPlenumPressure ) .focused($focusedField, equals: .supplyPlenumPressure) .onSubmit { send(.submitField) } .decimalPad() textField( "Airflow", value: $store.measurement.airflow, fractionLength: 0 ) .focused($focusedField, equals: .airflow) .onSubmit { send(.submitField) } .numberPad() } header: { HStack { Text("System Measurements") Spacer() InfoButton { send(.infoButtonTapped) } .font(.title3) .labelStyle(.iconOnly) .buttonStyle(.plain) .foregroundStyle(Color.accentColor) } } Section { FanTypePicker(selection: $store.fanType) .pickerStyle(.segmented) textField( "Updated Airflow", value: $store.updatedAirflow, fractionLength: 0 ) .focused($focusedField, equals: .updatedAirflow) .onSubmit { send(.submitField) } .numberPad() } header: { Text("Estimate Settings") } footer: { HStack { Spacer() ResetButton { send(.resetButtonTapped) } Spacer() } .padding(.top) } if let calculatedMeasurement = store.calculatedMeasurement { Section { Text("Display calculated measurement here.") } } } .bind($focusedField, to: $store.focusedField) } private func textField( _ title: LocalizedStringKey, value: Binding, fractionLength: Int = 2 ) -> TextField { .init(title, value: value, fractionLength: fractionLength, prompt: Text(title)) } } #Preview { AirHandlerMeasurementFormView( store: Store(initialState: AirHandlerMeasurementForm.State()) { AirHandlerMeasurementForm() } ) }