352 lines
9.7 KiB
Swift
352 lines
9.7 KiB
Swift
import ComposableArchitecture
|
|
import FlaggedViews
|
|
import InfoViewFeature
|
|
import SharedModels
|
|
import Styleguide
|
|
import SwiftUI
|
|
|
|
@Reducer
|
|
public struct EquipmentSettingsForm {
|
|
|
|
@CasePathable
|
|
public enum InfoView {
|
|
case capacities
|
|
case manufacturersIncludedFilterPressureDrop
|
|
case ratedStaticPressures
|
|
}
|
|
|
|
@Reducer(state: .equatable)
|
|
public enum Destination {
|
|
case infoView(InfoViewFeature)
|
|
}
|
|
|
|
@ObservableState
|
|
public struct State: Equatable {
|
|
@Presents public var destination: Destination.State?
|
|
public var includesFilterDrop: Bool
|
|
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,
|
|
includesFilterDrop: Bool = false,
|
|
equipmentType: EquipmentMeasurement.EquipmentType = .airHandler,
|
|
sharedSettings: Shared<SharedPressureEstimationState>
|
|
) {
|
|
self.destination = destination
|
|
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 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 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 }
|
|
}
|
|
}
|
|
|
|
public enum Action: BindableAction, ViewAction {
|
|
case binding(BindingAction<State>)
|
|
case destination(PresentationAction<Destination.Action>)
|
|
case ratedStaticPressures(RatedStaticPressuresSection.Action)
|
|
case view(View)
|
|
|
|
@CasePathable
|
|
public enum View {
|
|
case infoButtonTapped(InfoView)
|
|
case resetButtonTapped
|
|
case submitField
|
|
}
|
|
}
|
|
|
|
public var body: some Reducer<State, Action> {
|
|
BindingReducer()
|
|
Scope(state: \.ratedStaticPressures, action: \.ratedStaticPressures) {
|
|
RatedStaticPressuresSection()
|
|
}
|
|
Reduce<State, Action> { 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
|
|
|
|
case .destination(.dismiss):
|
|
state.destination = nil
|
|
return .none
|
|
|
|
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 {
|
|
|
|
case let .infoButtonTapped(infoView):
|
|
state.destination = .infoView(.init(view: infoView))
|
|
return .none
|
|
|
|
case .resetButtonTapped:
|
|
state.sharedSettings.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 {
|
|
EquipmentTypePicker(selection: $store.equipmentType)
|
|
.dynamicBottomPadding()
|
|
.pickerStyle(.segmented)
|
|
} header: {
|
|
SectionHeaderLabel("Equipment Type")
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
|
|
Section {
|
|
FanTypePicker(selection: $store.sharedSettings.fanType)
|
|
.dynamicBottomPadding()
|
|
.pickerStyle(.segmented)
|
|
} header: {
|
|
SectionHeaderLabel("Fan Type")
|
|
}
|
|
.listRowBackground(Color.clear)
|
|
|
|
Section {
|
|
Grid(alignment: .leading, horizontalSpacing: 40) {
|
|
GridRow {
|
|
TextLabel(
|
|
store.equipmentType == .furnaceAndCoil
|
|
? "Cooling"
|
|
: "Capacity"
|
|
)
|
|
Spacer()
|
|
CoolingCapacityPicker(
|
|
selection: $store.sharedSettings.coolingCapacity
|
|
)
|
|
}
|
|
if store.equipmentType == .furnaceAndCoil {
|
|
GridRow {
|
|
TextLabel("Heating")
|
|
textField(
|
|
"Heating Capacity",
|
|
value: $store.sharedSettings.heatingCapacity,
|
|
fractionLength: 0
|
|
)
|
|
.focused($focusedField, equals: .heatingCapacity)
|
|
.numberPad()
|
|
.gridCellColumns(2)
|
|
}
|
|
}
|
|
}
|
|
.dynamicBottomPadding()
|
|
} header: {
|
|
if store.equipmentType == .airHandler {
|
|
EmptyView()
|
|
} else {
|
|
SectionHeaderLabel("Capacities")
|
|
}
|
|
}
|
|
|
|
RatedStaticPressuresSectionView(
|
|
store: store.scope(state: \.ratedStaticPressures, action: \.ratedStaticPressures)
|
|
)
|
|
|
|
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
|
|
)
|
|
.focused($focusedField, equals: .manufacturersIncludedFilterPressureDrop)
|
|
.decimalPad()
|
|
.padding(.leading, 40)
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
header(infoView: .manufacturersIncludedFilterPressureDrop) {
|
|
VStack(alignment: .leading) {
|
|
Text("Manufacturer's")
|
|
Text("Filter Pressure Drop")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.applyFormStyle()
|
|
.bind($focusedField, to: $store.focusedField)
|
|
.sheet(
|
|
item: $store.scope(
|
|
state: \.destination?.infoView,
|
|
action: \.destination.infoView
|
|
)
|
|
) { store in
|
|
NavigationStack {
|
|
InfoView(store: store)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func header<Label: View>(
|
|
infoView: EquipmentSettingsForm.InfoView,
|
|
label: @escaping () -> Label
|
|
) -> some View {
|
|
HStack {
|
|
SectionHeaderLabel { label() }
|
|
Spacer()
|
|
InfoButton { send(.infoButtonTapped(infoView)) }
|
|
}
|
|
}
|
|
|
|
private func header(
|
|
_ title: String,
|
|
infoView: EquipmentSettingsForm.InfoView
|
|
) -> some View {
|
|
header(infoView: infoView) { Text(title) }
|
|
}
|
|
|
|
private func textField(
|
|
_ title: String,
|
|
value: Binding<Double?>,
|
|
fractionLength: Int = 2
|
|
) -> some View {
|
|
TextField(title, value: value, fractionLength: fractionLength, prompt: Text(title))
|
|
.onSubmit { send(.submitField) }
|
|
}
|
|
}
|
|
|
|
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 .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.
|
|
|
|
The defaults are generally acceptable for most unitary equipment.
|
|
"""
|
|
)
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
EquipmentSettingsFormView(
|
|
store: Store(
|
|
initialState: EquipmentSettingsForm.State(
|
|
sharedSettings: Shared(SharedPressureEstimationState())
|
|
)
|
|
) {
|
|
EquipmentSettingsForm()._printChanges()
|
|
}
|
|
)
|
|
#if os(macOS)
|
|
.frame(width: 400, height: 600)
|
|
.padding()
|
|
#endif
|
|
}
|
|
}
|