Feat: Working on pressure estimations feature
This commit is contained in:
@@ -5,16 +5,17 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "swift-estimated-pressures-core",
|
name: "swift-estimated-pressures-core",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v16),
|
.iOS(.v17),
|
||||||
.macOS(.v13),
|
.macOS(.v14),
|
||||||
.tvOS(.v14),
|
.tvOS(.v14),
|
||||||
.watchOS(.v7),
|
.watchOS(.v7),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "CalculateAtFeature", targets: ["CalculateAtFeature"]),
|
.library(name: "CalculateAtFeature", targets: ["CalculateAtFeature"]),
|
||||||
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
|
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
|
||||||
|
.library(name: "PressureEstimationsFeature", targets: ["PressureEstimationsFeature"]),
|
||||||
.library(name: "SharedModels", targets: ["SharedModels"]),
|
.library(name: "SharedModels", targets: ["SharedModels"]),
|
||||||
.library(name: "StaticPressureFeature", targets: ["StaticPressureFeature"]),
|
.library(name: "Styleguide", targets: ["Styleguide"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(
|
.package(
|
||||||
@@ -55,7 +56,12 @@ let package = Package(
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(name: "SharedModels"),
|
.target(name: "SharedModels"),
|
||||||
.target(name: "Styleguide"),
|
.target(
|
||||||
|
name: "Styleguide",
|
||||||
|
dependencies: [
|
||||||
|
"SharedModels"
|
||||||
|
]
|
||||||
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "EstimatedPressureTests",
|
name: "EstimatedPressureTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@@ -64,7 +70,7 @@ let package = Package(
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "StaticPressureFeature",
|
name: "PressureEstimationsFeature",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"EstimatedPressureDependency",
|
"EstimatedPressureDependency",
|
||||||
"SharedModels",
|
"SharedModels",
|
||||||
|
|||||||
@@ -321,6 +321,8 @@ public struct CalculateAtView: View {
|
|||||||
Text("Inputs")
|
Text("Inputs")
|
||||||
Spacer()
|
Spacer()
|
||||||
InfoButton { send(.infoButtonTapped) }
|
InfoButton { send(.infoButtonTapped) }
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.labelStyle(.iconOnly)
|
.labelStyle(.iconOnly)
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -338,7 +340,7 @@ public struct CalculateAtView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.bind($focus, to: $store.focus)
|
.bind($focus, to: $store.focus)
|
||||||
.onAppear { send(.onAppear) }
|
.onAppear { focus = .existingAirflow }
|
||||||
.sheet(isPresented: $store.isPresentingInfoView) {
|
.sheet(isPresented: $store.isPresentingInfoView) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
InfoView(calculationType: store.calculationType)
|
InfoView(calculationType: store.calculationType)
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
236
Sources/PressureEstimationsFeature/EstimateSettingsForm.swift
Normal file
236
Sources/PressureEstimationsFeature/EstimateSettingsForm.swift
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
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<State>)
|
||||||
|
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<State, Action> {
|
||||||
|
BindingReducer()
|
||||||
|
Reduce<State, Action> { 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<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.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<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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
EstimateSettingsFormView(
|
||||||
|
store: Store(
|
||||||
|
initialState: EstimateSettingsForm.State(equipmentType: .furnaceAndCoil)
|
||||||
|
) {
|
||||||
|
EstimateSettingsForm()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(width: 400, height: 600)
|
||||||
|
.padding()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,10 @@ public struct BudgetedPercentEnvelope: Equatable {
|
|||||||
self.supplyPlenumBudget = supplyPlenumBudget
|
self.supplyPlenumBudget = supplyPlenumBudget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var total: Percentage {
|
||||||
|
coilBudget + filterBudget + supplyPlenumBudget + returnPlenumBudget
|
||||||
|
}
|
||||||
|
|
||||||
public init(equipmentType: EquipmentType, fanType: FanType) {
|
public init(equipmentType: EquipmentType, fanType: FanType) {
|
||||||
switch equipmentType {
|
switch equipmentType {
|
||||||
case .furnaceAndCoil:
|
case .furnaceAndCoil:
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum FanType: Equatable, CaseIterable, CustomStringConvertible {
|
public enum FanType: Hashable, Equatable, CaseIterable, CustomStringConvertible, Identifiable {
|
||||||
case constantSpeed
|
case constantSpeed
|
||||||
case variableSpeed
|
case variableSpeed
|
||||||
|
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .constantSpeed:
|
case .constantSpeed:
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ extension Flagged.CheckHandler {
|
|||||||
.rated(RatedAirflowLimits(tons: tons, using: ratings), goodMessage: goodMessage)
|
.rated(RatedAirflowLimits(tons: tons, using: ratings), goodMessage: goodMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func percent(
|
||||||
|
goodMessage: Flagged.GoodMessageHandler = .none
|
||||||
|
) -> Self {
|
||||||
|
.using(maximum: 100, minimum: 100, rated: 100)
|
||||||
|
}
|
||||||
|
|
||||||
public static func rated<T>(
|
public static func rated<T>(
|
||||||
_ ratings: RatedEnvelope<T>,
|
_ ratings: RatedEnvelope<T>,
|
||||||
goodMessage: Flagged.GoodMessageHandler? = nil
|
goodMessage: Flagged.GoodMessageHandler? = nil
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public struct Percentage: Equatable, RawRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var fraction: Double {
|
public var fraction: Double {
|
||||||
self.rawValue / 100
|
get { self.rawValue / 100 }
|
||||||
|
set { self.rawValue = newValue * 100 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import DependenciesAdditions
|
|
||||||
import EstimatedPressureDependency
|
|
||||||
import SharedModels
|
|
||||||
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 init(
|
|
||||||
calculatedMeasurement: EquipmentMeasurement.AirHandler? = nil,
|
|
||||||
focusedField: Field? = nil,
|
|
||||||
measurement: EquipmentMeasurement.AirHandler = .init(),
|
|
||||||
updatedAirflow: Double? = nil
|
|
||||||
) {
|
|
||||||
self.calculatedMeasurement = calculatedMeasurement
|
|
||||||
self.focusedField = focusedField
|
|
||||||
self.measurement = measurement
|
|
||||||
self.updatedAirflow = updatedAirflow
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Action: BindableAction, ViewAction {
|
|
||||||
case binding(BindingAction<State>)
|
|
||||||
case receive(TaskResult<EquipmentMeasurement.AirHandler?>)
|
|
||||||
case view(View)
|
|
||||||
|
|
||||||
@CasePathable
|
|
||||||
public enum View {
|
|
||||||
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 .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
|
|
||||||
}
|
|
||||||
// return .receive(action: \.recieve) {
|
|
||||||
// try await estimatedPressuresClient.estimatedM
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct EquipmentMeasurementFormView: View {
|
|
||||||
public var body: some View {
|
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
EquipmentMeasurementFormView()
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct InfoButton: View {
|
public struct InfoButton: View {
|
||||||
|
|
||||||
|
@Environment(\.infoButtonStyle) private var infoButtonStyle
|
||||||
|
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
public init(action: @escaping () -> Void) {
|
public init(action: @escaping () -> Void) {
|
||||||
@@ -11,5 +14,22 @@ public struct InfoButton: View {
|
|||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
Label("Info", systemImage: "info.circle")
|
Label("Info", systemImage: "info.circle")
|
||||||
}
|
}
|
||||||
|
.buttonStyle(infoButtonStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ResetButton: View {
|
||||||
|
|
||||||
|
@Environment(\.resetButtonStyle) private var resetButtonStyle
|
||||||
|
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
public init(action: @escaping () -> Void) {
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Button("Reset") { action() }
|
||||||
|
.buttonStyle(resetButtonStyle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
Sources/Styleguide/FanTypePicker.swift
Normal file
21
Sources/Styleguide/FanTypePicker.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import SharedModels
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct FanTypePicker: View {
|
||||||
|
|
||||||
|
@Binding var selection: FanType
|
||||||
|
|
||||||
|
public init(selection: Binding<FanType>) {
|
||||||
|
self._selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Picker("Fan Type", selection: $selection) {
|
||||||
|
ForEach(FanType.allCases) {
|
||||||
|
Text($0.description)
|
||||||
|
.tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
142
Sources/Styleguide/FlaggedView.swift
Normal file
142
Sources/Styleguide/FlaggedView.swift
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import SharedModels
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct FlaggedView<Label: View>: View {
|
||||||
|
|
||||||
|
@Environment(\.flaggedViewStyle) private var flaggedViewStyle
|
||||||
|
|
||||||
|
let label: () -> Label
|
||||||
|
let flagged: Flagged
|
||||||
|
|
||||||
|
public init(
|
||||||
|
flagged: Flagged,
|
||||||
|
@ViewBuilder label: @escaping () -> Label
|
||||||
|
) {
|
||||||
|
self.label = label
|
||||||
|
self.flagged = flagged
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
flaggedViewStyle.makeBody(
|
||||||
|
configuration: .init(flagged: flagged, label: .init(label()))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlaggedView where Label == Text {
|
||||||
|
public init(_ title: LocalizedStringKey, flagged: Flagged) {
|
||||||
|
self.init(flagged: flagged) { Text(title) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlaggedView where Label == EmptyView {
|
||||||
|
public init(flagged: Flagged) {
|
||||||
|
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"))))
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol FocusableField {
|
public protocol FocusableField: Hashable {
|
||||||
var next: Self? { get }
|
var next: Self? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
90
Sources/Styleguide/Styles/ButtonStyles.swift
Normal file
90
Sources/Styleguide/Styles/ButtonStyles.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A name space for info button styles.
|
||||||
|
public enum InfoButtonType { }
|
||||||
|
|
||||||
|
/// A name space for info button styles.
|
||||||
|
public enum ResetButtonType { }
|
||||||
|
|
||||||
|
|
||||||
|
public struct AnyButtonStyle<ButtonType>: ButtonStyle {
|
||||||
|
private let _makeBody: (Configuration) -> AnyView
|
||||||
|
|
||||||
|
public init<S: ButtonStyle>(_ style: S) {
|
||||||
|
self._makeBody = { configuration in
|
||||||
|
AnyView(style.makeBody(configuration: configuration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
self._makeBody(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct DefaultInfoButtonStyle: ButtonStyle {
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundStyle(Color.accentColor)
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyButtonStyle where ButtonType == InfoButtonType {
|
||||||
|
public static var `default`: Self {
|
||||||
|
.init(DefaultInfoButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DefaultResetButtonStyle: ButtonStyle {
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyButtonStyle where ButtonType == ResetButtonType {
|
||||||
|
public static var `default`: Self {
|
||||||
|
.init(DefaultResetButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct InfoButtonStyleKey: EnvironmentKey {
|
||||||
|
static var defaultValue = AnyButtonStyle<InfoButtonType>.default
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ResetButtonStyleKey: EnvironmentKey {
|
||||||
|
static var defaultValue = AnyButtonStyle<ResetButtonType>.default
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
public var infoButtonStyle: AnyButtonStyle<InfoButtonType> {
|
||||||
|
get { self[InfoButtonStyleKey.self] }
|
||||||
|
set { self[InfoButtonStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var resetButtonStyle: AnyButtonStyle<ResetButtonType> {
|
||||||
|
get { self[ResetButtonStyleKey.self] }
|
||||||
|
set { self[ResetButtonStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
public func infoButtonStyle(_ style: AnyButtonStyle<InfoButtonType>) -> some View {
|
||||||
|
environment(\.infoButtonStyle, AnyButtonStyle(style))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func infoButtonStyle<S: ButtonStyle>(_ style: S) -> some View {
|
||||||
|
infoButtonStyle(AnyButtonStyle(style))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resetButtonStyle(_ style: AnyButtonStyle<ResetButtonType>) -> some View {
|
||||||
|
environment(\.resetButtonStyle, AnyButtonStyle(style))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resetButtonStyle<S: ButtonStyle>(_ style: S) -> some View {
|
||||||
|
resetButtonStyle(AnyButtonStyle(style))
|
||||||
|
}
|
||||||
|
}
|
||||||
176
Sources/Styleguide/Styles/FlaggedViewStyle.swift
Normal file
176
Sources/Styleguide/Styles/FlaggedViewStyle.swift
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import SharedModels
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public protocol FlaggedViewStyle {
|
||||||
|
associatedtype Body: View
|
||||||
|
typealias Configuration = FlaggedViewStyleConfiguration
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeBody(configuration: Self.Configuration) -> Self.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FlaggedViewStyleConfiguration {
|
||||||
|
public let flagged: Flagged
|
||||||
|
public let label: Label
|
||||||
|
|
||||||
|
/// A type erased label for a flagged view.
|
||||||
|
public struct Label: View {
|
||||||
|
public let body: AnyView
|
||||||
|
|
||||||
|
public init<Content: View>(_ content: Content) {
|
||||||
|
self.body = AnyView(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AnyFlaggedViewStyle: FlaggedViewStyle {
|
||||||
|
private var _makeBody: (Configuration) -> AnyView
|
||||||
|
|
||||||
|
internal init(makeBody: @escaping (Configuration) -> AnyView) {
|
||||||
|
self._makeBody = makeBody
|
||||||
|
}
|
||||||
|
|
||||||
|
public init<S: FlaggedViewStyle>(style: S) {
|
||||||
|
self.init { configuration in
|
||||||
|
AnyView(style.makeBody(configuration: configuration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
_makeBody(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DefaultFlagViewStyle: FlaggedViewStyle {
|
||||||
|
|
||||||
|
let alignment: HorizontalAlignment
|
||||||
|
let fractionLength: Int
|
||||||
|
let spacing: CGFloat
|
||||||
|
|
||||||
|
init(
|
||||||
|
alignment: HorizontalAlignment = .leading,
|
||||||
|
fractionLength: Int = 2,
|
||||||
|
spacing: CGFloat = 8
|
||||||
|
) {
|
||||||
|
self.alignment = alignment
|
||||||
|
self.fractionLength = fractionLength
|
||||||
|
self.spacing = spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
VStack(alignment: alignment, spacing: spacing) {
|
||||||
|
HStack {
|
||||||
|
configuration.label
|
||||||
|
Spacer()
|
||||||
|
Text(
|
||||||
|
configuration.flagged.wrappedValue,
|
||||||
|
format: .number.precision(.fractionLength(fractionLength))
|
||||||
|
)
|
||||||
|
configuration.flagged.flagImage
|
||||||
|
}
|
||||||
|
flaggedMessageView(flagged: configuration.flagged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FlagAndMessageOnlyStyle: FlaggedViewStyle {
|
||||||
|
|
||||||
|
public enum StackStyle {
|
||||||
|
case horizontal
|
||||||
|
case vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
let stackStyle: StackStyle
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
switch stackStyle {
|
||||||
|
case .horizontal:
|
||||||
|
HStack {
|
||||||
|
flaggedMessageView(flagged: configuration.flagged)
|
||||||
|
configuration.flagged.flagImage
|
||||||
|
}
|
||||||
|
case .vertical:
|
||||||
|
VStack {
|
||||||
|
configuration.flagged.flagImage
|
||||||
|
flaggedMessageView(flagged: configuration.flagged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlaggedViewStyle where Self == FlagAndMessageOnlyStyle {
|
||||||
|
public static func flagAndMessageOnly(_ stackStyle: FlagAndMessageOnlyStyle.StackStyle = .vertical) -> Self {
|
||||||
|
.init(stackStyle: stackStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct FlaggedViewStyleKey: EnvironmentKey {
|
||||||
|
static let defaultValue = AnyFlaggedViewStyle(style: DefaultFlagViewStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
public var flaggedViewStyle: AnyFlaggedViewStyle {
|
||||||
|
get { self[FlaggedViewStyleKey.self] }
|
||||||
|
set { self[FlaggedViewStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlaggedViewStyle where Self == DefaultFlagViewStyle {
|
||||||
|
public static func `default`(alignment: HorizontalAlignment = .leading, fractionLength: Int = 2, spacing: CGFloat = 8) -> Self {
|
||||||
|
.init(alignment: alignment, fractionLength: fractionLength, spacing: spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Flagged.CheckResult.Key {
|
||||||
|
|
||||||
|
public var flagColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .good:
|
||||||
|
return .green
|
||||||
|
case .warning:
|
||||||
|
return .yellow
|
||||||
|
case .error:
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Flagged {
|
||||||
|
public var flagColor: Color { projectedValue.key.flagColor }
|
||||||
|
|
||||||
|
public var flagImage: some View {
|
||||||
|
Image(systemName: "flag.fill")
|
||||||
|
.foregroundStyle(flagColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var messageView: some View { flaggedMessageView(flagged: self) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func flaggedMessageView(flagged: Flagged) -> some View {
|
||||||
|
if let message = flagged.message {
|
||||||
|
HStack {
|
||||||
|
Text(flagged.projectedValue.key.title)
|
||||||
|
.bold()
|
||||||
|
.foregroundStyle(flagged.projectedValue.key.flagColor)
|
||||||
|
TextLabel(message)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func flaggedViewStyle(_ style: AnyFlaggedViewStyle) -> some View {
|
||||||
|
environment(\.flaggedViewStyle, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func flaggedViewStyle<S: FlaggedViewStyle>(
|
||||||
|
_ style: S
|
||||||
|
) -> some View {
|
||||||
|
environment(\.flaggedViewStyle, AnyFlaggedViewStyle(style: style))
|
||||||
|
}
|
||||||
|
}
|
||||||
175
Sources/Styleguide/Styles/TextLabelStyle.swift
Normal file
175
Sources/Styleguide/Styles/TextLabelStyle.swift
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public protocol TextLabelStyle {
|
||||||
|
|
||||||
|
associatedtype Body: View
|
||||||
|
typealias Configuration = TextLabelConfiguration
|
||||||
|
|
||||||
|
func makeBody(configuration: Self.Configuration) -> Self.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextLabelStyle {
|
||||||
|
public func combining<Other: TextLabelStyle>(_ style: Other) -> AnyTextLabelStyle {
|
||||||
|
AnyTextLabelStyle(style: self).combining(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct TextLabelConfiguration {
|
||||||
|
|
||||||
|
/// A type erased label for a text label.
|
||||||
|
public struct Label: View {
|
||||||
|
public var body: AnyView
|
||||||
|
|
||||||
|
public init<Content: View>(content: Content) {
|
||||||
|
body = AnyView(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public let label: TextLabelConfiguration.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AnyTextLabelStyle: TextLabelStyle {
|
||||||
|
private var _makeBody: (Configuration) -> AnyView
|
||||||
|
|
||||||
|
internal init(makeBody: @escaping (Configuration) -> AnyView) {
|
||||||
|
self._makeBody = makeBody
|
||||||
|
}
|
||||||
|
|
||||||
|
public init<S: TextLabelStyle>(style: S) {
|
||||||
|
self._makeBody = { configuration in
|
||||||
|
AnyView(style.makeBody(configuration: configuration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
_makeBody(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func combining<S: TextLabelStyle>(_ style: S) -> Self {
|
||||||
|
Self.init { configuration in
|
||||||
|
AnyView(
|
||||||
|
self.makeBody(
|
||||||
|
configuration: TextLabelConfiguration(
|
||||||
|
label: .init(content: style.makeBody(configuration: configuration))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AutomaticTextLabelStyle: TextLabelStyle {
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TextLabelStyleKey: EnvironmentKey {
|
||||||
|
static let defaultValue = AnyTextLabelStyle(style: AutomaticTextLabelStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SectionHeaderLabelStyleKey: EnvironmentKey {
|
||||||
|
static let defaultValue = AnyTextLabelStyle(style: AutomaticTextLabelStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
public var textLabelStyle: AnyTextLabelStyle {
|
||||||
|
get { self[TextLabelStyleKey.self] }
|
||||||
|
set { self[TextLabelStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var sectionHeaderLabelStyle: AnyTextLabelStyle {
|
||||||
|
get { self[SectionHeaderLabelStyleKey.self] }
|
||||||
|
set { self[SectionHeaderLabelStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FontTextLabelStyle: TextLabelStyle {
|
||||||
|
let font: Font?
|
||||||
|
let fontWeight: Font.Weight?
|
||||||
|
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.font(font)
|
||||||
|
.fontWeight(fontWeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ColoredTextLabelStyle: TextLabelStyle {
|
||||||
|
|
||||||
|
let primary: Color
|
||||||
|
let secondary: Color?
|
||||||
|
let tertiary: Color?
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
public func makeBody(configuration: Configuration) -> some View {
|
||||||
|
switch (secondary, tertiary) {
|
||||||
|
case let (.some(secondary), .some(tertiary)):
|
||||||
|
configuration.label.foregroundStyle(primary, secondary, tertiary)
|
||||||
|
case let (.some(secondary), .none):
|
||||||
|
configuration.label.foregroundStyle(primary, secondary)
|
||||||
|
case let (.none, .some(tertiary)):
|
||||||
|
configuration.label.foregroundStyle(primary, tertiary)
|
||||||
|
case (.none, .none):
|
||||||
|
configuration.label.foregroundStyle(primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func font(_ font: Font? = nil, fontWeight: Font.Weight? = nil) -> AnyTextLabelStyle {
|
||||||
|
self.combining(.font(font, fontWeight: fontWeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextLabelStyle where Self == AutomaticTextLabelStyle {
|
||||||
|
public static var automatic: Self {
|
||||||
|
.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextLabelStyle where Self == FontTextLabelStyle {
|
||||||
|
public static func font(_ font: Font? = nil, fontWeight: Font.Weight? = nil) -> Self {
|
||||||
|
.init(font: font, fontWeight: fontWeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var heavyTitle2: Self {
|
||||||
|
.font(.title2, fontWeight: .heavy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextLabelStyle where Self == ColoredTextLabelStyle {
|
||||||
|
|
||||||
|
public static func colored(_ color: Color) -> Self {
|
||||||
|
.init(primary: color, secondary: nil, tertiary: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func colored(_ primary: Color, _ secondary: Color, _ tertiary: Color? = nil) -> Self {
|
||||||
|
.init(primary: primary, secondary: secondary, tertiary: tertiary)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var secondary: Self {
|
||||||
|
self.colored(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnyTextLabelStyle {
|
||||||
|
|
||||||
|
public static var boldSecondary: AnyTextLabelStyle {
|
||||||
|
ColoredTextLabelStyle(primary: .secondary, secondary: nil, tertiary: nil)
|
||||||
|
.combining(.font(fontWeight: .bold))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
public func textLabelStyle(_ style: AnyTextLabelStyle) -> some View {
|
||||||
|
environment(\.textLabelStyle, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func textLabelStyle<S: TextLabelStyle>(_ style: S) -> some View {
|
||||||
|
environment(\.textLabelStyle, AnyTextLabelStyle(style: style))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sectionHeaderLabelStyle<S: TextLabelStyle>(_ style: S) -> some View {
|
||||||
|
environment(\.sectionHeaderLabelStyle, AnyTextLabelStyle(style: style))
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Sources/Styleguide/TextField+Precision.swift
Normal file
19
Sources/Styleguide/TextField+Precision.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
Sources/Styleguide/TextLabel.swift
Normal file
52
Sources/Styleguide/TextLabel.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A view that can be styled view the `.textLabelStyle` modifier, generally they will be
|
||||||
|
/// simple `Text` views, however it will accept any content view and will apply the style to.
|
||||||
|
///
|
||||||
|
/// Custom styles can be created by conforming to the ``TextLabelStyle`` protocol.
|
||||||
|
///
|
||||||
|
public struct TextLabel<Content: View>: View {
|
||||||
|
|
||||||
|
@Environment(\.textLabelStyle) private var textLabelStyle
|
||||||
|
|
||||||
|
private let content: () -> Content
|
||||||
|
|
||||||
|
public init(@ViewBuilder content: @escaping () -> Content) {
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
textLabelStyle.makeBody(
|
||||||
|
configuration: TextLabelConfiguration(
|
||||||
|
label: TextLabelConfiguration.Label(content: content())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextLabel where Content == Text {
|
||||||
|
public init<S>(_ text: S) where S: StringProtocol {
|
||||||
|
self.init { Text(text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack {
|
||||||
|
TextLabel("Automatic")
|
||||||
|
|
||||||
|
TextLabel("Secondary-Bold")
|
||||||
|
.textLabelStyle(AnyTextLabelStyle.boldSecondary)
|
||||||
|
|
||||||
|
TextLabel("Secondary")
|
||||||
|
.textLabelStyle(.secondary)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
TextLabel("One")
|
||||||
|
TextLabel("Two")
|
||||||
|
TextLabel("Three")
|
||||||
|
}
|
||||||
|
.font(.title)
|
||||||
|
.textLabelStyle(.boldSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user