Skip to content

Commit

Permalink
feat: 🎸 [HCPSDKFIORIUIKIT-2791]SwiftUI: floating value support (#834) (…
Browse files Browse the repository at this point in the history
…#849)

* feat: 🎸 [HCPSDKFIORIUIKIT-2791]SwiftUI: floating value support

* feat: 🎸 [HCPSDKFIORIUIKIT-2791]Added a number formatter
  • Loading branch information
zzchao-1999 authored Oct 30, 2024
1 parent b204c53 commit 359d846
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int> { get }
var stepRange: ClosedRange<Double> { get }

/// Indicates whether the stepper field supports decimal values. Default is false.
// sourcery: defaultValue = false
var isDecimalSupported: Bool { get }
}

// sourcery: CompositeComponent
Expand Down
79 changes: 61 additions & 18 deletions Sources/FioriSwiftUICore/_FioriStyles/StepperFieldStyle.fiori.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>
let stepRange: ClosedRange<Double>
/// Indicates whether the stepper field supports decimal values. Default is false.
let isDecimalSupported: Bool

@Environment(\.stepperFieldStyle) var style

Expand All @@ -21,25 +23,28 @@ public struct StepperField {
public init(@ViewBuilder decrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.less } },
text: Binding<String>,
@ViewBuilder incrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.add } },
step: Int = 1,
stepRange: ClosedRange<Int>)
step: Double = 1,
stepRange: ClosedRange<Double>,
isDecimalSupported: Bool = false)
{
self.decrementAction = DecrementAction { decrementAction() }
self._text = text
self.incrementAction = IncrementAction { incrementAction() }
self.step = step
self.stepRange = stepRange
self.isDecimalSupported = isDecimalSupported
}
}

public extension StepperField {
init(decrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.less },
text: Binding<String>,
incrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.add },
step: Int = 1,
stepRange: ClosedRange<Int>)
step: Double = 1,
stepRange: ClosedRange<Double>,
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)
}
}

Expand All @@ -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
}
}
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>
public let step: Double
public let stepRange: ClosedRange<Double>
public let isDecimalSupported: Bool

public typealias DecrementAction = ConfigurationViewWrapper
public typealias IncrementAction = ConfigurationViewWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>
let stepRange: ClosedRange<Double>
/// Indicates whether the stepper field supports decimal values. Default is false.
let isDecimalSupported: Bool
let icon: any View
let description: any View

Expand All @@ -25,8 +27,9 @@ public struct StepperView {
@ViewBuilder decrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.less } },
text: Binding<String>,
@ViewBuilder incrementAction: () -> any View = { FioriButton { _ in FioriIcon.actions.add } },
step: Int = 1,
stepRange: ClosedRange<Int>,
step: Double = 1,
stepRange: ClosedRange<Double>,
isDecimalSupported: Bool = false,
@ViewBuilder icon: () -> any View = { EmptyView() },
@ViewBuilder description: () -> any View = { EmptyView() })
{
Expand All @@ -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)
}
}

Expand All @@ -46,12 +50,13 @@ public extension StepperView {
decrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.less },
text: Binding<String>,
incrementAction: FioriButton? = FioriButton { _ in FioriIcon.actions.add },
step: Int = 1,
stepRange: ClosedRange<Int>,
step: Double = 1,
stepRange: ClosedRange<Double>,
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) })
}
}

Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>
public let step: Double
public let stepRange: ClosedRange<Double>
public let isDecimalSupported: Bool
public let icon: Icon
public let description: Description

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 359d846

Please sign in to comment.