diff --git a/Apps/Examples/Examples/FioriSwiftUICore/FormCells/StepperViewExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/FormCells/StepperViewExample.swift index 826d9ab33..f53ead3d0 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/FormCells/StepperViewExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/FormCells/StepperViewExample.swift @@ -3,6 +3,8 @@ import SwiftUI struct StepperViewExample: View { @State var normalStepValue = "3" + @State var doubleStepValue = "6.0" + @State var multipleDigits = "6.895" @State var longTitleStepValue = "3" @State var customStyleStepValue = "3" @State var noFocusValue = "79" @@ -37,13 +39,34 @@ struct StepperViewExample: View { ) .disabled(self.isDisabled) + StepperView( + title: { Text("Value") }, + text: self.$doubleStepValue, + step: 0.5, + stepRange: 0.5 ... 80.5, + isDecimalSupported: true, + description: { Text("Double Value") } + ) + .disabled(self.isDisabled) + + StepperView( + title: { Text("Value") }, + text: self.$multipleDigits, + step: 0.005, + stepRange: 0.005 ... 80.895, + isDecimalSupported: true, + description: { Text("Multi-digit Double Value") } + ) + .disabled(self.isDisabled) + StepperView( title: { Text("Value") }, text: self.$negativeValue, stepRange: 10 ... 100, description: { Text(self.isInputValueValid ? "Hint Text" : "Validation failed.") } ).onChange(of: self.negativeValue, perform: { value in - if Int(value) ?? 1 > 80 { + let cValue = Double(value) ?? 10 + if cValue > 80 || cValue < 20 { self.isInputValueValid = false } else { self.isInputValueValid = true diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 912d3d9aa..74c13a93a 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -133,10 +133,14 @@ protocol _JouleWelcomeScreen: _MediaImageComponent, _GreetingTextComponent, _Tit protocol _StepperFieldComponent: _DecrementActionComponent, _TextInputFieldComponent, _IncrementActionComponent { /// The step value // sourcery: defaultValue = 1 - var step: Int { get } + var step: Double { get } /// a range of values - var stepRange: ClosedRange { get } + var stepRange: ClosedRange { get } + + /// Indicates whether the stepper field supports decimal values. Default is false. + // sourcery: defaultValue = false + var isDecimalSupported: Bool { get } } // sourcery: CompositeComponent diff --git a/Sources/FioriSwiftUICore/_FioriStyles/StepperFieldStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/StepperFieldStyle.fiori.swift index b2ae121f1..eb2febb62 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/StepperFieldStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/StepperFieldStyle.fiori.swift @@ -16,36 +16,79 @@ import SwiftUI // Base Layout style public struct StepperFieldBaseStyle: StepperFieldStyle { + @State private var previousValue: String = "" public func makeBody(_ configuration: StepperFieldConfiguration) -> some View { HStack(spacing: 0) { configuration.decrementAction .onSimultaneousTapGesture { - if var currentTextValue = Int(configuration.text) { - currentTextValue -= configuration.step - currentTextValue = currentTextValue < configuration.stepRange.lowerBound ? configuration.stepRange.lowerBound : currentTextValue - configuration.text = String(currentTextValue) - } + self.adjustValue(by: -configuration.step, configuration: configuration) } configuration._textInputField .textInputFieldStyle(.number) .onChange(of: configuration.text) { newValue in - let value = Int(newValue) - if value ?? 0 > configuration.stepRange.upperBound { - configuration.text = String(configuration.stepRange.upperBound) - } else if value ?? 0 < configuration.stepRange.lowerBound { - configuration.text = String(configuration.stepRange.lowerBound) - } + self.updateText(for: newValue, configuration: configuration) } configuration.incrementAction .onSimultaneousTapGesture { - if var currentTextValue = Int(configuration.text) { - currentTextValue += configuration.step - currentTextValue = currentTextValue > configuration.stepRange.upperBound ? configuration.stepRange.upperBound : currentTextValue - configuration.text = String(currentTextValue) - } + self.adjustValue(by: configuration.step, configuration: configuration) } } } + + private func adjustValue(by step: Double, configuration: StepperFieldConfiguration) { + let currentValue = Double(configuration.text) + let newValue = currentValue.map { $0 + step } ?? 0.0 + let clampedValue = self.clampValue(newValue, configuration: configuration) + if configuration.isDecimalSupported { + configuration.text = String(describing: clampedValue) + } else { + configuration.text = String(describing: Int(clampedValue)) + } + self.previousValue = configuration.text + } + + private func updateText(for text: String, configuration: StepperFieldConfiguration) { + if configuration.isDecimalSupported { + if let doubleValue = Double(text) { + let clampedValue = self.clampValue(doubleValue, configuration: configuration) + let formattedValue = self.numberFormatter(forStep: configuration.step).string(from: NSNumber(value: clampedValue)) ?? "" + configuration.text = formattedValue + } + } else { + if text.contains(".") || text.isEmpty { + configuration.text = self.previousValue + } else if let doubleValue = Double(text) { + let clampedValue = self.clampValue(doubleValue, configuration: configuration) + configuration.text = String(Int(clampedValue)) + } + } + self.previousValue = configuration.text + } + + private func clampValue(_ value: Double, configuration: StepperFieldConfiguration) -> Double { + min(max(value, configuration.stepRange.lowerBound), configuration.stepRange.upperBound) + } + + private func getDecimalPlaces(step: Double) -> Int { + let stepString = String(step) + if let decimalPointIndex = stepString.firstIndex(of: ".") { + let decimalPointPosition = stepString.distance(from: stepString.startIndex, to: decimalPointIndex) + let endPosition = stepString.distance(from: stepString.startIndex, to: stepString.endIndex) + let decimalPlacesCount = endPosition - decimalPointPosition - 1 + return max(0, decimalPlacesCount) + } else { + return 0 + } + } + + private func numberFormatter(forStep step: Double) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + let decimalPlaces = self.getDecimalPlaces(step: step) + formatter.minimumFractionDigits = decimalPlaces + formatter.maximumFractionDigits = decimalPlaces + return formatter + } } // Default fiori styles @@ -64,7 +107,7 @@ extension StepperFieldFioriStyle { @Environment(\.colorScheme) var colorScheme func makeBody(_ configuration: DecrementActionConfiguration) -> some View { - let isDecrementBtnEnabled: Bool = self.isEnabled ? Int(self.stepperFieldConfiguration.text) ?? self.stepperFieldConfiguration.stepRange.lowerBound > self.stepperFieldConfiguration.stepRange.lowerBound ? true : false : false + let isDecrementBtnEnabled: Bool = self.isEnabled ? Double(self.stepperFieldConfiguration.text) ?? self.stepperFieldConfiguration.stepRange.lowerBound > self.stepperFieldConfiguration.stepRange.lowerBound ? true : false : false let decrementDescFormat = NSLocalizedString("Decrease the value by %d", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") let decrementDesc = String(format: decrementDescFormat, stepperFieldConfiguration.step) return DecrementAction(configuration) @@ -93,7 +136,7 @@ extension StepperFieldFioriStyle { @Environment(\.colorScheme) var colorScheme func makeBody(_ configuration: IncrementActionConfiguration) -> some View { - let isIncrementBtnEnabled: Bool = self.isEnabled ? Int(self.stepperFieldConfiguration.text) ?? self.stepperFieldConfiguration.stepRange.upperBound < self.stepperFieldConfiguration.stepRange.upperBound ? true : false : false + let isIncrementBtnEnabled: Bool = self.isEnabled ? Double(self.stepperFieldConfiguration.text) ?? self.stepperFieldConfiguration.stepRange.upperBound < self.stepperFieldConfiguration.stepRange.upperBound ? true : false : false let incrementDescFormat = NSLocalizedString("Increase the value by %d", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") let incrementDesc = String(format: incrementDescFormat, stepperFieldConfiguration.step) return IncrementAction(configuration) diff --git a/Sources/FioriSwiftUICore/_FioriStyles/TextInputFieldStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/TextInputFieldStyle.fiori.swift index 1dbc75710..07f8e83cb 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/TextInputFieldStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/TextInputFieldStyle.fiori.swift @@ -26,7 +26,7 @@ public struct TextInputFieldNumberStyle: TextInputFieldStyle { .frame(minHeight: 44) .keyboardType(.numberPad) .onChange(of: configuration.text) { newValue in - let filtered = newValue.filter(\.isNumber) + let filtered = newValue.filter { $0.isNumber || $0 == "." } if filtered != newValue { configuration.text = filtered } diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperField.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperField.generated.swift index 6dccd189b..7fd4e464b 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperField.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperField.generated.swift @@ -10,9 +10,11 @@ public struct StepperField { @Binding var text: String let incrementAction: any View /// The step value - let step: Int + let step: Double /// a range of values - let stepRange: ClosedRange + let stepRange: ClosedRange + /// Indicates whether the stepper field supports decimal values. Default is false. + let isDecimalSupported: Bool @Environment(\.stepperFieldStyle) var style @@ -21,14 +23,16 @@ public struct StepperField { public init(@ViewBuilder decrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.less } }, text: Binding, @ViewBuilder incrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.add } }, - step: Int = 1, - stepRange: ClosedRange) + step: Double = 1, + stepRange: ClosedRange, + isDecimalSupported: Bool = false) { self.decrementAction = DecrementAction { decrementAction() } self._text = text self.incrementAction = IncrementAction { incrementAction() } self.step = step self.stepRange = stepRange + self.isDecimalSupported = isDecimalSupported } } @@ -36,10 +40,11 @@ public extension StepperField { init(decrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.less }, text: Binding, incrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.add }, - step: Int = 1, - stepRange: ClosedRange) + step: Double = 1, + stepRange: ClosedRange, + isDecimalSupported: Bool = false) { - self.init(decrementAction: { decrementAction }, text: text, incrementAction: { incrementAction }, step: step, stepRange: stepRange) + self.init(decrementAction: { decrementAction }, text: text, incrementAction: { incrementAction }, step: step, stepRange: stepRange, isDecimalSupported: isDecimalSupported) } } @@ -54,6 +59,7 @@ public extension StepperField { self.incrementAction = configuration.incrementAction self.step = configuration.step self.stepRange = configuration.stepRange + self.isDecimalSupported = configuration.isDecimalSupported self._shouldApplyDefaultStyle = shouldApplyDefaultStyle } } @@ -63,7 +69,7 @@ extension StepperField: View { if self._shouldApplyDefaultStyle { self.defaultStyle() } else { - self.style.resolve(configuration: .init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange)).typeErased + self.style.resolve(configuration: .init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, isDecimalSupported: self.isDecimalSupported)).typeErased .transformEnvironment(\.stepperFieldStyleStack) { stack in if !stack.isEmpty { stack.removeLast() @@ -81,7 +87,7 @@ private extension StepperField { } func defaultStyle() -> some View { - StepperField(.init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange)) + StepperField(.init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, isDecimalSupported: self.isDecimalSupported)) .shouldApplyDefaultStyle(false) .stepperFieldStyle(StepperFieldFioriStyle.ContentFioriStyle()) .typeErased diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperFieldStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperFieldStyle.generated.swift index e2c01c874..b0a8dfc77 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperFieldStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperField/StepperFieldStyle.generated.swift @@ -25,8 +25,9 @@ public struct StepperFieldConfiguration { public let decrementAction: DecrementAction @Binding public var text: String public let incrementAction: IncrementAction - public let step: Int - public let stepRange: ClosedRange + public let step: Double + public let stepRange: ClosedRange + public let isDecimalSupported: Bool public typealias DecrementAction = ConfigurationViewWrapper public typealias IncrementAction = ConfigurationViewWrapper diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperView.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperView.generated.swift index 64469349e..252cc05d8 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperView.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperView.generated.swift @@ -11,9 +11,11 @@ public struct StepperView { @Binding var text: String let incrementAction: any View /// The step value - let step: Int + let step: Double /// a range of values - let stepRange: ClosedRange + let stepRange: ClosedRange + /// Indicates whether the stepper field supports decimal values. Default is false. + let isDecimalSupported: Bool let icon: any View let description: any View @@ -25,8 +27,9 @@ public struct StepperView { @ViewBuilder decrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.less } }, text: Binding, @ViewBuilder incrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.add } }, - step: Int = 1, - stepRange: ClosedRange, + step: Double = 1, + stepRange: ClosedRange, + isDecimalSupported: Bool = false, @ViewBuilder icon: () -> any View = { EmptyView() }, @ViewBuilder description: () -> any View = { EmptyView() }) { @@ -36,8 +39,9 @@ public struct StepperView { self.incrementAction = IncrementAction { incrementAction() } self.step = step self.stepRange = stepRange - self.icon = Icon { icon() } - self.description = Description { description() } + self.isDecimalSupported = isDecimalSupported + self.icon = Icon(icon: icon) + self.description = Description(description: description) } } @@ -46,12 +50,13 @@ public extension StepperView { decrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.less }, text: Binding, incrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.add }, - step: Int = 1, - stepRange: ClosedRange, + step: Double = 1, + stepRange: ClosedRange, + isDecimalSupported: Bool = false, icon: Image? = nil, description: AttributedString? = nil) { - self.init(title: { Text(title) }, decrementAction: { decrementAction }, text: text, incrementAction: { incrementAction }, step: step, stepRange: stepRange, icon: { icon }, description: { OptionalText(description) }) + self.init(title: { Text(title) }, decrementAction: { decrementAction }, text: text, incrementAction: { incrementAction }, step: step, stepRange: stepRange, isDecimalSupported: isDecimalSupported, icon: { icon }, description: { OptionalText(description) }) } } @@ -67,6 +72,7 @@ public extension StepperView { self.incrementAction = configuration.incrementAction self.step = configuration.step self.stepRange = configuration.stepRange + self.isDecimalSupported = configuration.isDecimalSupported self.icon = configuration.icon self.description = configuration.description self._shouldApplyDefaultStyle = shouldApplyDefaultStyle @@ -78,7 +84,7 @@ extension StepperView: View { if self._shouldApplyDefaultStyle { self.defaultStyle() } else { - self.style.resolve(configuration: .init(title: .init(self.title), decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, icon: .init(self.icon), description: .init(self.description))).typeErased + self.style.resolve(configuration: .init(title: .init(self.title), decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, isDecimalSupported: self.isDecimalSupported, icon: .init(self.icon), description: .init(self.description))).typeErased .transformEnvironment(\.stepperViewStyleStack) { stack in if !stack.isEmpty { stack.removeLast() @@ -96,7 +102,7 @@ private extension StepperView { } func defaultStyle() -> some View { - StepperView(.init(title: .init(self.title), decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, icon: .init(self.icon), description: .init(self.description))) + StepperView(.init(title: .init(self.title), decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, isDecimalSupported: self.isDecimalSupported, icon: .init(self.icon), description: .init(self.description))) .shouldApplyDefaultStyle(false) .stepperViewStyle(StepperViewFioriStyle.ContentFioriStyle()) .typeErased diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperViewStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperViewStyle.generated.swift index 14412d789..6ce477eb1 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperViewStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/StepperView/StepperViewStyle.generated.swift @@ -26,8 +26,9 @@ public struct StepperViewConfiguration { public let decrementAction: DecrementAction @Binding public var text: String public let incrementAction: IncrementAction - public let step: Int - public let stepRange: ClosedRange + public let step: Double + public let stepRange: ClosedRange + public let isDecimalSupported: Bool public let icon: Icon public let description: Description diff --git a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift index 692361fe9..2d1af5be2 100755 --- a/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/SupportingFiles/StyleConfiguration+Extension.generated.swift @@ -125,7 +125,7 @@ extension StepperFieldConfiguration { extension StepperViewConfiguration { var _stepperField: StepperField { - StepperField(.init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange), shouldApplyDefaultStyle: true) + StepperField(.init(decrementAction: .init(self.decrementAction), text: self.$text, incrementAction: .init(self.incrementAction), step: self.step, stepRange: self.stepRange, isDecimalSupported: self.isDecimalSupported), shouldApplyDefaultStyle: true) } var _informationView: InformationView {