Feat: Consilidates calculated at views.
This commit is contained in:
@@ -6,15 +6,14 @@ let package = Package(
|
|||||||
name: "swift-estimated-pressures-core",
|
name: "swift-estimated-pressures-core",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v16),
|
.iOS(.v16),
|
||||||
.macOS(.v11),
|
.macOS(.v13),
|
||||||
.tvOS(.v14),
|
.tvOS(.v14),
|
||||||
.watchOS(.v7),
|
.watchOS(.v7),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "AirflowAtPressureFeature", targets: ["AirflowAtPressureFeature"]),
|
.library(name: "CalculateAtFeature", targets: ["CalculateAtFeature"]),
|
||||||
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
|
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
|
||||||
.library(name: "SharedModels", targets: ["SharedModels"]),
|
.library(name: "SharedModels", targets: ["SharedModels"]),
|
||||||
.library(name: "TCAHelpers", targets: ["TCAHelpers"]),
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(
|
.package(
|
||||||
@@ -29,17 +28,21 @@ let package = Package(
|
|||||||
url: "https://github.com/tgrapperon/swift-dependencies-additions.git",
|
url: "https://github.com/tgrapperon/swift-dependencies-additions.git",
|
||||||
from: "1.0.1"
|
from: "1.0.1"
|
||||||
),
|
),
|
||||||
|
.package(
|
||||||
|
url: "https://github.com/m-housh/swift-tca-extras.git",
|
||||||
|
from: "0.1.0"
|
||||||
|
),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "AirflowAtPressureFeature",
|
name: "CalculateAtFeature",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"EstimatedPressureDependency",
|
"EstimatedPressureDependency",
|
||||||
"SharedModels",
|
"SharedModels",
|
||||||
"Styleguide",
|
"Styleguide",
|
||||||
"TCAHelpers",
|
|
||||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||||
.product(name: "DependenciesAdditions", package: "swift-dependencies-additions")
|
.product(name: "DependenciesAdditions", package: "swift-dependencies-additions"),
|
||||||
|
.product(name: "TCAExtras", package: "swift-tca-extras")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
@@ -52,12 +55,6 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(name: "SharedModels"),
|
.target(name: "SharedModels"),
|
||||||
.target(name: "Styleguide"),
|
.target(name: "Styleguide"),
|
||||||
.target(
|
|
||||||
name: "TCAHelpers",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "EstimatedPressureTests",
|
name: "EstimatedPressureTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import DependenciesAdditions
|
|
||||||
import EstimatedPressureDependency
|
|
||||||
import SharedModels
|
|
||||||
import Styleguide
|
|
||||||
import SwiftUI
|
|
||||||
import TCAHelpers
|
|
||||||
|
|
||||||
@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, ReceiveAction, ViewAction {
|
|
||||||
case binding(BindingAction<State>)
|
|
||||||
case receive(TaskResult<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
|
|
||||||
@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(.failure(error)):
|
|
||||||
return .fail(error: error, logger: logger)
|
|
||||||
|
|
||||||
case let .receive(.success(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 .receive(\.calculatedAirflow) { [state] in
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
401
Sources/CalculateAtFeature/CalculateAtView.swift
Normal file
401
Sources/CalculateAtFeature/CalculateAtView.swift
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
import ComposableArchitecture
|
||||||
|
import DependenciesAdditions
|
||||||
|
import EstimatedPressureDependency
|
||||||
|
import SharedModels
|
||||||
|
import Styleguide
|
||||||
|
import SwiftUI
|
||||||
|
import TCAExtras
|
||||||
|
|
||||||
|
@Reducer
|
||||||
|
public struct CalculateAtFeature {
|
||||||
|
|
||||||
|
@ObservableState
|
||||||
|
@dynamicMemberLookup
|
||||||
|
public struct State: Equatable {
|
||||||
|
public var focus: Focus? = nil
|
||||||
|
public var isPresentingInfoView: Bool = false
|
||||||
|
public var values: ValueContainer
|
||||||
|
public var calculationType: CalculationType
|
||||||
|
|
||||||
|
public init(
|
||||||
|
values: ValueContainer = .init(),
|
||||||
|
calculationType: CalculationType = .airflowAtNewPressure
|
||||||
|
) {
|
||||||
|
self.values = values
|
||||||
|
self.calculationType = calculationType
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isValid: Bool {
|
||||||
|
values.existingAirflow != nil
|
||||||
|
&& values.existingPressure != nil
|
||||||
|
&& values.targetValue != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscript<T>(dynamicMember keyPath: KeyPath<CalculationType, T>) -> T {
|
||||||
|
calculationType[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscript<T>(dynamicMember keyPath: WritableKeyPath<ValueContainer, T>) -> T {
|
||||||
|
get { values[keyPath: keyPath] }
|
||||||
|
set { values[keyPath: keyPath] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Focus: Hashable {
|
||||||
|
case existingAirflow
|
||||||
|
case existingPressure
|
||||||
|
case targetValue
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ValueContainer: Equatable {
|
||||||
|
public var existingAirflow: Double?
|
||||||
|
public var existingPressure: Double?
|
||||||
|
public var targetValue: Double?
|
||||||
|
public var calculatedValue: Double?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
existingAirflow: Double? = nil,
|
||||||
|
existingPressure: Double? = nil,
|
||||||
|
targetValue: Double? = nil,
|
||||||
|
calculatedValue: Double? = nil
|
||||||
|
) {
|
||||||
|
self.existingAirflow = existingAirflow
|
||||||
|
self.existingPressure = existingPressure
|
||||||
|
self.targetValue = targetValue
|
||||||
|
self.calculatedValue = calculatedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CalculationType: Hashable, CaseIterable, Identifiable {
|
||||||
|
case airflowAtNewPressure
|
||||||
|
case pressureAtNewAirflow
|
||||||
|
|
||||||
|
public var id: Self { self }
|
||||||
|
|
||||||
|
private var precision: Int {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return 0
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var format: FloatingPointFormatStyle<Double> {
|
||||||
|
.number.precision(.fractionLength(precision))
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return "Pressure"
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return "Airflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var calculationLabel: String {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return "Airflow"
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return "Pressure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var calculationLabelSpacing: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return 5
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var calculationPostfix: String {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return "CFM"
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return "\" w.c."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return "Airflow at Pressure"
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return "Pressure at Airflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Action: BindableAction, ReceiveAction, ViewAction {
|
||||||
|
case binding(BindingAction<State>)
|
||||||
|
case receive(TaskResult<ReceiveAction>)
|
||||||
|
case view(View)
|
||||||
|
|
||||||
|
@CasePathable
|
||||||
|
public enum ReceiveAction {
|
||||||
|
case calculatedValue(Positive<Double>?)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CasePathable
|
||||||
|
public enum View {
|
||||||
|
case dismissInfoViewButtonTapped
|
||||||
|
case infoButtonTapped
|
||||||
|
case onAppear
|
||||||
|
case resetButtonTapped
|
||||||
|
case submit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(\.calculationType):
|
||||||
|
state.targetValue = nil
|
||||||
|
state.calculatedValue = nil
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .binding:
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .receive(.success(.calculatedValue(calculatedAirflow))):
|
||||||
|
state.calculatedValue = calculatedAirflow?.positiveValue
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .receive:
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case let .view(action):
|
||||||
|
switch action {
|
||||||
|
case .dismissInfoViewButtonTapped:
|
||||||
|
state.isPresentingInfoView = false
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .infoButtonTapped:
|
||||||
|
state.isPresentingInfoView = true
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .onAppear:
|
||||||
|
state.focus = .existingAirflow
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .resetButtonTapped:
|
||||||
|
state.values = .init()
|
||||||
|
return .none
|
||||||
|
|
||||||
|
case .submit:
|
||||||
|
guard state.isValid else {
|
||||||
|
return .none
|
||||||
|
}
|
||||||
|
return calculate(state: state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure(.log(logger: logger))
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculate(state: State) -> Effect<Action> {
|
||||||
|
return .receive(\.calculatedValue) { [state] in
|
||||||
|
switch state.calculationType {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
return try await estimatedPressuresClient.estimatedAirflow(
|
||||||
|
.init(
|
||||||
|
existingAirflow: state.existingAirflow ?? 0,
|
||||||
|
existingPressure: state.existingPressure ?? 0,
|
||||||
|
targetPressure: state.targetValue ?? 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
return try await estimatedPressuresClient.estimatedPressure(
|
||||||
|
existingPressure: state.existingPressure ?? 0,
|
||||||
|
existingAirflow: state.existingAirflow ?? 0,
|
||||||
|
targetAirflow: state.targetValue ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewAction(for: CalculateAtFeature.self)
|
||||||
|
public struct CalculateAtView: View {
|
||||||
|
|
||||||
|
@FocusState private var focus: CalculateAtFeature.State.Focus?
|
||||||
|
|
||||||
|
@Perception.Bindable
|
||||||
|
public var store: StoreOf<CalculateAtFeature>
|
||||||
|
|
||||||
|
let allowChangingCalculationType: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
allowChangingCalculationType: Bool = true,
|
||||||
|
store: StoreOf<CalculateAtFeature>
|
||||||
|
) {
|
||||||
|
self.allowChangingCalculationType = allowChangingCalculationType
|
||||||
|
self.store = store
|
||||||
|
self.focus = store.focus
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
VStack {
|
||||||
|
if allowChangingCalculationType {
|
||||||
|
Picker("Calculation Type", selection: $store.calculationType) {
|
||||||
|
ForEach(CalculateAtFeature.State.CalculationType.allCases) {
|
||||||
|
Text($0.label)
|
||||||
|
.tag($0.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.labelsHidden()
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
TextField(
|
||||||
|
"Existing Airflow",
|
||||||
|
value: $store.existingAirflow,
|
||||||
|
format: .number,
|
||||||
|
prompt: Text("Existing Airflow")
|
||||||
|
)
|
||||||
|
.focused($focus, equals: .existingAirflow)
|
||||||
|
.onSubmit { send(.submit) }
|
||||||
|
.numberPad()
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
"Existing Pressure",
|
||||||
|
value: $store.existingPressure,
|
||||||
|
format: .number,
|
||||||
|
prompt: Text("Existing Pressure")
|
||||||
|
)
|
||||||
|
.focused($focus, equals: .existingPressure)
|
||||||
|
.onSubmit { send(.submit) }
|
||||||
|
.decimalPad()
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
"Target \(store.targetLabel)",
|
||||||
|
value: $store.targetValue,
|
||||||
|
format: .number,
|
||||||
|
prompt: Text("Target \(store.targetLabel)")
|
||||||
|
)
|
||||||
|
.focused($focus, equals: .existingPressure)
|
||||||
|
.onSubmit { send(.submit) }
|
||||||
|
.keyboard(for: store.calculationType)
|
||||||
|
|
||||||
|
if let airflow = store.calculatedValue {
|
||||||
|
HStack {
|
||||||
|
Text("Calculated \(store.calculationLabel)")
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
.font(.callout.bold())
|
||||||
|
Spacer()
|
||||||
|
ZStack {
|
||||||
|
HStack(spacing: store.calculationLabelSpacing) {
|
||||||
|
Text(airflow, format: store.format)
|
||||||
|
Text(store.calculationPostfix)
|
||||||
|
}
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.font(.callout)
|
||||||
|
.bold()
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.foregroundStyle(Color.green)
|
||||||
|
.opacity(0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Inputs")
|
||||||
|
Spacer()
|
||||||
|
InfoButton { send(.infoButtonTapped) }
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
} footer: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button("Reset") { send(.resetButtonTapped) }
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.bind($focus, to: $store.focus)
|
||||||
|
.onAppear { send(.onAppear) }
|
||||||
|
.sheet(isPresented: $store.isPresentingInfoView) {
|
||||||
|
NavigationStack {
|
||||||
|
InfoView(calculationType: store.calculationType)
|
||||||
|
.toolbar {
|
||||||
|
Button("Done") { send(.dismissInfoViewButtonTapped) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension InfoView {
|
||||||
|
|
||||||
|
init(calculationType: CalculateAtFeature.State.CalculationType) {
|
||||||
|
switch calculationType {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
self.init(
|
||||||
|
heading: """
|
||||||
|
Calculate the airflow at the target pressure from the existing airflow and existing pressure.
|
||||||
|
""",
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
self.init(
|
||||||
|
heading: """
|
||||||
|
Calculate the pressure at the target airflow from the existing airflow and existing pressure.
|
||||||
|
""",
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension View {
|
||||||
|
@ViewBuilder
|
||||||
|
func keyboard(for calculationType: CalculateAtFeature.State.CalculationType) -> some View {
|
||||||
|
switch calculationType {
|
||||||
|
case .airflowAtNewPressure:
|
||||||
|
self.decimalPad()
|
||||||
|
case .pressureAtNewAirflow:
|
||||||
|
self.numberPad()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
CalculateAtView(
|
||||||
|
store: Store(initialState: CalculateAtFeature.State()) {
|
||||||
|
CalculateAtFeature()._printChanges()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(width: 400, height: 400)
|
||||||
|
.padding()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AirflowAtPressureInfoView: View {
|
public struct InfoView: View {
|
||||||
|
let headingText: String
|
||||||
|
let bodyText: String
|
||||||
|
|
||||||
static let heading = """
|
|
||||||
Calculate the airflow at the target pressure from the existing airflow and existing pressure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
static let body = """
|
public init(heading headingText: String, body bodyText: String) {
|
||||||
This can be useful to determine the effect of a different blower speed on a specific measurement location (generally the supply or return plenum).
|
self.headingText = headingText
|
||||||
"""
|
self.bodyText = bodyText
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 40) {
|
VStack(spacing: 40) {
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Text(Self.heading)
|
Text(headingText)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.bold()
|
.bold()
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
@@ -27,7 +27,7 @@ struct AirflowAtPressureInfoView: View {
|
|||||||
|
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
Text(Self.body)
|
Text(bodyText)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
@@ -42,3 +42,7 @@ struct AirflowAtPressureInfoView: View {
|
|||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
InfoView(heading: "Generic Info View", body: "Explain what this view does here...")
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
import OSLog
|
|
||||||
|
|
||||||
extension Effect {
|
|
||||||
public static func fail(
|
|
||||||
prefix: String = "Failed error:",
|
|
||||||
error: Error,
|
|
||||||
logger: Logger? = nil
|
|
||||||
) -> Self {
|
|
||||||
let message = "\(prefix) \(error)"
|
|
||||||
XCTFail("\(message)")
|
|
||||||
logger?.error("\(message)")
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func fail(
|
|
||||||
_ message: String,
|
|
||||||
logger: Logger? = nil
|
|
||||||
) -> Self {
|
|
||||||
XCTFail("\(message)")
|
|
||||||
logger?.error("\(message)")
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import ComposableArchitecture
|
|
||||||
|
|
||||||
public protocol ReceiveAction<ReceiveAction>: CasePathable {
|
|
||||||
associatedtype ReceiveAction: CasePathable
|
|
||||||
static func receive(_ result: TaskResult<ReceiveAction>) -> Self
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Effect where Action: ReceiveAction {
|
|
||||||
|
|
||||||
public static func receive(
|
|
||||||
_ operation: @escaping () async throws -> Action.ReceiveAction
|
|
||||||
) -> Self {
|
|
||||||
.run { send in
|
|
||||||
await send(.receive(
|
|
||||||
TaskResult { try await operation() }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func receive<T>(
|
|
||||||
_ operation: @escaping () async throws -> T,
|
|
||||||
transform: @escaping (T) -> Action.ReceiveAction
|
|
||||||
) -> Self {
|
|
||||||
.run { send in
|
|
||||||
await send(.receive(
|
|
||||||
TaskResult { try await operation() }
|
|
||||||
.map(transform)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func receive<T>(
|
|
||||||
_ toReceiveAction: CaseKeyPath<Action.ReceiveAction, T>,
|
|
||||||
_ operation: @escaping () async throws -> T
|
|
||||||
) -> Self {
|
|
||||||
return .receive(operation) {
|
|
||||||
AnyCasePath(toReceiveAction).embed($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user