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 allowEquipmentTypeSelection: Bool public var equipmentType: EquipmentType public var focusedField: Field? public var measurements: Measurements public init( allowEquipmentTypeSelection: Bool = true, destination: Destination.State? = nil, equipmentType: EquipmentType = .airHandler, focusedField: Field? = nil, measurements: Measurements = .init() ) { self.allowEquipmentTypeSelection = allowEquipmentTypeSelection self.destination = destination self.equipmentType = equipmentType self.focusedField = focusedField self.measurements = measurements } public var equipmentMeasurement: EquipmentMeasurement { measurements.equipmentMeasurement(type: equipmentType) } public var isValid: Bool { measurements.airflow != nil && measurements.returnPlenumPressure != nil && measurements.postFilterPressure != nil && measurements.coilPressure != nil && measurements.supplyPlenumPressure != nil } public struct Measurements: 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: CaseIterable, FocusableField, Hashable, 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: state.destination = .infoView(.init()) return .none case .resetButtonTapped: state.measurements = .init() return .none case .submitField: state.focusedField = state.focusedField?.next return .none } } } .ifLet(\.$destination, action: \.destination) } } 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 { if store.allowEquipmentTypeSelection { 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) .sheet( item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView) ){ store in NavigationStack { InfoView(store: store) } } } private func textField( for field: EquipmentMeasurementForm.State.Field ) -> some View { let value: Binding var fractionLength: Int = 2 switch field { case .returnPlenumPressure: value = $store.measurements.returnPlenumPressure case .postFilterPressure: value = $store.measurements.postFilterPressure case .coilPressure: value = $store.measurements.coilPressure case .supplyPlenumPressure: value = $store.measurements.supplyPlenumPressure case .airflow: value = $store.measurements.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() } } } fileprivate extension InfoViewFeature.State { init() { self.init( title: "Existing Measurements", body: """ Record the current static pressure and airflow measurements of the existing system. """ ) } } #Preview { NavigationStack { EquipmentMeasurementFormView( store: Store(initialState: EquipmentMeasurementForm.State()) { EquipmentMeasurementForm() } ) } }