Files
swift-estimated-pressures-core/Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift

406 lines
11 KiB
Swift

import ComposableArchitecture
import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct EquipmentSettingsForm {
@CasePathable
public enum InfoView {
case capacities
case ratedStaticPressures
}
@Reducer(state: .equatable)
public enum Destination {
case infoView(InfoViewFeature)
}
@ObservableState
public struct State: Equatable {
@Presents public var destination: Destination.State?
public var coolingCapacity: CoolingCapacity
public var equipmentType: EquipmentType
public var fanType: FanType
public var focusedField: Field? = nil
public var heatingCapacity: Double?
public var ratedStaticPressures: RatedStaticPressures
public init(
coolingCapacity: CoolingCapacity = .default,
destination: Destination.State? = nil,
equipmentType: EquipmentType = .airHandler,
fanType: FanType = .constantSpeed,
heatingCapacity: Double? = nil,
ratedStaticPressures: RatedStaticPressures = .init()
) {
self.coolingCapacity = coolingCapacity
self.destination = destination
self.equipmentType = equipmentType
self.fanType = fanType
self.heatingCapacity = heatingCapacity
self.ratedStaticPressures = ratedStaticPressures
}
public var isValid: Bool {
guard equipmentType == .furnaceAndCoil else { return true }
return heatingCapacity != nil
}
// Note: These need to be in display order.
public enum Field: Hashable, CaseIterable, FocusableField {
case heatingCapacity
case minimumStaticPressure
case maximumStaticPressure
case ratedStaticPressure
}
}
public enum Action: BindableAction, ViewAction {
case binding(BindingAction<State>)
case destination(PresentationAction<Destination.Action>)
case view(View)
@CasePathable
public enum View {
case infoButtonTapped(InfoView)
case nextButtonTapped
case resetButtonTapped
case submitField
}
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { state, action in
switch action {
case .binding:
return .none
case .destination(.dismiss):
state.destination = nil
return .none
case .destination:
return .none
case let .view(action):
switch action {
case let .infoButtonTapped(infoView):
state.destination = .infoView(.init(view: infoView))
return .none
case .nextButtonTapped:
#warning("Fix me.")
return .none
case .resetButtonTapped:
state.heatingCapacity = nil
return .none
case .submitField:
state.focusedField = state.focusedField?.next
return .none
}
}
}
.ifLet(\.$destination, action: \.destination)
}
}
@ViewAction(for: EquipmentSettingsForm.self)
public struct EquipmentSettingsFormView: View {
@FocusState private var focusedField: EquipmentSettingsForm.State.Field?
@Bindable public var store: StoreOf<EquipmentSettingsForm>
public init(store: StoreOf<EquipmentSettingsForm>) {
self.store = store
self.focusedField = store.focusedField
}
public var body: some View {
Form {
Section {
EmptyView()
} header: {
Text("Equipment Type")
} footer: {
EquipmentTypePicker(selection: $store.equipmentType)
.pickerStyle(.segmented)
}
Section {
EmptyView()
} header: {
Text("Fan Type")
#if os(macOS)
.font(.title2)
#endif
} footer: {
FanTypePicker(selection: $store.fanType)
.pickerStyle(.segmented)
}
Section {
Grid(alignment: .leading, horizontalSpacing: 40) {
GridRow {
TextLabel("Cooling")
Picker("Cooling Capcity", selection: $store.coolingCapacity) {
ForEach(CoolingCapacity.allCases) {
Text($0.description)
.tag($0)
}
}
}
if store.equipmentType == .furnaceAndCoil {
GridRow {
TextLabel("Heating")
textField(
"Heating Capacity",
value: $store.heatingCapacity,
fractionLength: 0
)
.focused($focusedField, equals: .heatingCapacity)
.numberPad()
}
}
}
} header: {
header("Capacities", infoView: .capacities)
}
Section {
Grid(alignment: .leading, horizontalSpacing: 40) {
GridRow {
TextLabel("Minimum")
textField(
"Minimum Pressure",
value: $store.ratedStaticPressures.minimum,
fractionLength: 2
)
.focused($focusedField, equals: .minimumStaticPressure)
.decimalPad()
}
GridRow {
TextLabel("Maximum")
textField(
"Maximum Pressure",
value: $store.ratedStaticPressures.maximum,
fractionLength: 2
)
.focused($focusedField, equals: .maximumStaticPressure)
.decimalPad()
}
GridRow {
TextLabel("Rated")
textField(
"Rated Pressure",
value: $store.ratedStaticPressures.rated,
fractionLength: 2
)
.focused($focusedField, equals: .ratedStaticPressure)
.decimalPad()
}
}
} header: {
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)
// }
}
.labelsHidden()
.bind($focusedField, to: $store.focusedField)
.navigationTitle("Equipment Data")
.textLabelStyle(.boldSecondary)
.textFieldStyle(.roundedBorder)
.sheet(
item: $store.scope(
state: \.destination?.infoView,
action: \.destination.infoView
)
) { store in
NavigationStack {
InfoView(store: store)
}
}
}
private func header(
_ title: String,
infoView: EquipmentSettingsForm.InfoView
) -> some View {
HStack {
Text(title)
Spacer()
InfoButton { send(.infoButtonTapped(infoView)) }
}
}
// private func percentField(
// _ title: LocalizedStringKey,
// value: Binding<Double>
// ) -> some View {
// TextField(title, value: value, format: .percent, prompt: Text(title))
// }
private func textField(
_ title: String,
value: Binding<Double>,
fractionLength: Int = 2
) -> some View {
TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title))
}
private func textField(
_ title: String,
value: Binding<Double?>,
fractionLength: Int = 2
) -> some View {
TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title))
}
}
fileprivate struct BudgetFlagViewStyle: FlaggedViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.flagged.flagImage
Spacer()
configuration.flagged.messageView
}
}
}
fileprivate extension InfoViewFeature.State {
init(view: EquipmentSettingsForm.InfoView) {
switch view {
case .capacities:
self.init(
title: "Capacities",
body: """
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 .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.
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.
// """
// )
}
}
}
#Preview {
NavigationStack {
EquipmentSettingsFormView(
store: Store(
initialState: EquipmentSettingsForm.State()
) {
EquipmentSettingsForm()
}
)
#if os(macOS)
.frame(width: 400, height: 600)
.padding()
#endif
}
}