Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 [HCPSDKFIORIUIKIT-2791]SwiftUI: floating value support (Cherry-pick) #849

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading