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 #834

Merged
merged 3 commits into from
Oct 28, 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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I control the interval of the stepper?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean the increment/decrement size? If so, you can use the "step" parameter.
var step: 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: decrementAction)
self._text = text
self.incrementAction = 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,6 +39,7 @@ public struct StepperView {
self.incrementAction = IncrementAction(incrementAction: incrementAction)
self.step = step
self.stepRange = stepRange
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 @@ -129,7 +129,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