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? public init( existingAirflow: Double? = nil, existingPressure: Double? = nil, targetPressure: Double? = 0.5, calculatedAirflow: Positive? = 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) case receive(ReceiveAction) case view(View) @CasePathable public enum ReceiveAction { case calculatedAirflow(Positive?) } @CasePathable public enum View { case dismissInfoViewButtonTapped case infoButtonTapped case resetButtonTapped case submit } } @Dependency(\.estimatedPressuresClient) var estimatedPressuresClient public var body: some Reducer { BindingReducer() Reduce { 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 public init(store: StoreOf) { 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() } ) }