feat: Adds airflow at pressure feature.

This commit is contained in:
2024-05-28 10:09:59 -04:00
parent c1741de3f9
commit 187a682a63
6 changed files with 323 additions and 93 deletions

View File

@@ -5,23 +5,36 @@ import PackageDescription
let package = Package(
name: "swift-estimated-pressures-core",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6),
.iOS(.v16),
.macOS(.v11),
.tvOS(.v14),
.watchOS(.v7),
],
products: [
.library(name: "AirflowAtPressureFeature", targets: ["AirflowAtPressureFeature"]),
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
.library(name: "SharedModels", targets: ["SharedModels"]),
],
dependencies: [
.package(
url: "https://github.com/pointfreeco/swift-dependencies.git",
from: "1.3.0"
),
.package(
url:"https://github.com/pointfreeco/swift-composable-architecture.git",
from: "1.10.0"
),
],
targets: [
.target(
name: "AirflowAtPressureFeature",
dependencies: [
"EstimatedPressureDependency",
"SharedModels",
"Styleguide",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
.target(
name: "EstimatedPressureDependency",
dependencies: [
@@ -31,6 +44,7 @@ let package = Package(
]
),
.target(name: "SharedModels"),
.target(name: "Styleguide"),
.testTarget(
name: "EstimatedPressureTests",
dependencies: [

View File

@@ -0,0 +1,189 @@
import ComposableArchitecture
import EstimatedPressureDependency
import SharedModels
import Styleguide
import SwiftUI
@Reducer
public struct AirflowAtPressureFeature {
@ObservableState
public struct State: Equatable {
public var isPresentingInfoView: Bool = false
public var existingAirflow: Double?
public var existingPressure: Double?
public var targetPressure: Double?
public var calculatedAirflow: Positive<Double>?
public init(
existingAirflow: Double? = nil,
existingPressure: Double? = nil,
targetPressure: Double? = 0.5,
calculatedAirflow: Positive<Double>? = nil
) {
self.existingAirflow = existingAirflow
self.existingPressure = existingPressure
self.targetPressure = targetPressure
self.calculatedAirflow = calculatedAirflow
}
public var isValid: Bool {
existingAirflow != nil
&& existingPressure != nil
&& targetPressure != nil
}
}
public enum Action: BindableAction, ViewAction {
case binding(BindingAction<State>)
case receive(ReceiveAction)
case view(View)
@CasePathable
public enum ReceiveAction {
case calculatedAirflow(Positive<Double>?)
}
@CasePathable
public enum View {
case dismissInfoViewButtonTapped
case infoButtonTapped
case resetButtonTapped
case submit
}
}
@Dependency(\.estimatedPressuresClient) var estimatedPressuresClient
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce<State, Action> { state, action in
switch action {
case .binding:
return .none
case let .receive(action):
switch action {
case let .calculatedAirflow(airflow):
state.calculatedAirflow = airflow
return .none
}
case let .view(action):
switch action {
case .dismissInfoViewButtonTapped:
state.isPresentingInfoView = false
return .none
case .infoButtonTapped:
state.isPresentingInfoView = true
return .none
case .resetButtonTapped:
state = .init()
return .none
case .submit:
guard state.isValid else { return .none }
return .run { [state] send in
await send(
.receive(.calculatedAirflow(
try? await estimatedPressuresClient.estimatedAirflow(.init(
existingAirflow: state.existingAirflow ?? 0,
existingPressure: state.existingPressure ?? 0,
targetPressure: state.targetPressure ?? 0
))
))
)
}
}
}
}
}
}
@ViewAction(for: AirflowAtPressureFeature.self)
public struct AirflowAtPressureView: View {
@Perception.Bindable
public var store: StoreOf<AirflowAtPressureFeature>
public init(store: StoreOf<AirflowAtPressureFeature>) {
self.store = store
}
public var body: some View {
Form {
Section {
TextField(
"Existing Airflow",
value: $store.existingAirflow,
format: .number,
prompt: Text("Airflow")
)
.onSubmit { send(.submit) }
.numberPad()
TextField(
"Existing Pressure",
value: $store.existingPressure,
format: .number,
prompt: Text("Existing Pressure")
)
.onSubmit { send(.submit) }
.decimalPad()
TextField(
"Target Pressure",
value: $store.targetPressure,
format: .number,
prompt: Text("Target Pressure")
)
.onSubmit { send(.submit) }
.decimalPad()
} header: {
HStack {
Text("Inputs")
Spacer()
InfoButton { send(.infoButtonTapped) }
}
.labelStyle(.iconOnly)
} footer: {
HStack {
Spacer()
Button("Reset") { send(.resetButtonTapped) }
Spacer()
}
.padding(.top)
.buttonStyle(.borderedProminent)
}
Section {
if let airflow = store.calculatedAirflow {
Text(airflow.positiveValue, format: .number.precision(.fractionLength(0)))
}
} header: {
Text("Calculated Airflow")
}
}
.sheet(isPresented: $store.isPresentingInfoView) {
NavigationStack {
AirflowAtPressureInfoView()
.toolbar {
Button("Done") { send(.dismissInfoViewButtonTapped) }
}
}
}
}
}
#Preview {
AirflowAtPressureView(
store: Store(initialState: AirflowAtPressureFeature.State()) {
AirflowAtPressureFeature()
}
)
}

View File

@@ -0,0 +1,44 @@
import SwiftUI
struct AirflowAtPressureInfoView: View {
static let heading = """
Calculate the airflow at the target pressure from the existing airflow and existing pressure.
"""
static let body = """
This can be useful to determine the effect of a different blower speed on a specific measurement location (generally the supply or return plenum).
"""
var body: some View {
VStack(spacing: 40) {
ZStack {
Text(Self.heading)
.font(.headline)
.bold()
.foregroundStyle(Color.white)
.padding()
}
.background {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(Color.orange.opacity(0.6))
}
ZStack {
Text(Self.body)
.font(.callout)
.padding()
}
.background {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(Color.secondary.opacity(0.3))
}
.padding(.horizontal, 10)
Spacer()
}
.padding(.horizontal)
}
}

View File

@@ -1,65 +1,5 @@
import Foundation
public enum FlaggedValue<Value> {
case good(Value, String?)
case warning(Value, String?)
case error(Value, String?)
public init(_ value: Value, key: Key, message: String? = nil) {
switch key {
case .good:
self = .good(value, message)
case .warning:
self = .warning(value, message)
case .error:
self = .error(value, message)
}
}
public var key: Key {
switch self {
case .good:
return .good
case .warning:
return .warning
case .error:
return .error
}
}
public var value: Value {
switch self {
case let .good(value, _):
return value
case let .warning(value, _):
return value
case let .error(value, _):
return value
}
}
public var message: String? {
switch self {
case let .good(_, message):
return message
case let .warning(_, message):
return message
case let .error(_, message):
return message
}
}
public enum Key: String, Equatable, CaseIterable {
case good, warning, error
public var title: String {
self.rawValue.capitalized
}
}
}
extension FlaggedValue: Equatable where Value: Equatable { }
@dynamicMemberLookup
public struct Flagged: Equatable {
@@ -88,32 +28,8 @@ public struct Flagged: Equatable {
}
}
public var projectedValue: FlaggedValue<Double> {
let checkedResult = checkValue(wrappedValue)
let key = checkedResult.key
let message: String?
switch checkedResult {
case let .aboveMaximum(max):
message = "Above maximum: \(doubleString(max))"
case let .belowMinimum(min):
message = "Below minimum: \(doubleString(min))"
case .betweenRange(minimum: let minimum, maximum: let maximum):
message = "Between: \(minimum) and \(maximum)"
case .betweenRatedAndMaximum(rated: let rated, maximum: let maximum):
message = "Between rated: \(doubleString(rated)) and maximum: \(doubleString(maximum))"
case let .good(goodMessage):
message = goodMessage
case let .error(errorMessage):
message = errorMessage
}
return .init(wrappedValue, key: key, message: message)
public var projectedValue: CheckResult {
checkValue(wrappedValue)
}
public struct GoodMessageHandler {
@@ -154,7 +70,7 @@ public struct Flagged: Equatable {
case good(String? = nil)
case error(String)
var key: FlaggedValue<Double>.Key {
public var key: Key {
switch self {
case .aboveMaximum(_):
return .error
@@ -170,13 +86,45 @@ public struct Flagged: Equatable {
return .error
}
}
public var message: String? {
switch self {
case let .aboveMaximum(max):
return "Above maximum: \(doubleString(max))"
case let .belowMinimum(min):
return "Below minimum: \(doubleString(min))"
case .betweenRange(minimum: let minimum, maximum: let maximum):
return "Between: \(minimum) and \(maximum)"
case .betweenRatedAndMaximum(rated: let rated, maximum: let maximum):
return "Between rated: \(doubleString(rated)) and maximum: \(doubleString(maximum))"
case let .good(goodMessage):
return goodMessage
case let .error(errorMessage):
return errorMessage
}
}
public enum Key: String, Equatable, CaseIterable {
case good, warning, error
public var title: String {
self.rawValue.capitalized
}
}
}
public static func == (lhs: Flagged, rhs: Flagged) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
public subscript<T>(dynamicMember keyPath: KeyPath<FlaggedValue<Double>, T>) -> T {
public subscript<T>(dynamicMember keyPath: KeyPath<CheckResult, T>) -> T {
self.projectedValue[keyPath: keyPath]
}
}

View File

@@ -0,0 +1,15 @@
import SwiftUI
public struct InfoButton: View {
let action: () -> Void
public init(action: @escaping () -> Void) {
self.action = action
}
public var body: some View {
Button(action: action) {
Label("Info", systemImage: "info.circle")
}
}
}

View File

@@ -0,0 +1,20 @@
import SwiftUI
extension View {
public func numberPad() -> some View {
#if os(iOS)
self.keyboardType(.numberPad)
#else
self
#endif
}
public func decimalPad() -> some View {
#if os(iOS)
self.keyboardType(.decimalPad)
#else
self
#endif
}
}