Feat: Working on pressure estimations feature

This commit is contained in:
2024-06-04 13:06:49 -04:00
parent 04c899da8e
commit 076488beb4
19 changed files with 1206 additions and 128 deletions

View File

@@ -5,16 +5,17 @@ import PackageDescription
let package = Package(
name: "swift-estimated-pressures-core",
platforms: [
.iOS(.v16),
.macOS(.v13),
.iOS(.v17),
.macOS(.v14),
.tvOS(.v14),
.watchOS(.v7),
],
products: [
.library(name: "CalculateAtFeature", targets: ["CalculateAtFeature"]),
.library(name: "EstimatedPressureDependency", targets: ["EstimatedPressureDependency"]),
.library(name: "PressureEstimationsFeature", targets: ["PressureEstimationsFeature"]),
.library(name: "SharedModels", targets: ["SharedModels"]),
.library(name: "StaticPressureFeature", targets: ["StaticPressureFeature"]),
.library(name: "Styleguide", targets: ["Styleguide"]),
],
dependencies: [
.package(
@@ -55,7 +56,12 @@ let package = Package(
]
),
.target(name: "SharedModels"),
.target(name: "Styleguide"),
.target(
name: "Styleguide",
dependencies: [
"SharedModels"
]
),
.testTarget(
name: "EstimatedPressureTests",
dependencies: [
@@ -64,7 +70,7 @@ let package = Package(
]
),
.target(
name: "StaticPressureFeature",
name: "PressureEstimationsFeature",
dependencies: [
"EstimatedPressureDependency",
"SharedModels",

View File

@@ -321,6 +321,8 @@ public struct CalculateAtView: View {
Text("Inputs")
Spacer()
InfoButton { send(.infoButtonTapped) }
.foregroundStyle(Color.accentColor)
.buttonStyle(.plain)
}
.labelStyle(.iconOnly)
} footer: {
@@ -338,7 +340,7 @@ public struct CalculateAtView: View {
Spacer()
}
.bind($focus, to: $store.focus)
.onAppear { send(.onAppear) }
.onAppear { focus = .existingAirflow }
.sheet(isPresented: $store.isPresentingInfoView) {
NavigationStack {
InfoView(calculationType: store.calculationType)

View File

@@ -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()
}
)
}

View 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
}
}

View File

@@ -23,6 +23,10 @@ public struct BudgetedPercentEnvelope: Equatable {
self.supplyPlenumBudget = supplyPlenumBudget
}
public var total: Percentage {
coilBudget + filterBudget + supplyPlenumBudget + returnPlenumBudget
}
public init(equipmentType: EquipmentType, fanType: FanType) {
switch equipmentType {
case .furnaceAndCoil:

View File

@@ -1,9 +1,11 @@
import Foundation
public enum FanType: Equatable, CaseIterable, CustomStringConvertible {
public enum FanType: Hashable, Equatable, CaseIterable, CustomStringConvertible, Identifiable {
case constantSpeed
case variableSpeed
public var id: Self { self }
public var description: String {
switch self {
case .constantSpeed:

View File

@@ -158,6 +158,12 @@ extension Flagged.CheckHandler {
.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>(
_ ratings: RatedEnvelope<T>,
goodMessage: Flagged.GoodMessageHandler? = nil

View File

@@ -13,7 +13,8 @@ public struct Percentage: Equatable, RawRepresentable {
}
public var fraction: Double {
self.rawValue / 100
get { self.rawValue / 100 }
set { self.rawValue = newValue * 100 }
}
}

View File

@@ -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()
}

View File

@@ -1,6 +1,9 @@
import SwiftUI
public struct InfoButton: View {
@Environment(\.infoButtonStyle) private var infoButtonStyle
let action: () -> Void
public init(action: @escaping () -> Void) {
@@ -11,5 +14,22 @@ public struct InfoButton: View {
Button(action: action) {
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)
}
}

View 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)
}
}
}
}

View 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"))))
// }
//}

View File

@@ -1,6 +1,6 @@
import Foundation
public protocol FocusableField {
public protocol FocusableField: Hashable {
var next: Self? { get }
}

View 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))
}
}

View 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))
}
}

View 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))
}
}

View 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
)
}
}

View 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)
}
}