Feat: Fixing equipment settings form
This commit is contained in:
@@ -1,242 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import DependenciesAdditions
|
|
||||||
import EstimatedPressureDependency
|
|
||||||
import SharedModels
|
|
||||||
import Styleguide
|
|
||||||
import SwiftUI
|
|
||||||
import TCAExtras
|
|
||||||
|
|
||||||
@Reducer
|
|
||||||
public struct AirHandlerMeasurementForm {
|
|
||||||
|
|
||||||
@ObservableState
|
|
||||||
public struct State: Equatable {
|
|
||||||
public var calculatedMeasurement: EquipmentMeasurement.AirHandler?
|
|
||||||
public var focusedField: Field?
|
|
||||||
public var measurement: EquipmentMeasurement.AirHandler
|
|
||||||
public var updatedAirflow: Double?
|
|
||||||
public var fanType: FanType
|
|
||||||
public var ratedStaticPressures = RatedStaticPressures()
|
|
||||||
|
|
||||||
public init(
|
|
||||||
calculatedMeasurement: EquipmentMeasurement.AirHandler? = nil,
|
|
||||||
focusedField: Field? = nil,
|
|
||||||
measurement: EquipmentMeasurement.AirHandler = .init(),
|
|
||||||
updatedAirflow: Double? = nil,
|
|
||||||
fanType: FanType = .constantSpeed
|
|
||||||
) {
|
|
||||||
self.calculatedMeasurement = calculatedMeasurement
|
|
||||||
self.focusedField = focusedField
|
|
||||||
self.measurement = measurement
|
|
||||||
self.updatedAirflow = updatedAirflow
|
|
||||||
self.fanType = fanType
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isValid: Bool {
|
|
||||||
return measurement.returnPlenumPressure != nil
|
|
||||||
&& measurement.postFilterPressure != nil
|
|
||||||
&& measurement.postCoilPressure != nil
|
|
||||||
&& measurement.supplyPlenumPressure != nil
|
|
||||||
&& measurement.airflow != nil
|
|
||||||
&& updatedAirflow != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Field: String, Equatable, CaseIterable, FocusableField {
|
|
||||||
case returnPlenumPressure
|
|
||||||
case postFilterPressure
|
|
||||||
case postCoilPressure
|
|
||||||
case supplyPlenumPressure
|
|
||||||
case airflow
|
|
||||||
case updatedAirflow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Action: BindableAction, ViewAction {
|
|
||||||
case binding(BindingAction<State>)
|
|
||||||
case receive(TaskResult<EquipmentMeasurement.AirHandler?>)
|
|
||||||
case view(View)
|
|
||||||
|
|
||||||
@CasePathable
|
|
||||||
public enum View {
|
|
||||||
case infoButtonTapped
|
|
||||||
case resetButtonTapped
|
|
||||||
case submitField
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dependency(\.estimatedPressuresClient) var estimatedPressuresClient
|
|
||||||
@Dependency(\.logger["\(Self.self)"]) var logger
|
|
||||||
|
|
||||||
public var body: some Reducer<State, Action> {
|
|
||||||
BindingReducer()
|
|
||||||
Reduce<State, Action> { state, action in
|
|
||||||
switch action {
|
|
||||||
case .binding:
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case let .receive(.success(calculatedMeasurement)):
|
|
||||||
state.calculatedMeasurement = calculatedMeasurement
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .receive:
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case let .view(action):
|
|
||||||
switch action {
|
|
||||||
|
|
||||||
case .infoButtonTapped:
|
|
||||||
#warning("Fix me.")
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .resetButtonTapped:
|
|
||||||
state.measurement = .init()
|
|
||||||
state.updatedAirflow = nil
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .submitField:
|
|
||||||
state.focusedField = state.focusedField?.next
|
|
||||||
guard state.isValid else { return .none }
|
|
||||||
return calculateEstimates(state: state)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure(case: \.receive, .log(logger: logger))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func calculateEstimates(state: State) -> Effect<Action> {
|
|
||||||
.receive(action: \.receive) {
|
|
||||||
let result = try await estimatedPressuresClient.estimatedPressure(
|
|
||||||
for: .airHandler(state.measurement),
|
|
||||||
at: state.updatedAirflow ?? 0
|
|
||||||
)
|
|
||||||
|
|
||||||
guard case let .airHandler(airHandler) = result else {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
return airHandler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewAction(for: AirHandlerMeasurementForm.self)
|
|
||||||
public struct AirHandlerMeasurementFormView: View {
|
|
||||||
@FocusState private var focusedField: AirHandlerMeasurementForm.State.Field?
|
|
||||||
@Bindable public var store: StoreOf<AirHandlerMeasurementForm>
|
|
||||||
|
|
||||||
public init(
|
|
||||||
store: StoreOf<AirHandlerMeasurementForm>
|
|
||||||
) {
|
|
||||||
self.store = store
|
|
||||||
self.focusedField = store.focusedField
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
textField(
|
|
||||||
"Return Plenum Pressure",
|
|
||||||
value: $store.measurement.returnPlenumPressure
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .returnPlenumPressure)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.decimalPad()
|
|
||||||
|
|
||||||
textField(
|
|
||||||
"Post-Filter Pressure",
|
|
||||||
value: $store.measurement.postFilterPressure
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .postFilterPressure)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.decimalPad()
|
|
||||||
|
|
||||||
textField(
|
|
||||||
"Post-Coil Pressure",
|
|
||||||
value: $store.measurement.postCoilPressure
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .postCoilPressure)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.decimalPad()
|
|
||||||
|
|
||||||
textField(
|
|
||||||
"Supply Plenum Pressure",
|
|
||||||
value: $store.measurement.supplyPlenumPressure
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .supplyPlenumPressure)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.decimalPad()
|
|
||||||
|
|
||||||
textField(
|
|
||||||
"Airflow",
|
|
||||||
value: $store.measurement.airflow,
|
|
||||||
fractionLength: 0
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .airflow)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.numberPad()
|
|
||||||
|
|
||||||
} header: {
|
|
||||||
HStack {
|
|
||||||
Text("System Measurements")
|
|
||||||
Spacer()
|
|
||||||
InfoButton {
|
|
||||||
send(.infoButtonTapped)
|
|
||||||
}
|
|
||||||
.font(.title3)
|
|
||||||
.labelStyle(.iconOnly)
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
FanTypePicker(selection: $store.fanType)
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
|
|
||||||
textField(
|
|
||||||
"Updated Airflow",
|
|
||||||
value: $store.updatedAirflow,
|
|
||||||
fractionLength: 0
|
|
||||||
)
|
|
||||||
.focused($focusedField, equals: .updatedAirflow)
|
|
||||||
.onSubmit { send(.submitField) }
|
|
||||||
.numberPad()
|
|
||||||
|
|
||||||
} header: {
|
|
||||||
Text("Estimate Settings")
|
|
||||||
} footer: {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
ResetButton { send(.resetButtonTapped) }
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let calculatedMeasurement = store.calculatedMeasurement {
|
|
||||||
Section {
|
|
||||||
Text("Display calculated measurement here.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.bind($focusedField, to: $store.focusedField)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func textField(
|
|
||||||
_ title: LocalizedStringKey,
|
|
||||||
value: Binding<Double?>,
|
|
||||||
fractionLength: Int = 2
|
|
||||||
) -> TextField<Text> {
|
|
||||||
.init(title, value: value, fractionLength: fractionLength, prompt: Text(title))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
AirHandlerMeasurementFormView(
|
|
||||||
store: Store(initialState: AirHandlerMeasurementForm.State()) {
|
|
||||||
AirHandlerMeasurementForm()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -16,35 +16,38 @@ public struct EquipmentMeasurementForm {
|
|||||||
@ObservableState
|
@ObservableState
|
||||||
public struct State: Equatable {
|
public struct State: Equatable {
|
||||||
@Presents public var destination: Destination.State?
|
@Presents public var destination: Destination.State?
|
||||||
|
public var allowEquipmentTypeSelection: Bool
|
||||||
public var equipmentType: EquipmentType
|
public var equipmentType: EquipmentType
|
||||||
public var focusedField: Field?
|
public var focusedField: Field?
|
||||||
public var fields: FormFields
|
public var measurements: Measurements
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
allowEquipmentTypeSelection: Bool = true,
|
||||||
destination: Destination.State? = nil,
|
destination: Destination.State? = nil,
|
||||||
equipmentType: EquipmentType = .airHandler,
|
equipmentType: EquipmentType = .airHandler,
|
||||||
focusedField: Field? = nil,
|
focusedField: Field? = nil,
|
||||||
fields: FormFields = .init()
|
measurements: Measurements = .init()
|
||||||
) {
|
) {
|
||||||
|
self.allowEquipmentTypeSelection = allowEquipmentTypeSelection
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.equipmentType = equipmentType
|
self.equipmentType = equipmentType
|
||||||
self.focusedField = focusedField
|
self.focusedField = focusedField
|
||||||
self.fields = fields
|
self.measurements = measurements
|
||||||
}
|
}
|
||||||
|
|
||||||
public var equipmentMeasurement: EquipmentMeasurement {
|
public var equipmentMeasurement: EquipmentMeasurement {
|
||||||
fields.equipmentMeasurement(type: equipmentType)
|
measurements.equipmentMeasurement(type: equipmentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isValid: Bool {
|
public var isValid: Bool {
|
||||||
fields.airflow != nil
|
measurements.airflow != nil
|
||||||
&& fields.returnPlenumPressure != nil
|
&& measurements.returnPlenumPressure != nil
|
||||||
&& fields.postFilterPressure != nil
|
&& measurements.postFilterPressure != nil
|
||||||
&& fields.coilPressure != nil
|
&& measurements.coilPressure != nil
|
||||||
&& fields.supplyPlenumPressure != nil
|
&& measurements.supplyPlenumPressure != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct FormFields: Equatable {
|
public struct Measurements: Equatable {
|
||||||
public var airflow: Double?
|
public var airflow: Double?
|
||||||
public var returnPlenumPressure: Double?
|
public var returnPlenumPressure: Double?
|
||||||
public var postFilterPressure: Double?
|
public var postFilterPressure: Double?
|
||||||
@@ -112,7 +115,7 @@ public struct EquipmentMeasurementForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Field: Hashable, CaseIterable, FocusableField, Identifiable {
|
public enum Field: CaseIterable, FocusableField, Hashable, Identifiable {
|
||||||
case returnPlenumPressure
|
case returnPlenumPressure
|
||||||
case postFilterPressure
|
case postFilterPressure
|
||||||
case coilPressure
|
case coilPressure
|
||||||
@@ -148,15 +151,15 @@ public struct EquipmentMeasurementForm {
|
|||||||
case .destination:
|
case .destination:
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
|
|
||||||
case let .view(action):
|
case let .view(action):
|
||||||
switch action {
|
switch action {
|
||||||
|
|
||||||
case .infoButtonTapped:
|
case .infoButtonTapped:
|
||||||
|
state.destination = .infoView(.init())
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .resetButtonTapped:
|
case .resetButtonTapped:
|
||||||
state.fields = .init()
|
state.measurements = .init()
|
||||||
return .none
|
return .none
|
||||||
|
|
||||||
case .submitField:
|
case .submitField:
|
||||||
@@ -220,21 +223,21 @@ public struct EquipmentMeasurementFormView: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
if store.allowEquipmentTypeSelection {
|
||||||
|
Section {
|
||||||
} header: {
|
} header: {
|
||||||
Text("Equipment Type")
|
Text("Equipment Type")
|
||||||
} footer: {
|
} footer: {
|
||||||
Picker("Equipment Type", selection: $store.equipmentType) {
|
Picker("Equipment Type", selection: $store.equipmentType) {
|
||||||
ForEach(EquipmentType.allCases) {
|
ForEach(EquipmentType.allCases) {
|
||||||
Text($0.description)
|
Text($0.description)
|
||||||
.tag($0)
|
.tag($0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Grid(alignment: .leading, horizontalSpacing: 40) {
|
Grid(alignment: .leading, horizontalSpacing: 40) {
|
||||||
ForEach(store.pressureFields) { field in
|
ForEach(store.pressureFields) { field in
|
||||||
@@ -268,8 +271,16 @@ public struct EquipmentMeasurementFormView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle("Existing Measurements")
|
||||||
.textLabelStyle(.boldSecondary)
|
.textLabelStyle(.boldSecondary)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.sheet(
|
||||||
|
item: $store.scope(state: \.destination?.infoView, action: \.destination.infoView)
|
||||||
|
){ store in
|
||||||
|
NavigationStack {
|
||||||
|
InfoView(store: store)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func textField(
|
private func textField(
|
||||||
@@ -280,15 +291,15 @@ public struct EquipmentMeasurementFormView: View {
|
|||||||
|
|
||||||
switch field {
|
switch field {
|
||||||
case .returnPlenumPressure:
|
case .returnPlenumPressure:
|
||||||
value = $store.fields.returnPlenumPressure
|
value = $store.measurements.returnPlenumPressure
|
||||||
case .postFilterPressure:
|
case .postFilterPressure:
|
||||||
value = $store.fields.postFilterPressure
|
value = $store.measurements.postFilterPressure
|
||||||
case .coilPressure:
|
case .coilPressure:
|
||||||
value = $store.fields.coilPressure
|
value = $store.measurements.coilPressure
|
||||||
case .supplyPlenumPressure:
|
case .supplyPlenumPressure:
|
||||||
value = $store.fields.supplyPlenumPressure
|
value = $store.measurements.supplyPlenumPressure
|
||||||
case .airflow:
|
case .airflow:
|
||||||
value = $store.fields.airflow
|
value = $store.measurements.airflow
|
||||||
fractionLength = 0
|
fractionLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,13 +336,26 @@ public struct EquipmentMeasurementFormView: View {
|
|||||||
.decimalPad()
|
.decimalPad()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension InfoViewFeature.State {
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.init(
|
||||||
|
title: "Existing Measurements",
|
||||||
|
body: """
|
||||||
|
Record the current static pressure and airflow measurements of the existing system.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
EquipmentMeasurementFormView(
|
NavigationStack {
|
||||||
store: Store(initialState: EquipmentMeasurementForm.State()) {
|
EquipmentMeasurementFormView(
|
||||||
EquipmentMeasurementForm()
|
store: Store(initialState: EquipmentMeasurementForm.State()) {
|
||||||
}
|
EquipmentMeasurementForm()
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
405
Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift
Normal file
405
Sources/PressureEstimationsFeature/EquipmentSettingsForm.swift
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import SharedModels
|
|
||||||
import Styleguide
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@Reducer
|
|
||||||
public struct EstimateSettingsForm {
|
|
||||||
|
|
||||||
@CasePathable
|
|
||||||
public enum InfoView {
|
|
||||||
case budgets
|
|
||||||
case updatedAirflow
|
|
||||||
}
|
|
||||||
|
|
||||||
@Reducer(state: .equatable)
|
|
||||||
public enum Destination {
|
|
||||||
case infoView(InfoViewFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObservableState
|
|
||||||
public struct State: Equatable {
|
|
||||||
|
|
||||||
@Presents public var destination: Destination.State?
|
|
||||||
public var budgets: BudgetedPercentEnvelope
|
|
||||||
public let equipmentMeasurement: EquipmentMeasurement
|
|
||||||
public var fanType: FanType
|
|
||||||
public var focusedField: Field? = nil
|
|
||||||
public var updatedAirflow: Double?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
destination: Destination.State? = nil,
|
|
||||||
equipmentMeasurement: EquipmentMeasurement,
|
|
||||||
fanType: FanType = .constantSpeed,
|
|
||||||
updatedAirflow: Double? = nil
|
|
||||||
) {
|
|
||||||
self.destination = destination
|
|
||||||
self.equipmentMeasurement = equipmentMeasurement
|
|
||||||
self.budgets = .init(
|
|
||||||
equipmentType: equipmentMeasurement.equipmentType,
|
|
||||||
fanType: fanType
|
|
||||||
)
|
|
||||||
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<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(\.fanType):
|
|
||||||
state.budgets = .init(
|
|
||||||
equipmentType: state.equipmentMeasurement.equipmentType,
|
|
||||||
fanType: state.fanType
|
|
||||||
)
|
|
||||||
return .none
|
|
||||||
|
|
||||||
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.budgets = .init(
|
|
||||||
equipmentType: state.equipmentMeasurement.equipmentType,
|
|
||||||
fanType: state.fanType
|
|
||||||
)
|
|
||||||
state.updatedAirflow = nil
|
|
||||||
return .none
|
|
||||||
|
|
||||||
case .submitField:
|
|
||||||
state.focusedField = state.focusedField?.next
|
|
||||||
return .none
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ifLet(\.$destination, action: \.destination)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewAction(for: EstimateSettingsForm.self)
|
|
||||||
public struct EstimateSettingsFormView: View {
|
|
||||||
|
|
||||||
@FocusState private var focusedField: EstimateSettingsForm.State.Field?
|
|
||||||
@Bindable public var store: StoreOf<EstimateSettingsForm>
|
|
||||||
|
|
||||||
public init(store: StoreOf<EstimateSettingsForm>) {
|
|
||||||
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.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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 {
|
|
||||||
TextField(
|
|
||||||
"Airflow",
|
|
||||||
value: $store.updatedAirflow,
|
|
||||||
fractionLength: 0
|
|
||||||
)
|
|
||||||
} header: {
|
|
||||||
HStack {
|
|
||||||
Text("Updated Airflow")
|
|
||||||
Spacer()
|
|
||||||
InfoButton { send(.infoButtonTapped(.updatedAirflow)) }
|
|
||||||
}
|
|
||||||
} footer: {
|
|
||||||
HStack {
|
|
||||||
ResetButton { send(.resetButtonTapped) }
|
|
||||||
Spacer()
|
|
||||||
NextButton { send(.nextButtonTapped) }
|
|
||||||
.disabled(!store.isValid)
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.bind($focusedField, to: $store.focusedField)
|
|
||||||
.navigationTitle("Estimate Settings")
|
|
||||||
.sheet(
|
|
||||||
item: $store.scope(
|
|
||||||
state: \.destination?.infoView,
|
|
||||||
action: \.destination.infoView
|
|
||||||
)
|
|
||||||
) { store in
|
|
||||||
NavigationStack {
|
|
||||||
InfoView(store: store)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func percentField(
|
|
||||||
_ title: LocalizedStringKey,
|
|
||||||
value: Binding<Double>
|
|
||||||
) -> 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate extension InfoViewFeature.State {
|
|
||||||
init(view: EstimateSettingsForm.InfoView) {
|
|
||||||
switch view {
|
|
||||||
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 .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 {
|
|
||||||
EstimateSettingsFormView(
|
|
||||||
store: Store(
|
|
||||||
initialState: EstimateSettingsForm.State(equipmentMeasurement: .furnaceAndCoil(.init()))
|
|
||||||
) {
|
|
||||||
EstimateSettingsForm()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
#if os(macOS)
|
|
||||||
.frame(width: 400, height: 600)
|
|
||||||
.padding()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum CoolingCapacity: Double, Equatable, CaseIterable {
|
public enum CoolingCapacity: Double, Hashable, CaseIterable, Identifiable, CustomStringConvertible {
|
||||||
case half = 0.5
|
case half = 0.5
|
||||||
case threeQuarter = 0.75
|
case threeQuarter = 0.75
|
||||||
case one = 1
|
case one = 1
|
||||||
@@ -12,5 +12,32 @@ public enum CoolingCapacity: Double, Equatable, CaseIterable {
|
|||||||
case four = 4
|
case four = 4
|
||||||
case five = 5
|
case five = 5
|
||||||
|
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
public static var `default`: Self { .three }
|
public static var `default`: Self { .three }
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case .half:
|
||||||
|
return "1/2 Ton"
|
||||||
|
case .threeQuarter:
|
||||||
|
return "3/4 Ton"
|
||||||
|
case .one:
|
||||||
|
return "1 Ton"
|
||||||
|
case .oneAndAHalf:
|
||||||
|
return "1.5 Tons"
|
||||||
|
case .two:
|
||||||
|
return "2 Tons"
|
||||||
|
case .twoAndAHalf:
|
||||||
|
return "2.5 Tons"
|
||||||
|
case .three:
|
||||||
|
return "3 Tons"
|
||||||
|
case .threeAndAHalf:
|
||||||
|
return "3.5 Tons"
|
||||||
|
case .four:
|
||||||
|
return "4 Tons"
|
||||||
|
case .five:
|
||||||
|
return "5 Tons"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
#warning("Make values non-optional")
|
||||||
public enum EquipmentMeasurement: Equatable {
|
public enum EquipmentMeasurement: Equatable {
|
||||||
|
|
||||||
case airHandler(AirHandler)
|
case airHandler(AirHandler)
|
||||||
|
|||||||
19
Sources/Styleguide/EquipmentTypePicker.swift
Normal file
19
Sources/Styleguide/EquipmentTypePicker.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import SharedModels
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct EquipmentTypePicker: View {
|
||||||
|
@Binding var selection: EquipmentType
|
||||||
|
|
||||||
|
public init(selection: Binding<EquipmentType>) {
|
||||||
|
self._selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Picker("Equipment Type", selection: $selection) {
|
||||||
|
ForEach(EquipmentType.allCases) {
|
||||||
|
Text($0.description)
|
||||||
|
.tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,109 +34,3 @@ extension FlaggedView where Label == EmptyView {
|
|||||||
self.init(flagged: flagged) { EmptyView() }
|
self.init(flagged: flagged) { EmptyView() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//public struct FlaggedView: View {
|
|
||||||
//
|
|
||||||
// public let style: Style?
|
|
||||||
// public let title: String
|
|
||||||
// public let flagged: Flagged
|
|
||||||
// public let fractionLength: Int
|
|
||||||
//
|
|
||||||
// public init(
|
|
||||||
// style: Style? = nil,
|
|
||||||
// title: String,
|
|
||||||
// flagged: Flagged,
|
|
||||||
// fractionLength: Int = 3
|
|
||||||
// ) {
|
|
||||||
// self.style = style
|
|
||||||
// self.title = title
|
|
||||||
// self.flagged = flagged
|
|
||||||
// self.fractionLength = fractionLength
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public init(
|
|
||||||
// _ title: String,
|
|
||||||
// flagged: Flagged,
|
|
||||||
// style: Style? = nil,
|
|
||||||
// fractionLength: Int = 3
|
|
||||||
// ) {
|
|
||||||
// self.style = style
|
|
||||||
// self.title = title
|
|
||||||
// self.flagged = flagged
|
|
||||||
// self.fractionLength = fractionLength
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @ViewBuilder
|
|
||||||
// private var messageView: some View {
|
|
||||||
// if let message = flagged.message {
|
|
||||||
// HStack {
|
|
||||||
// Text(flagged.projectedValue.key.title)
|
|
||||||
// .bold()
|
|
||||||
// .foregroundStyle(flagged.projectedValue.key.flagColor)
|
|
||||||
//
|
|
||||||
// Text(message)
|
|
||||||
// .foregroundStyle(Color.secondary)
|
|
||||||
// }
|
|
||||||
// .font(.caption)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public var body: some View {
|
|
||||||
// switch style {
|
|
||||||
// case .none:
|
|
||||||
// VStack(alignment: .leading, spacing: 8) {
|
|
||||||
// HStack {
|
|
||||||
// Text(title)
|
|
||||||
// .foregroundStyle(Color.secondary)
|
|
||||||
// Spacer()
|
|
||||||
// Text(
|
|
||||||
// flagged.wrappedValue,
|
|
||||||
// format: .number.precision(.fractionLength(fractionLength))
|
|
||||||
// )
|
|
||||||
// Image(systemName: "flag.fill")
|
|
||||||
// .foregroundStyle(flagged.projectedValue.key.flagColor)
|
|
||||||
// }
|
|
||||||
// messageView
|
|
||||||
// }
|
|
||||||
// case .some(.gridRow):
|
|
||||||
// GridRow {
|
|
||||||
// VStack(alignment: .leading, spacing: 8) {
|
|
||||||
// Text(title)
|
|
||||||
// .foregroundStyle(Color.secondary)
|
|
||||||
// messageView
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Spacer()
|
|
||||||
//
|
|
||||||
// Text(flagged.wrappedValue, format: .number.precision(.fractionLength(fractionLength)))
|
|
||||||
// .gridCellAnchor(.trailing)
|
|
||||||
//
|
|
||||||
// Image(systemName: "flag.fill")
|
|
||||||
// .foregroundStyle(flagged.flagColor)
|
|
||||||
// .gridCellAnchor(.trailing)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public enum Style {
|
|
||||||
// case gridRow
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public static func gridRow(_ title: String, flagged: Flagged) -> Self {
|
|
||||||
// .init(style: .gridRow, title: title, flagged: flagged)
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
//#Preview {
|
|
||||||
// List {
|
|
||||||
// Grid(horizontalSpacing: 20) {
|
|
||||||
// FlaggedView(style: .gridRow, title: "Test", flagged: .error(message: "Error message"))
|
|
||||||
// FlaggedView(style: .gridRow, title: "Test", flagged: .init(wrappedValue: 0.5, .result(.good("Good message"))))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// FlaggedView(style: nil, title: "Test", flagged: .error(message: "Error message"))
|
|
||||||
// FlaggedView(style: nil, title: "Test", flagged: .init(wrappedValue: 0.5, .result(.good("Good message"))))
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|||||||
@@ -44,13 +44,12 @@ public struct InfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 40) {
|
VStack {
|
||||||
Text(store.bodyText)
|
TextLabel(store.bodyText)
|
||||||
.font(.callout)
|
|
||||||
.padding()
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.navigationTitle(store.titleText)
|
.navigationTitle(store.titleText)
|
||||||
|
.textLabelStyle(.secondary)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.navigationBarBackButtonHidden()
|
.navigationBarBackButtonHidden()
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
|||||||
@@ -30,4 +30,32 @@ extension TextField where Label == Text {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
_ titleKey: LocalizedStringKey,
|
||||||
|
value: Binding<Double>,
|
||||||
|
fractionLength: Int,
|
||||||
|
prompt: Text? = nil
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
titleKey,
|
||||||
|
value: value,
|
||||||
|
format: .number.precision(.fractionLength(fractionLength)),
|
||||||
|
prompt: prompt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public init<S: StringProtocol>(
|
||||||
|
_ titleKey: S,
|
||||||
|
value: Binding<Double>,
|
||||||
|
fractionLength: Int,
|
||||||
|
prompt: Text? = nil
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
titleKey,
|
||||||
|
value: value,
|
||||||
|
format: .number.precision(.fractionLength(fractionLength)),
|
||||||
|
prompt: prompt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user