diff --git a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift index 690bac2..9cb04d0 100644 --- a/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift +++ b/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift @@ -27,6 +27,7 @@ public struct EquipmentSettingsForm { public var equipmentType: EquipmentMeasurement.EquipmentType public var focusedField: Field? = nil @Shared public var sharedSettings: SharedPressureEstimationState + public var ratedStaticPressures: RatedStaticPressuresSection.State public init( destination: Destination.State? = nil, @@ -38,20 +39,30 @@ public struct EquipmentSettingsForm { self.includesFilterDrop = includesFilterDrop self.equipmentType = equipmentType self._sharedSettings = sharedSettings + self.ratedStaticPressures = .init(staticPressures: sharedSettings.equipmentMetadata.ratedStaticPressures) } - + public var isValid: Bool { - guard equipmentType == .furnaceAndCoil else { return true } - return sharedSettings.heatingCapacity != nil + guard equipmentType == .furnaceAndCoil + else { return ratedStaticPressures.isValid } + return ratedStaticPressures.isValid && sharedSettings.heatingCapacity != nil } // Note: These need to be in display order. public enum Field: Hashable, CaseIterable, FocusableField, Identifiable { case heatingCapacity - case minimumStaticPressure - case maximumStaticPressure - case ratedStaticPressure + case ratedStaticPressure(RatedStaticPressuresSection.State.FocusedField) case manufacturersIncludedFilterPressureDrop + + public static var allCases: [EquipmentSettingsForm.State.Field] { + [ + .heatingCapacity, + .ratedStaticPressure(.maximum), + .ratedStaticPressure(.minimum), + .ratedStaticPressure(.rated), + .manufacturersIncludedFilterPressureDrop + ] + } public var id: Self { self } } @@ -60,6 +71,7 @@ public struct EquipmentSettingsForm { public enum Action: BindableAction, ViewAction { case binding(BindingAction) case destination(PresentationAction) + case ratedStaticPressures(RatedStaticPressuresSection.Action) case view(View) @CasePathable @@ -72,6 +84,9 @@ public struct EquipmentSettingsForm { public var body: some Reducer { BindingReducer() + Scope(state: \.ratedStaticPressures, action: \.ratedStaticPressures) { + RatedStaticPressuresSection() + } Reduce { state, action in switch action { @@ -95,6 +110,14 @@ public struct EquipmentSettingsForm { case .destination: return .none + + + case .ratedStaticPressures(.delegate(.infoButtonTapped)): + state.destination = .infoView(.init(view: .ratedStaticPressures)) + return .none + + case .ratedStaticPressures: + return .none case let .view(action): switch action { @@ -177,7 +200,6 @@ public struct EquipmentSettingsFormView: View { } } .dynamicBottomPadding() -// .applyPadding() } header: { if store.equipmentType == .airHandler { EmptyView() @@ -186,15 +208,9 @@ public struct EquipmentSettingsFormView: View { } } - Section { - Grid(alignment: .leading, horizontalSpacing: 40) { - ForEach(RatingsField.allCases, content: ratingsRow(for:)) - } - .dynamicBottomPadding() - .labeledContentStyle(.gridRow) - } header: { - header("Rated Static Pressure", infoView: .ratedStaticPressures) - } + RatedStaticPressuresSectionView( + store: store.scope(state: \.ratedStaticPressures, action: \.ratedStaticPressures) + ) Section { VStack(alignment: .leading) { @@ -240,27 +256,6 @@ public struct EquipmentSettingsFormView: View { } } - private func ratingsRow(for ratingsField: RatingsField) -> some View { - - func binding(for ratingsField: RatingsField) -> Binding { - switch ratingsField { - case .maximum: - return $store.sharedSettings.ratedStaticPressures.maximum - case .minimum: - return $store.sharedSettings.ratedStaticPressures.minimum - case .rated: - return $store.sharedSettings.ratedStaticPressures.rated - } - } - - return TextLabeledContent(ratingsField.label) { - TextField(ratingsField.prompt, value: binding(for: ratingsField), fractionLength: 2) - .decimalPad() - .focused($focusedField, equals: ratingsField.field) - .onSubmit { send(.submitField) } - } - } - private func header( infoView: EquipmentSettingsForm.InfoView, label: @escaping () -> Label @@ -289,17 +284,6 @@ public struct EquipmentSettingsFormView: View { } } -fileprivate extension View { - @ViewBuilder - func applyPadding() -> some View { - #if os(macOS) - self.padding(.bottom, 20) - #else - self - #endif - } -} - fileprivate struct BudgetFlagViewStyle: FlaggedViewStyle { func makeBody(configuration: Configuration) -> some View { @@ -348,26 +332,6 @@ fileprivate extension InfoViewFeature.State { } } -fileprivate enum RatingsField: String, Hashable, CaseIterable, Identifiable { - case maximum - case minimum - case rated - - var id: Self { self } - - var field: EquipmentSettingsForm.State.Field { - switch self { - case .maximum: return .maximumStaticPressure - case .minimum: return .minimumStaticPressure - case .rated: return .ratedStaticPressure - } - } - - var label: String { rawValue.capitalized } - - var prompt: String { "\(label) Pressure" } -} - #Preview { NavigationStack { EquipmentSettingsFormView( diff --git a/Sources/PressureEstimationsFeature/RatedStaticPressuresSection.swift b/Sources/PressureEstimationsFeature/RatedStaticPressuresSection.swift new file mode 100644 index 0000000..05ee997 --- /dev/null +++ b/Sources/PressureEstimationsFeature/RatedStaticPressuresSection.swift @@ -0,0 +1,141 @@ +import ComposableArchitecture +import SharedModels +import Styleguide +import SwiftUI + +#warning("Add info view destination??") +/// Allows for rated static pressure fields to be optional values, setting their corresponding shared state values to zero when they're nilled out. +/// +@Reducer +public struct RatedStaticPressuresSection { + + @ObservableState + public struct State: Equatable { + + @Shared public var staticPressures: RatedStaticPressures + public var focusedField: FocusedField? + public var maxPressure: Double? + public var minPressure: Double? + public var ratedPressure: Double? + + public init( + staticPressures: Shared + ) { + self._staticPressures = staticPressures + self.maxPressure = staticPressures.maximum.wrappedValue + self.minPressure = staticPressures.minimum.wrappedValue + self.ratedPressure = staticPressures.rated.wrappedValue + } + + public var isValid: Bool { + maxPressure != nil + && minPressure != nil + && ratedPressure != nil + } + + public enum FocusedField: String, Hashable, CaseIterable, FocusableField { + case maximum + case minimum + case rated + + var label: String { rawValue.capitalized } + } + } + + public enum Action: BindableAction { + case binding(BindingAction) + case delegate(DelegateAction) + + public enum DelegateAction { + case infoButtonTapped + } + } + + public var body: some Reducer { + BindingReducer() + Reduce { state, action in + switch action { + case .binding(\.maxPressure): + state.staticPressures.maximum = state.maxPressure ?? 0 + return .none + + case .binding(\.minPressure): + state.staticPressures.minimum = state.minPressure ?? 0 + return .none + + case .binding(\.ratedPressure): + state.staticPressures.rated = state.ratedPressure ?? 0 + return .none + + case .binding: + return .none + + case .delegate: + return .none + } + } + } +} + +public struct RatedStaticPressuresSectionView: View { + + @FocusState private var focusedField: RatedStaticPressuresSection.State.FocusedField? + @Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + Section { + Grid(alignment: .leading, horizontalSpacing: 40) { + GridRow { + label(for: .maximum) + TextField( + "Maximum", + value: $store.maxPressure, + fractionLength: 2, + prompt: Text("Max Static Pressure") + ) + .decimalPad() + .focused($focusedField, equals: .maximum) + } + GridRow { + label(for: .minimum) + TextField( + "Minimum", + value: $store.minPressure, + fractionLength: 2, + prompt: Text("Min Static Pressure") + ) + .decimalPad() + .focused($focusedField, equals: .minimum) + } + GridRow { + label(for: .rated) + TextField( + "Rated", + value: $store.ratedPressure, + fractionLength: 2, + prompt: Text("Rated Static Pressure") + ) + .decimalPad() + .focused($focusedField, equals: .rated) + } + } + .dynamicBottomPadding() + + } header: { + HStack { + SectionHeaderLabel("Rated Static Pressures") + Spacer() + InfoButton { store.send(.delegate(.infoButtonTapped)) } + } + } + .bind($focusedField, to: $store.focusedField) + } + + private func label(for field: RatedStaticPressuresSection.State.FocusedField) -> some View { + TextLabel(field.label) + } +}