import ComposableArchitecture import SharedModels import Styleguide import SwiftUI @Reducer public struct EstimateSettingsForm { @ObservableState public struct State: Equatable { public var budgets: BudgetedPercentEnvelope public let equipmentType: EquipmentType public var fanType: FanType public var focusedField: Field? = nil public var updatedAirflow: Double? public init( equipmentType: EquipmentType, fanType: FanType = .constantSpeed, updatedAirflow: Double? = nil ) { self.budgets = .init(equipmentType: equipmentType, fanType: fanType) self.equipmentType = equipmentType self.fanType = fanType self.updatedAirflow = updatedAirflow } public var isValid: Bool { updatedAirflow != nil && Int(budgets.total.rawValue) == 100 } public enum Field: Hashable, CaseIterable, FocusableField { case coilBudget case filterBudget case returnPlenumBudget case supplyPlenumBudget case updatedAirflow } } public enum Action: BindableAction, ViewAction { case binding(BindingAction) case view(View) @CasePathable public enum View { case infoButtonTapped(InfoButton) case resetButtonTapped case submitField @CasePathable public enum InfoButton { case budgets case updatedAirflow } } } public var body: some Reducer { BindingReducer() Reduce { state, action in switch action { case .binding(\.fanType): state.budgets = .init( equipmentType: state.equipmentType, fanType: state.fanType ) return .none case .binding: return .none case let .view(action): switch action { case .infoButtonTapped: #warning("Fix me.") return .none case .resetButtonTapped: state.budgets = .init(equipmentType: state.equipmentType, fanType: state.fanType) state.updatedAirflow = nil return .none case .submitField: state.focusedField = state.focusedField?.next return .none } } } } } @ViewAction(for: EstimateSettingsForm.self) public struct EstimateSettingsFormView: View { @FocusState private var focusedField: EstimateSettingsForm.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 { EmptyView() } header: { TextLabel("Fan Type") #if os(macOS) .font(.title2) #endif } footer: { FanTypePicker(selection: $store.fanType) .pickerStyle(.segmented) } Section { Grid(alignment: .leading, horizontalSpacing: 40) { if store.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() } } .textLabelStyle(.boldSecondary) .textFieldStyle(.roundedBorder) } header: { VStack { HStack { Text("Budgets") Spacer() InfoButton { send(.infoButtonTapped(.budgets)) } } #if os(macOS) .font(.title2) .padding(.top, 20) #endif FlaggedView( flagged: Flagged(wrappedValue: store.budgets.total.rawValue, .percent()) ) .flaggedViewStyle(BudgetFlagViewStyle()) } } Section { } header: { HStack { Text("Updated Airflow") Spacer() InfoButton { send(.infoButtonTapped(.updatedAirflow)) } } } footer: { HStack { ResetButton { send(.resetButtonTapped) } Spacer() Button("Next") { #warning("Fix me.") } } .padding(.top) } } .labelsHidden() .bind($focusedField, to: $store.focusedField) .navigationTitle("Estimate Settings") } private func percentField( _ title: LocalizedStringKey, value: Binding ) -> some View { TextField(title, value: value, format: .percent, prompt: Text(title)) } } fileprivate struct BudgetFlagViewStyle: FlaggedViewStyle { func makeBody(configuration: Configuration) -> some View { HStack { configuration.flagged.flagImage Spacer() configuration.flagged.messageView } } } #Preview { NavigationStack { EstimateSettingsFormView( store: Store( initialState: EstimateSettingsForm.State(equipmentType: .furnaceAndCoil) ) { EstimateSettingsForm() } ) #if os(macOS) .frame(width: 400, height: 600) .padding() #endif } }