diff --git a/Sources/EstimatedPressureDependency/Key.swift b/Sources/EstimatedPressureDependency/Key.swift index c9746d4..fe4d256 100644 --- a/Sources/EstimatedPressureDependency/Key.swift +++ b/Sources/EstimatedPressureDependency/Key.swift @@ -66,15 +66,15 @@ public struct EstimatedPressureDependency { extension EstimatedPressureDependency { private func estimatedPressure( - existingPressure: Positive, - existingAirflow: Double, - targetAirflow: Double + existingPressure: Positive, + existingAirflow: Positive, + targetAirflow: Positive ) async throws -> Positive { try await self.estimatedPressure( .init( - existingPressure: existingPressure.positiveValue ?? 0, - existingAirflow: existingAirflow, - targetAirflow: targetAirflow + existingPressure: existingPressure.positiveValue, + existingAirflow: existingAirflow.positiveValue, + targetAirflow: targetAirflow.positiveValue ) ) } @@ -83,78 +83,75 @@ extension EstimatedPressureDependency { equipmentMeasurement: EquipmentMeasurement, airflow updatedAirflow: Double ) async throws -> EquipmentMeasurement { + let updatedAirflow = Positive(updatedAirflow) + switch equipmentMeasurement { case let .airHandler(airHandler): - guard let existingAirflow = airHandler.airflow else { - throw InvalidAirflow() - } return try await .airHandler( .init( airflow: updatedAirflow, + manufacturersIncludedFilterPressureDrop: airHandler.$manufacturersIncludedFilterPressureDrop, returnPlenumPressure: self.estimatedPressure( existingPressure: airHandler.$returnPlenumPressure, - existingAirflow: existingAirflow, + existingAirflow: airHandler.$airflow, targetAirflow: updatedAirflow ), postFilterPressure: self.estimatedPressure( existingPressure: airHandler.$postFilterPressure, - existingAirflow: existingAirflow, + existingAirflow: airHandler.$airflow, targetAirflow: updatedAirflow ), postCoilPressure: self.estimatedPressure( existingPressure: airHandler.$postCoilPressure, - existingAirflow: existingAirflow, + existingAirflow: airHandler.$airflow, targetAirflow: updatedAirflow ), supplyPlenumPressure: self.estimatedPressure( existingPressure: airHandler.$supplyPlenumPressure, - existingAirflow: existingAirflow, + existingAirflow: airHandler.$airflow, targetAirflow: updatedAirflow ) ) ) case let .furnaceAndCoil(furnaceAndCoil): - guard let existingAirflow = furnaceAndCoil.airflow else { - throw InvalidAirflow() - } return try await .furnaceAndCoil( .init( airflow: updatedAirflow, + manufacturersIncludedFilterPressureDrop: furnaceAndCoil.$manufacturersIncludedFilterPressureDrop, returnPlenumPressure: self.estimatedPressure( existingPressure: furnaceAndCoil.$returnPlenumPressure, - existingAirflow: existingAirflow, + existingAirflow: furnaceAndCoil.$airflow, targetAirflow: updatedAirflow ), postFilterPressure: self.estimatedPressure( existingPressure: furnaceAndCoil.$postFilterPressure, - existingAirflow: existingAirflow, + existingAirflow: furnaceAndCoil.$airflow, targetAirflow: updatedAirflow ), preCoilPressure: self.estimatedPressure( existingPressure: furnaceAndCoil.$preCoilPressure, - existingAirflow: existingAirflow, + existingAirflow: furnaceAndCoil.$airflow, targetAirflow: updatedAirflow ), supplyPlenumPressure: self.estimatedPressure( existingPressure: furnaceAndCoil.$supplyPlenumPressure, - existingAirflow: existingAirflow, + existingAirflow: furnaceAndCoil.$airflow, targetAirflow: updatedAirflow ) ) ) - } } public func estimatedPressure( equipmentMeasurement: EquipmentMeasurement, - airflow updatedAirflow: Double, + airflow updatedAirflow: Positive, filterPressureDrop: Positive ) async throws -> EquipmentMeasurement { let estimate = try await estimatedPressure( equipmentMeasurement: equipmentMeasurement, - airflow: updatedAirflow + airflow: updatedAirflow.positiveValue ) switch estimate { @@ -183,7 +180,7 @@ extension EstimatedPressureDependency { return try await estimatedPressure( equipmentMeasurement: equipmentMeasurement, airflow: updatedAirflow, - filterPressureDrop: filterPressureDrop + filterPressureDrop: Positive(filterPressureDrop.positiveValue) ) } } @@ -219,42 +216,6 @@ extension EstimatedPressureDependency: DependencyKey { } } -fileprivate extension EquipmentMeasurement.AirHandler { - init( - airflow: Double? = nil, - returnPlenumPressure: Positive, - postFilterPressure: Positive, - postCoilPressure: Positive, - supplyPlenumPressure: Positive - ) { - self.init( - airflow: airflow, - returnPlenumPressure: returnPlenumPressure.positiveValue, - postFilterPressure: postFilterPressure.positiveValue, - postCoilPressure: postCoilPressure.positiveValue, - supplyPlenumPressure: supplyPlenumPressure.positiveValue - ) - } -} - -fileprivate extension EquipmentMeasurement.FurnaceAndCoil { - init( - airflow: Double? = nil, - returnPlenumPressure: Positive, - postFilterPressure: Positive, - preCoilPressure: Positive, - supplyPlenumPressure: Positive - ) { - self.init( - airflow: airflow, - returnPlenumPressure: returnPlenumPressure.positiveValue, - postFilterPressure: postFilterPressure.positiveValue, - preCoilPressure: preCoilPressure.positiveValue, - supplyPlenumPressure: supplyPlenumPressure.positiveValue - ) - } -} - struct InvalidAirflow: Error { } enum LessThanZeroError: Error { diff --git a/Sources/FlaggedViews/FlaggedEquipmentMeasurementView.swift b/Sources/FlaggedViews/FlaggedEquipmentMeasurementView.swift index b8abd9b..122594d 100644 --- a/Sources/FlaggedViews/FlaggedEquipmentMeasurementView.swift +++ b/Sources/FlaggedViews/FlaggedEquipmentMeasurementView.swift @@ -145,6 +145,7 @@ fileprivate extension EquipmentMeasurement.FlaggedMeasurement { #if DEBUG private let equipmentMeasurement = EquipmentMeasurement.airHandler(.init( airflow: 1600, + manufacturersIncludedFilterPressureDrop: 0.1, returnPlenumPressure: 0.37, postFilterPressure: 0.78, postCoilPressure: 0.9, diff --git a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift index 68ae453..845ed36 100644 --- a/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentMeasurementForm.swift @@ -1,9 +1,9 @@ import ComposableArchitecture +import DependenciesAdditions import SharedModels import Styleguide import SwiftUI - @Reducer public struct EquipmentMeasurementForm { @@ -11,12 +11,14 @@ public struct EquipmentMeasurementForm { @Reducer(state: .equatable) public enum Destination { + case flaggedMeasurementsList(FlaggedMeasurementsList) case infoView(InfoViewFeature) } @ObservableState public struct State: Equatable { @Presents public var destination: Destination.State? + @Shared public var sharedSettings: SharedPressureEstimationSettings public var allowEquipmentTypeSelection: Bool public var equipmentType: EquipmentMeasurement.EquipmentType public var focusedField: Field? @@ -25,12 +27,14 @@ public struct EquipmentMeasurementForm { public init( allowEquipmentTypeSelection: Bool = true, destination: Destination.State? = nil, + sharedSettings: Shared, equipmentType: EquipmentMeasurement.EquipmentType = .airHandler, focusedField: Field? = nil, measurements: Measurements = .init() ) { self.allowEquipmentTypeSelection = allowEquipmentTypeSelection self.destination = destination + self._sharedSettings = sharedSettings self.equipmentType = equipmentType self.focusedField = focusedField self.measurements = measurements @@ -39,14 +43,27 @@ public struct EquipmentMeasurementForm { public var equipmentMeasurement: EquipmentMeasurement { measurements.equipmentMeasurement(type: equipmentType) } - - public var isValid: Bool { - measurements.airflow != nil + + private var baseValidations: Bool { + return measurements.airflow != nil && measurements.returnPlenumPressure != nil - && measurements.postFilterPressure != nil - && measurements.coilPressure != nil && measurements.supplyPlenumPressure != nil } + + private var furnaceAndCoilValidations: Bool { + return measurements.postFilterPressure != nil + && measurements.coilPressure != nil + && baseValidations + } + + public var isValid: Bool { + switch equipmentType { + case .airHandler: + return baseValidations + case .furnaceAndCoil: + return furnaceAndCoilValidations + } + } public struct Measurements: Equatable { public var airflow: Double? @@ -96,23 +113,24 @@ public struct EquipmentMeasurementForm { type: EquipmentMeasurement.EquipmentType ) -> EquipmentMeasurement { switch type { - case .airHandler: return .airHandler(.init( - airflow: airflow, - returnPlenumPressure: returnPlenumPressure, - postFilterPressure: postFilterPressure, - postCoilPressure: coilPressure, - supplyPlenumPressure: supplyPlenumPressure + airflow: airflow ?? 0, + manufacturersIncludedFilterPressureDrop: 0.1, + returnPlenumPressure: returnPlenumPressure ?? 0, + postFilterPressure: postFilterPressure ?? 0, + postCoilPressure: coilPressure ?? 0, + supplyPlenumPressure: supplyPlenumPressure ?? 0 )) case .furnaceAndCoil: return .furnaceAndCoil(.init( - airflow: airflow, - returnPlenumPressure: returnPlenumPressure, - postFilterPressure: postFilterPressure, - preCoilPressure: coilPressure, - supplyPlenumPressure: supplyPlenumPressure + airflow: airflow ?? 0, + manufacturersIncludedFilterPressureDrop: 0, + returnPlenumPressure: returnPlenumPressure ?? 0, + postFilterPressure: postFilterPressure ?? 0, + preCoilPressure: coilPressure ?? 0, + supplyPlenumPressure: supplyPlenumPressure ?? 0 )) } } @@ -138,11 +156,15 @@ public struct EquipmentMeasurementForm { @CasePathable public enum View { case infoButtonTapped + case nextButtonTapped + case onAppear case resetButtonTapped case submitField } } - + + @Dependency(\.logger["\(Self.self)"]) var logger + public var body: some Reducer { BindingReducer() Reduce { state, action in @@ -160,12 +182,37 @@ public struct EquipmentMeasurementForm { case .infoButtonTapped: state.destination = .infoView(.init()) return .none - + + case .nextButtonTapped: + guard state.isValid else { + return .fail( + """ + Received next button tapped action, but the state is not valid. + + This is considered an application logic error. + """, + logger: logger + ) + } + state.sharedSettings.equipmentMeasurement = state.equipmentMeasurement + state.destination = .flaggedMeasurementsList(.init(sharedSettings: state.$sharedSettings)) + return .none + + case .onAppear: + guard let measurement = state.sharedSettings.equipmentMeasurement else { + return .none + } + state.measurements = .init(equipmentMeasurement: measurement) + return .none + case .resetButtonTapped: state.measurements = .init() return .none - + case .submitField: + if state.focusedField == .airflow { + return .send(.view(.nextButtonTapped)) + } state.focusedField = state.focusedField?.next return .none @@ -183,6 +230,12 @@ extension Store where State == EquipmentMeasurementForm.State { ) -> String { let label = label(field: field) guard field != .airflow else { return label } + if field == .coilPressure && state.equipmentType == .airHandler { + return "\(label) (Optional)" + } + if field == .postFilterPressure && state.equipmentType == .airHandler { + return "\(label) (Optional)" + } return "\(label) Pressure" } @@ -269,8 +322,14 @@ public struct EquipmentMeasurementFormView: View { } } } + .onAppear { send(.onAppear) } .textLabelStyle(.boldSecondary) .textFieldStyle(.roundedBorder) + .toolbar { + NextButton { send(.nextButtonTapped) } + .nextButtonStyle(.toolbar) + .disabled(!store.isValid) + } .sheet( item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView) ){ store in @@ -278,6 +337,14 @@ public struct EquipmentMeasurementFormView: View { InfoView(store: store) } } + .navigationDestination( + item: $store.scope( + state: \.destination?.flaggedMeasurementsList, + action: \.destination.flaggedMeasurementsList + ) + ) { store in + FlaggedMeasurementListView(store: store) + } } private func textField( @@ -350,7 +417,9 @@ fileprivate extension InfoViewFeature.State { #Preview { NavigationStack { EquipmentMeasurementFormView( - store: Store(initialState: EquipmentMeasurementForm.State()) { + store: Store(initialState: EquipmentMeasurementForm.State( + sharedSettings: Shared(SharedPressureEstimationSettings())) + ) { EquipmentMeasurementForm() } ) diff --git a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift index 4f45778..8e78e01 100644 --- a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift @@ -4,13 +4,13 @@ import SharedModels import Styleguide import SwiftUI - @Reducer public struct EquipmentSettingsForm { @CasePathable public enum InfoView { case capacities + case manufacturersIncludedFilterPressureDrop case ratedStaticPressures } @@ -21,34 +21,27 @@ public struct EquipmentSettingsForm { @ObservableState public struct State: Equatable { - @Presents public var destination: Destination.State? - public var coolingCapacity: EquipmentMetadata.CoolingCapacity + public var includesFilterDrop: Bool public var equipmentType: EquipmentMeasurement.EquipmentType - public var fanType: EquipmentMetadata.FanType public var focusedField: Field? = nil - public var heatingCapacity: Double? - public var ratedStaticPressures: RatedStaticPressures + @Shared public var sharedSettings: SharedPressureEstimationSettings public init( - coolingCapacity: EquipmentMetadata.CoolingCapacity = .default, destination: Destination.State? = nil, + includesFilterDrop: Bool = false, equipmentType: EquipmentMeasurement.EquipmentType = .airHandler, - fanType: EquipmentMetadata.FanType = .constantSpeed, - heatingCapacity: Double? = nil, - ratedStaticPressures: RatedStaticPressures = .init() + sharedSettings: Shared ) { - self.coolingCapacity = coolingCapacity self.destination = destination + self.includesFilterDrop = includesFilterDrop self.equipmentType = equipmentType - self.fanType = fanType - self.heatingCapacity = heatingCapacity - self.ratedStaticPressures = ratedStaticPressures + self._sharedSettings = sharedSettings } public var isValid: Bool { guard equipmentType == .furnaceAndCoil else { return true } - return heatingCapacity != nil + return sharedSettings.heatingCapacity != nil } // Note: These need to be in display order. @@ -57,6 +50,7 @@ public struct EquipmentSettingsForm { case minimumStaticPressure case maximumStaticPressure case ratedStaticPressure + case manufacturersIncludedFilterPressureDrop } } @@ -68,7 +62,6 @@ public struct EquipmentSettingsForm { @CasePathable public enum View { case infoButtonTapped(InfoView) - case nextButtonTapped case resetButtonTapped case submitField } @@ -79,6 +72,17 @@ public struct EquipmentSettingsForm { Reduce { state, action in switch action { + case .binding(\.includesFilterDrop): + guard state.includesFilterDrop else { + state.sharedSettings.manufacturersIncludedFilterPressureDrop = nil + return .none + } + guard state.sharedSettings.manufacturersIncludedFilterPressureDrop != nil else { + return .none + } + state.sharedSettings.manufacturersIncludedFilterPressureDrop = 0.1 + return .none + case .binding: return .none @@ -96,12 +100,8 @@ public struct EquipmentSettingsForm { state.destination = .infoView(.init(view: infoView)) return .none - case .nextButtonTapped: - #warning("Fix me.") - return .none - case .resetButtonTapped: - state.heatingCapacity = nil + state.sharedSettings.heatingCapacity = nil return .none case .submitField: @@ -138,7 +138,7 @@ public struct EquipmentSettingsFormView: View { .listRowBackground(Color.clear) Section { - FanTypePicker(selection: $store.fanType) + FanTypePicker(selection: $store.sharedSettings.fanType) .pickerStyle(.segmented) } header: { Text("Fan Type") @@ -154,20 +154,16 @@ public struct EquipmentSettingsFormView: View { : "Capacity" ) Spacer() - CoolingCapacityPicker(selection: $store.coolingCapacity) -// Picker("Cooling Capcity", selection: $store.coolingCapacity) { -// ForEach(CoolingCapacity.allCases) { -// Text($0.description) -// .tag($0) -// } -// } + CoolingCapacityPicker( + selection: $store.sharedSettings.coolingCapacity + ) } if store.equipmentType == .furnaceAndCoil { GridRow { TextLabel("Heating") textField( "Heating Capacity", - value: $store.heatingCapacity, + value: $store.sharedSettings.heatingCapacity, fractionLength: 0 ) .focused($focusedField, equals: .heatingCapacity) @@ -190,7 +186,7 @@ public struct EquipmentSettingsFormView: View { TextLabel("Minimum") textField( "Minimum Pressure", - value: $store.ratedStaticPressures.minimum, + value: $store.sharedSettings.ratedStaticPressures.minimum, fractionLength: 2 ) .focused($focusedField, equals: .minimumStaticPressure) @@ -200,7 +196,7 @@ public struct EquipmentSettingsFormView: View { TextLabel("Maximum") textField( "Maximum Pressure", - value: $store.ratedStaticPressures.maximum, + value: $store.sharedSettings.ratedStaticPressures.maximum, fractionLength: 2 ) .focused($focusedField, equals: .maximumStaticPressure) @@ -210,7 +206,7 @@ public struct EquipmentSettingsFormView: View { TextLabel("Rated") textField( "Rated Pressure", - value: $store.ratedStaticPressures.rated, + value: $store.sharedSettings.ratedStaticPressures.rated, fractionLength: 2 ) .focused($focusedField, equals: .ratedStaticPressure) @@ -221,74 +217,36 @@ public struct EquipmentSettingsFormView: View { header("Rated Static Pressure", infoView: .ratedStaticPressures) } -// Section { -// Grid(alignment: .leading, horizontalSpacing: 40) { -// if store.equipmentMeasurement.equipmentType == .furnaceAndCoil { -// GridRow { -// TextLabel("Coil") -// percentField("Coil Budget", value: $store.budgets.coilBudget.fraction) -// .focused($focusedField, equals: .coilBudget) -// .onSubmit { send(.submitField) } -// .numberPad() -// } -// } -// -// GridRow { -// TextLabel("Filter") -// percentField("Filter Budget", value: $store.budgets.filterBudget.fraction) -// .focused($focusedField, equals: .filterBudget) -// .onSubmit { send(.submitField) } -// .numberPad() -// } -// -// GridRow { -// TextLabel("Return") -// percentField("Return Plenum Budget", value: $store.budgets.returnPlenumBudget.fraction) -// .focused($focusedField, equals: .returnPlenumBudget) -// .onSubmit { send(.submitField) } -// .numberPad() -// } -// -// GridRow { -// TextLabel("Supply") -// percentField("Supply Plenum Budget", value: $store.budgets.supplyPlenumBudget.fraction) -// .focused($focusedField, equals: .supplyPlenumBudget) -// .onSubmit { send(.submitField) } -// .numberPad() -// } -// } -// -// } header: { -// VStack { -// header("Budgets", infoView: .budgets) -// #if os(macOS) -// .font(.title2) -// .padding(.top, 20) -// #endif -// FlaggedView( -// flagged: Flagged(wrappedValue: store.budgets.total.rawValue, .percent()) -// ) -// .flaggedViewStyle(BudgetFlagViewStyle()) -// } -// } - -// Section { -// TextField( -// "Airflow", -// value: $store.updatedAirflow, -// fractionLength: 0 -// ) -// } header: { -// header("Updated Airflow", infoView: .updatedAirflow) -// } footer: { -// HStack { -// ResetButton { send(.resetButtonTapped) } -// Spacer() -// NextButton { send(.nextButtonTapped) } -// .disabled(!store.isValid) -// } -// .padding(.top) -// } + Section { + VStack(alignment: .leading) { + HStack { + TextLabel("Includes Filter Drop") + Spacer() + Toggle("Includes Filter Drop", isOn: $store.includesFilterDrop) + } + if store.includesFilterDrop { + HStack { + TextLabel("Filter Drop") + Spacer() + textField( + "Filter Drop", + value: $store.sharedSettings.manufacturersIncludedFilterPressureDrop, + fractionLength: 2 + ) + .focused($focusedField, equals: .manufacturersIncludedFilterPressureDrop) + .decimalPad() + .padding(.leading, 40) + } + } + } + } header: { + header(infoView: .manufacturersIncludedFilterPressureDrop) { + VStack(alignment: .leading) { + Text("Manufacturer's") + Text("Filter Pressure Drop") + } + } + } } .labelsHidden() .bind($focusedField, to: $store.focusedField) @@ -306,23 +264,23 @@ public struct EquipmentSettingsFormView: View { } } - private func header( - _ title: String, - infoView: EquipmentSettingsForm.InfoView + private func header( + infoView: EquipmentSettingsForm.InfoView, + label: @escaping () -> Label ) -> some View { HStack { - Text(title) + label() Spacer() InfoButton { send(.infoButtonTapped(infoView)) } } } -// private func percentField( -// _ title: LocalizedStringKey, -// value: Binding -// ) -> some View { -// TextField(title, value: value, format: .percent, prompt: Text(title)) -// } + private func header( + _ title: String, + infoView: EquipmentSettingsForm.InfoView + ) -> some View { + header(infoView: infoView) { Text(title) } + } private func textField( _ title: String, @@ -362,37 +320,29 @@ fileprivate extension InfoViewFeature.State { Record the cooling and heating capacities of the system. """ ) -// case .budgets: -// self.init( -// title: "Budgets", -// body: """ -// Budgeted percentages for static pressure estimations, these generally are set to -// reasonable defaults, however you can change them if desired. -// -// Note: These must total up to 100%. -// """ -// ) + case .manufacturersIncludedFilterPressureDrop: + self.init( + title: "Filter Pressure Drop", + body: """ + Record the filter pressure drop that the manufacturer includes in their blower performance table, if applicable. + + Sometimes this information is not listed, therefore it may be reasonable to use a sensible default value of '0.1'. + + Note: The value that is set get's deducted from the filter pressure drop when determining the external static pressure of a system. + """ + ) case .ratedStaticPressures: self.init( title: "Rated Static", body: """ Record the rated static pressures of the system. - These are generally found on the nameplate of the equipment or in the installation - manual. + These are generally found on the nameplate of the equipment or in the installation manual. The defaults are generally acceptable for most unitary equipment. """ ) -// case .updatedAirflow: -// self.init( -// title: "Updated Airflow", -// body: """ -// This is used to generated estimated static pressures at the updated airflow -// compared to the existing airflow of the system. -// """ -// ) } } } @@ -401,9 +351,11 @@ fileprivate extension InfoViewFeature.State { NavigationStack { EquipmentSettingsFormView( store: Store( - initialState: EquipmentSettingsForm.State() + initialState: EquipmentSettingsForm.State( + sharedSettings: Shared(SharedPressureEstimationSettings()) + ) ) { - EquipmentSettingsForm() + EquipmentSettingsForm()._printChanges() } ) #if os(macOS) diff --git a/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift b/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift index 4749eb8..23f3c7a 100644 --- a/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift +++ b/Sources/PressureEstimationsFeature/FlaggedMeasurementsList.swift @@ -20,12 +20,12 @@ public struct FlaggedMeasurementsList { public struct State: Equatable { @Presents public var destination: Destination.State? - @Shared var sharedSettings: SharedSettings + @Shared var sharedSettings: SharedPressureEstimationSettings public var estimatedMeasurements: IdentifiedArrayOf init( destination: Destination.State? = nil, - sharedSettings: Shared, + sharedSettings: Shared, estimatedMeasurements: IdentifiedArrayOf = [] ) { self.destination = destination @@ -33,6 +33,7 @@ public struct FlaggedMeasurementsList { self.estimatedMeasurements = estimatedMeasurements } + #warning("Move to shared settings.") public struct FlaggedMeasurementContainer: Equatable, Identifiable { public let id: UUID public var flaggedMeasurement: EquipmentMeasurement.FlaggedMeasurement @@ -115,7 +116,7 @@ public struct FlaggedMeasurementsList { case .addButtonTapped: state.destination = .estimationForm(.init( - coolingCapacity: state.sharedSettings.coolingCapacity + coolingCapacity: state.sharedSettings.equipmentMetadata.coolingCapacity )) return .none @@ -145,16 +146,20 @@ public struct FlaggedMeasurementsList { return .none case .onAppear: - guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement, - let budgets = state.sharedSettings.budgets - else { + guard let equipmentMeasurement = state.sharedSettings.equipmentMeasurement else { return .none } + if state.sharedSettings.budgets == nil { + state.sharedSettings.budgets = .init( + equipmentType: state.sharedSettings.equipmentMeasurement!.equipmentType, + fanType: state.sharedSettings.equipmentMetadata.fanType + ) + } state.sharedSettings.flaggedEquipmentMeasurement = .init( - budgets: budgets, + budgets: state.sharedSettings.budgets!, measurement: equipmentMeasurement, - ratedPressures: state.sharedSettings.ratedStaticPressures, - tons: state.sharedSettings.coolingCapacity + ratedPressures: state.sharedSettings.equipmentMetadata.ratedStaticPressures, + tons: state.sharedSettings.equipmentMetadata.coolingCapacity ) return .none } @@ -178,8 +183,8 @@ public struct FlaggedMeasurementsList { ) } - return .receive(action: \.receive) { [ratedStaticPressures = state.sharedSettings.ratedStaticPressures] in - + return .receive(action: \.receive) { [ratedStaticPressures = state.sharedSettings.equipmentMetadata.ratedStaticPressures] in + let filterPressureDrop = form.filterPressureDrop != nil ? Positive(wrappedValue: form.filterPressureDrop!) : nil @@ -221,8 +226,6 @@ public struct FlaggedMeasurementListView: View { } header: { HStack { Text("Existing Measurements") - Spacer() -// Button("Edit") { } } } } @@ -278,7 +281,7 @@ public struct FlaggedMeasurementListView: View { } #if DEBUG -private let sharedSettings = SharedSettings( +private let sharedPressureEstimationSettings = SharedPressureEstimationSettings( budgets: .init(equipmentType: .airHandler, fanType: .constantSpeed), equipmentMeasurement: .mock(type: .airHandler), flaggedEquipmentMeasurement: nil @@ -290,10 +293,10 @@ private let flaggedMeasurements = IdentifiedArrayOf { Scope(state: \.equipmentSettings, action: \.equipmentSettings) { EquipmentSettingsForm() @@ -57,13 +60,42 @@ public struct PressureEstimationsFeature { case let .view(action): switch action { case .nextButtonTapped: - return .none + return handleNextButtonTapped(&state) } } } .ifLet(\.$destination, action: \.destination) } - + + private func handleNextButtonTapped(_ state: inout State) -> Effect { + guard state.destination == nil else { + return .fail( + """ + Received next button tapped action on equipment settings form, but the destination is not nil. + + This is considered an application logic error. + """, + logger: logger + ) + } + guard state.equipmentSettings.isValid else { + return .fail( + """ + Received next button tapped action on equipment settings form, but the form is invalid. + + This is considered an application logic error. + """, + logger: logger + ) + } + state.destination = .equipmentMeasurements(.init( + allowEquipmentTypeSelection: false, + sharedSettings: state.$sharedSettings, + equipmentType: state.equipmentSettings.equipmentType + )) + return .none + } + } @ViewAction(for: PressureEstimationsFeature.self) @@ -79,5 +111,30 @@ public struct PressureEstimationsView: View { EquipmentSettingsFormView( store: store.scope(state: \.equipmentSettings, action: \.equipmentSettings) ) + .navigationTitle("Equipment Settings") + .toolbar { + NextButton { send(.nextButtonTapped) } + .nextButtonStyle(.toolbar) + .disabled(!store.equipmentSettings.isValid) + } + .navigationDestination( + item: $store.scope( + state: \.destination?.equipmentMeasurements, + action: \.destination.equipmentMeasurements + ) + ) { measurementStore in + EquipmentMeasurementFormView(store: measurementStore) + .navigationTitle("Existing Measurements") + } + } +} + +#Preview { + NavigationStack { + PressureEstimationsView( + store: Store(initialState: PressureEstimationsFeature.State()) { + PressureEstimationsFeature()._printChanges() + } + ) } } diff --git a/Sources/PressureEstimationsFeature/SharedPressureEstimationSettings.swift b/Sources/PressureEstimationsFeature/SharedPressureEstimationSettings.swift new file mode 100644 index 0000000..010fc57 --- /dev/null +++ b/Sources/PressureEstimationsFeature/SharedPressureEstimationSettings.swift @@ -0,0 +1,40 @@ +import ComposableArchitecture +import SharedModels + +/// Holds onto shared values for several of the views in this feature. +@dynamicMemberLookup +public struct SharedPressureEstimationSettings: Equatable { + public var budgets: BudgetedPercentEnvelope? + public var equipmentMeasurement: EquipmentMeasurement? + public var equipmentMetadata: EquipmentMetadata + public var flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? + public var heatingCapacity: Double? + public var manufacturersIncludedFilterPressureDrop: Double? + + public init( + budgets: BudgetedPercentEnvelope? = nil, + equipmentMeasurement: EquipmentMeasurement? = nil, + equipmentMetadata: EquipmentMetadata = .init(), + flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? = nil, + heatingCapacity: Double? = nil, + manufacturersIncludedFilterPressureDrop: Double? = nil + ) { + self.budgets = budgets + self.equipmentMeasurement = equipmentMeasurement + self.equipmentMetadata = equipmentMetadata + self.flaggedEquipmentMeasurement = flaggedEquipmentMeasurement + self.heatingCapacity = heatingCapacity + self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop + } + + public subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { equipmentMetadata[keyPath: keyPath] } + set { equipmentMetadata[keyPath: keyPath] = newValue } + } +} + +extension PersistenceReaderKey where Self == InMemoryKey { + static var sharedPressureEstimationSettings: Self { + .inMemory("sharedPressureEstimationSettings") + } +} diff --git a/Sources/PressureEstimationsFeature/SharedSettings.swift b/Sources/PressureEstimationsFeature/SharedSettings.swift deleted file mode 100644 index 118423f..0000000 --- a/Sources/PressureEstimationsFeature/SharedSettings.swift +++ /dev/null @@ -1,37 +0,0 @@ -import ComposableArchitecture -import SharedModels - - -struct SharedSettings: Equatable { - var budgets: BudgetedPercentEnvelope? - var coolingCapacity: EquipmentMetadata.CoolingCapacity - var equipmentMeasurement: EquipmentMeasurement? - var fanType: EquipmentMetadata.FanType - var flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? - var heatingCapacity: Double? - var ratedStaticPressures: RatedStaticPressures - - init( - budgets: BudgetedPercentEnvelope? = nil, - coolingCapacity: EquipmentMetadata.CoolingCapacity = .default, - equipmentMeasurement: EquipmentMeasurement? = nil, - fanType: EquipmentMetadata.FanType = .constantSpeed, - flaggedEquipmentMeasurement: EquipmentMeasurement.FlaggedMeasurement? = nil, - heatingCapacity: Double? = nil, - ratedStaticPressures: RatedStaticPressures = .init() - ) { - self.budgets = budgets - self.coolingCapacity = coolingCapacity - self.equipmentMeasurement = equipmentMeasurement - self.fanType = fanType - self.flaggedEquipmentMeasurement = flaggedEquipmentMeasurement - self.heatingCapacity = heatingCapacity - self.ratedStaticPressures = ratedStaticPressures - } -} - -extension PersistenceReaderKey where Self == InMemoryKey { - static var sharedSettings: Self { - .inMemory("sharedSettings") - } -} diff --git a/Sources/SharedModels/CoolingCapacity.swift b/Sources/SharedModels/CoolingCapacity.swift deleted file mode 100644 index fbf2875..0000000 --- a/Sources/SharedModels/CoolingCapacity.swift +++ /dev/null @@ -1,2 +0,0 @@ -import Foundation - diff --git a/Sources/SharedModels/EquipmentMeasurement.swift b/Sources/SharedModels/EquipmentMeasurement.swift index 401cbb5..d7ba7db 100644 --- a/Sources/SharedModels/EquipmentMeasurement.swift +++ b/Sources/SharedModels/EquipmentMeasurement.swift @@ -1,8 +1,5 @@ import Foundation -#warning("Make values non-optional") -#warning("Need to make air handler external static handle large filter pressure drops.") -#warning("Add an exterenal static pressure strategy for if the filter is built-in or external") public enum EquipmentMeasurement: Equatable { case airHandler(AirHandler) @@ -17,7 +14,7 @@ public enum EquipmentMeasurement: Equatable { } } - public var externalStaticPressure: Double { + public var externalStaticPressure: Positive { switch self { case let .airHandler(airHandler): return airHandler.externalStaticPressure @@ -26,47 +23,75 @@ public enum EquipmentMeasurement: Equatable { } } + public var manufacturersIncludedFilterPressureDrop: Positive { + switch self { + case let .airHandler(airHandler): + return airHandler.$manufacturersIncludedFilterPressureDrop + case let .furnaceAndCoil(furnaceAndCoil): + return furnaceAndCoil.$manufacturersIncludedFilterPressureDrop + } + } + public struct AirHandler: Equatable { @Positive - public var airflow: Double? + public var airflow: Double @Positive - public var returnPlenumPressure: Double? + public var manufacturersIncludedFilterPressureDrop: Double + + @Positive + public var returnPlenumPressure: Double @Positive - public var postFilterPressure: Double? + public var postFilterPressure: Double @Positive - public var postCoilPressure: Double? + public var postCoilPressure: Double @Positive - public var supplyPlenumPressure: Double? + public var supplyPlenumPressure: Double public init( - airflow: Double? = nil, - returnPlenumPressure: Double? = nil, - postFilterPressure: Double? = nil, - postCoilPressure: Double? = nil, - supplyPlenumPressure: Double? = nil + airflow: Double, + manufacturersIncludedFilterPressureDrop: Double, + returnPlenumPressure: Double, + postFilterPressure: Double, + postCoilPressure: Double, + supplyPlenumPressure: Double ) { self.airflow = airflow + self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop self.returnPlenumPressure = returnPlenumPressure self.postFilterPressure = postFilterPressure self.postCoilPressure = postCoilPressure self.supplyPlenumPressure = supplyPlenumPressure } - - public var externalStaticPressure: Double { - var postFilterAdder = 0.0 - if let postFilterPressure = $postFilterPressure.positiveValue, - postFilterPressure > 0.1 - { - postFilterAdder = postFilterPressure - 0.1 + + public init( + airflow: Positive, + manufacturersIncludedFilterPressureDrop: Positive, + returnPlenumPressure: Positive, + postFilterPressure: Positive, + postCoilPressure: Positive, + supplyPlenumPressure: Positive + ) { + self._airflow = airflow + self._manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop + self._returnPlenumPressure = returnPlenumPressure + self._postFilterPressure = postFilterPressure + self._postCoilPressure = postCoilPressure + self._supplyPlenumPressure = supplyPlenumPressure + } + + public var externalStaticPressure: Positive { + var postFilterAdder = Positive.zero + if $postFilterPressure > $manufacturersIncludedFilterPressureDrop { + postFilterAdder = $postFilterPressure - $manufacturersIncludedFilterPressureDrop } - return ($returnPlenumPressure.positiveValue ?? 0) + return $returnPlenumPressure + postFilterAdder - + ($supplyPlenumPressure.positiveValue ?? 0) + + $supplyPlenumPressure } } @@ -145,7 +170,7 @@ public enum EquipmentMeasurement: Equatable { .init( airflow: checkAirflow(value: measurement.airflow, tons: tons), coilPressureDrop: .init( - wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue) ?? 0, + wrappedValue: (measurement.$postCoilPressure.positiveValue - measurement.$postFilterPressure.positiveValue), .result(.good()) ), externalStaticPressure: checkExternalStaticPressure( @@ -155,7 +180,8 @@ public enum EquipmentMeasurement: Equatable { filterPressureDrop: .init( value: measurement.$postFilterPressure.positiveValue - measurement.$returnPlenumPressure.positiveValue, budget: budgets.filterBudget, - ratedPressures: ratedPressures + ratedPressures: ratedPressures, + ignoreMinimum: true ), returnPlenumPressure: .init( value: measurement.$returnPlenumPressure.positiveValue, @@ -163,7 +189,7 @@ public enum EquipmentMeasurement: Equatable { ratedPressures: ratedPressures ), supplyPlenumPressure: .init( - value: measurement.$supplyPlenumPressure.positiveValue ?? 0, + value: measurement.$supplyPlenumPressure.positiveValue, budget: budgets.supplyPlenumBudget, ratedPressures: ratedPressures ) @@ -211,61 +237,61 @@ public enum EquipmentMeasurement: Equatable { public struct FurnaceAndCoil: Equatable { @Positive - public var airflow: Double? + public var airflow: Double @Positive - public var returnPlenumPressure: Double? + public var manufacturersIncludedFilterPressureDrop: Double + + @Positive + public var returnPlenumPressure: Double @Positive - public var postFilterPressure: Double? + public var postFilterPressure: Double @Positive - public var preCoilPressure: Double? + public var preCoilPressure: Double @Positive - public var supplyPlenumPressure: Double? + public var supplyPlenumPressure: Double public init( - airflow: Double? = nil, - returnPlenumPressure: Double? = nil, - postFilterPressure: Double? = nil, - preCoilPressure: Double? = nil, - supplyPlenumPressure: Double? = nil + airflow: Double, + manufacturersIncludedFilterPressureDrop: Double, + returnPlenumPressure: Double, + postFilterPressure: Double, + preCoilPressure: Double, + supplyPlenumPressure: Double ) { self.airflow = airflow + self.manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop self.returnPlenumPressure = returnPlenumPressure self.postFilterPressure = postFilterPressure self.preCoilPressure = preCoilPressure self.supplyPlenumPressure = supplyPlenumPressure } - - public var externalStaticPressure: Double { - ($postFilterPressure.positiveValue ?? 0) + ($preCoilPressure.positiveValue ?? 0) + + public init( + airflow: Positive, + manufacturersIncludedFilterPressureDrop: Positive, + returnPlenumPressure: Positive, + postFilterPressure: Positive, + preCoilPressure: Positive, + supplyPlenumPressure: Positive + ) { + self._airflow = airflow + self._manufacturersIncludedFilterPressureDrop = manufacturersIncludedFilterPressureDrop + self._returnPlenumPressure = returnPlenumPressure + self._postFilterPressure = postFilterPressure + self._preCoilPressure = preCoilPressure + self._supplyPlenumPressure = supplyPlenumPressure + } + + public var externalStaticPressure: Positive { + ($postFilterPressure - $manufacturersIncludedFilterPressureDrop) + $preCoilPressure } } } -//extension EquipmentMeasurement.AirHandler { -// -// public enum Key: String, Equatable, CaseIterable { -// case returnPlenumPressure -// case postFilterPressure -// case postCoilPressure -// case supplyPlenumPressure -// case airflow -// } -//} -// -//extension EquipmentMeasurement.FurnaceAndCoil { -// -// public enum Key: String, Equatable, CaseIterable { -// case returnPlenumPressure -// case postFilterPressure -// case preCoilPressure -// case supplyPlenumPressure -// case airflow -// } -//} fileprivate extension Flagged { init( @@ -303,11 +329,11 @@ fileprivate extension Flagged { } fileprivate func checkExternalStaticPressure( - value: Double, + value: Positive, ratedPressures: RatedStaticPressures ) -> Flagged { .init( - wrappedValue: value, + wrappedValue: value.positiveValue, .rated(ratedPressures) ) } @@ -330,6 +356,7 @@ extension EquipmentMeasurement { case .airHandler: return .airHandler(.init( airflow: 1200, + manufacturersIncludedFilterPressureDrop: 0.1, returnPlenumPressure: 0.3, postFilterPressure: 0.6, postCoilPressure: 0.9, @@ -339,6 +366,7 @@ extension EquipmentMeasurement { case .furnaceAndCoil: return .furnaceAndCoil(.init( airflow: 1200, + manufacturersIncludedFilterPressureDrop: 0.0, returnPlenumPressure: 0.3, postFilterPressure: 0.6, preCoilPressure: 0.4, diff --git a/Sources/SharedModels/EquipmentMetadata.swift b/Sources/SharedModels/EquipmentMetadata.swift index 6507741..d4f6e42 100644 --- a/Sources/SharedModels/EquipmentMetadata.swift +++ b/Sources/SharedModels/EquipmentMetadata.swift @@ -3,16 +3,16 @@ import Foundation public struct EquipmentMetadata: Equatable { public var coolingCapacity: CoolingCapacity public var fanType: FanType - public var ratedStaticPressure: RatedStaticPressures + public var ratedStaticPressures: RatedStaticPressures public init( coolingCapacity: CoolingCapacity = .three, fanType: FanType = .constantSpeed, - ratedStaticPressure: RatedStaticPressures = .init() + ratedStaticPressures: RatedStaticPressures = .init() ) { self.coolingCapacity = coolingCapacity self.fanType = fanType - self.ratedStaticPressure = ratedStaticPressure + self.ratedStaticPressures = ratedStaticPressures } public enum CoolingCapacity: Double, Equatable, CaseIterable, Identifiable, CustomStringConvertible { diff --git a/Sources/SharedModels/Flagged.swift b/Sources/SharedModels/Flagged.swift index b16f053..65b47ae 100644 --- a/Sources/SharedModels/Flagged.swift +++ b/Sources/SharedModels/Flagged.swift @@ -1,5 +1,8 @@ import Foundation +/// Represents a number that can be checked if it is within an acceptable range. It can generate errors or warnings depending +/// on the current value. +/// @dynamicMemberLookup public struct Flagged: Equatable { diff --git a/Sources/SharedModels/HelperTypes/Positive.swift b/Sources/SharedModels/HelperTypes/Positive.swift index 48f8e6a..e51f5b1 100644 --- a/Sources/SharedModels/HelperTypes/Positive.swift +++ b/Sources/SharedModels/HelperTypes/Positive.swift @@ -5,6 +5,10 @@ public struct Positive where Value: Numeric, Value: Comparable { public var wrappedValue: Value + public init(_ value: Value) { + self.wrappedValue = value + } + public init(wrappedValue: Value) { self.wrappedValue = wrappedValue } diff --git a/Sources/Styleguide/KeyboardStyles.swift b/Sources/Styleguide/KeyboardStyles.swift index 5fa4a72..c3d3e7f 100644 --- a/Sources/Styleguide/KeyboardStyles.swift +++ b/Sources/Styleguide/KeyboardStyles.swift @@ -10,9 +10,13 @@ extension View { #endif } + #warning("Fix me.") + // The decimal pad autocompletes too quickly in the simulator, needs tested on an actual + // device. public func decimalPad() -> some View { #if os(iOS) - self.keyboardType(.decimalPad) +// self.keyboardType(.decimalPad) + self.keyboardType(.numberPad) #else self #endif diff --git a/Sources/Styleguide/Styles/ButtonStyles.swift b/Sources/Styleguide/Styles/ButtonStyles.swift index 9339c08..2007293 100644 --- a/Sources/Styleguide/Styles/ButtonStyles.swift +++ b/Sources/Styleguide/Styles/ButtonStyles.swift @@ -88,6 +88,22 @@ extension DefaultNextButtonStyle where ButtonStyle == BorderedProminentButtonSty } } +public struct ToolbarNextButtonStyle: PrimitiveButtonStyle { + + public func makeBody(configuration: Configuration) -> some View { + Button(role: configuration.role, action: configuration.trigger) { + configuration.label + .foregroundStyle(Color.accentColor) + } + .labelStyle(ReverseLabelStyle()) + .buttonStyle(.plain) + } +} + +extension AnyPrimitiveButtonStyle { + public static var toolbar: Self { .init(ToolbarNextButtonStyle()) } +} + extension AnyButtonStyle where ButtonType == InfoButtonType { public static var `default`: Self { .init(DefaultInfoButtonStyle(labelStyle: .iconOnly))