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: 🎸 [JIRA: HCPSDKFIORIUIKIT-2688] [SwiftUI] Value Picker #872

Merged
merged 8 commits into from
Nov 12, 2024
4 changes: 4 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
3B62AB7E2C0EE257003262EB /* EditableSideBarExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */; };
3C180C282B858CF6007CE79A /* IllustratedMessageExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */; };
3CC870962CB6F4F30081909C /* ToastMessageExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC870952CB6F4F30081909C /* ToastMessageExample.swift */; };
55598FAD2CDDB4F6007CFFBB /* ValuePickerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55598FAC2CDDB4F6007CFFBB /* ValuePickerExample.swift */; };
6432FFA02C5164F8008ECE89 /* SegmentedControlExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432FF9F2C5164F8008ECE89 /* SegmentedControlExample.swift */; };
64905D072C6D13E20062AAD4 /* SwitchExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64905D062C6D13E20062AAD4 /* SwitchExample.swift */; };
64905D092C7693970062AAD4 /* DateTimePickerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64905D082C7693970062AAD4 /* DateTimePickerExample.swift */; };
Expand Down Expand Up @@ -242,6 +243,7 @@
3B62AB7C2C0EE257003262EB /* EditableSideBarExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableSideBarExample.swift; sourceTree = "<group>"; };
3C180C272B858CF6007CE79A /* IllustratedMessageExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IllustratedMessageExample.swift; sourceTree = "<group>"; };
3CC870952CB6F4F30081909C /* ToastMessageExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastMessageExample.swift; sourceTree = "<group>"; };
55598FAC2CDDB4F6007CFFBB /* ValuePickerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuePickerExample.swift; sourceTree = "<group>"; };
6432FF9F2C5164F8008ECE89 /* SegmentedControlExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlExample.swift; sourceTree = "<group>"; };
64905D062C6D13E20062AAD4 /* SwitchExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchExample.swift; sourceTree = "<group>"; };
64905D082C7693970062AAD4 /* DateTimePickerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimePickerExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -839,6 +841,7 @@
B1D41B1E291A2D2E004E64A5 /* Picker */ = {
isa = PBXGroup;
children = (
55598FAC2CDDB4F6007CFFBB /* ValuePickerExample.swift */,
B1D41B1F291A2D97004E64A5 /* DurationPickerExample.swift */,
6432FF9F2C5164F8008ECE89 /* SegmentedControlExample.swift */,
64905D082C7693970062AAD4 /* DateTimePickerExample.swift */,
Expand Down Expand Up @@ -1135,6 +1138,7 @@
6D10F8A02C7DB3F50071DD3E /* BannerMultiMessageExample.swift in Sources */,
8A557A1A24C12C820098003A /* ChartsContentView.swift in Sources */,
8A5579CE24C1293C0098003A /* SettingColor.swift in Sources */,
55598FAD2CDDB4F6007CFFBB /* ValuePickerExample.swift in Sources */,
1F55FEF32AC941FF00D7A1BE /* View+Extensions.swift in Sources */,
6DEC31F42C463ED50084DD20 /* FioriButtonTestsExample.swift in Sources */,
8A6DE30B28DD27F9003222E3 /* Colors.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ struct CoreContentView: View {
{
Text("DateTimePicker")
}

NavigationLink(
destination: ValuePickerExample())
{
Text("ValuePicker")
}
}

Section(header: Text("Onboarding")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import FioriSwiftUICore
import FioriThemeManager
import Foundation
import SwiftUI

struct ValuePickerExample: View {
let valueOptions: [AttributedString] = ["1", "20", "300", "123456789999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", "40", "55555555", "6", "777", "eight"]
@State var negativeIndex: Int = -1
@State var selectedIndex0: Int = 0
@State var selectedIndex2: Int = 2
@State var selectedIndex4: Int = 4

@State var isRequired = false
@State var isTrackingLiveChanges = true
@State var stateIndex: Int = 0
@State var stateArray: [ControlState] = [.normal, .disabled, .readOnly]
@State var showsErrorMessage = false
@State var isCustomTheming = false

struct CustomValuePickerStyle: ValuePickerStyle {
func makeBody(_ configuration: ValuePickerConfiguration) -> some View {
ValuePicker(configuration)
.valueLabelStyle { c in
c.valueLabel
.lineLimit(2)
.font(.fiori(forTextStyle: .callout))
.foregroundStyle(Color.preferredColor(.green7))
}
.titleStyle { c in
c.title
.lineLimit(2)
.font(.fiori(forTextStyle: .title3))
.foregroundStyle(Color.preferredColor(.indigo7))
.background(Color.preferredColor(.grey2))
}
.mandatoryFieldIndicatorStyle { c in
c.mandatoryFieldIndicator
.font(.fiori(forTextStyle: .largeTitle))
.foregroundStyle(Color.preferredColor(.red8))
}
}
}

var body: some View {
List {
Section {
Toggle("Mandatory", isOn: self.$isRequired)
Toggle("Is Tracking Live Changes", isOn: self.$isTrackingLiveChanges)
Picker("Control State", selection: self.$stateIndex) {
ForEach(0 ..< self.stateArray.count, id: \.self) { index in
let state = self.stateArray[index]
Text(self.valueForState(state: state))
}
}
Toggle("Show Error Message", isOn: self.$showsErrorMessage)
Toggle("Theming", isOn: self.$isCustomTheming)
}
Section {
ValuePicker(title: "Picker Title(Default Style)", isRequired: self.isRequired, options: self.valueOptions, selectedIndex: self.$negativeIndex, isTrackingLiveChanges: self.isTrackingLiveChanges, controlState: self.stateArray[self.stateIndex])

ValuePicker(title: "Picker Title (Long String) Long long long long long long long long long long long long long long long long long long title", isRequired: self.isRequired,
options: self.valueOptions, selectedIndex: self.$selectedIndex2, isTrackingLiveChanges: self.isTrackingLiveChanges, controlState: self.stateArray[self.stateIndex])
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("Please choose one available data")).informationViewStyle(.error)

ValuePicker(title: "Picker Always Expanded", isRequired: self.isRequired, options: self.valueOptions, selectedIndex: self.$selectedIndex4, isTrackingLiveChanges: self.isTrackingLiveChanges, alwaysShowPicker: true, controlState: self.stateArray[self.stateIndex]).informationView(isPresented: self.$showsErrorMessage, description: AttributedString("Please choose one available data"))
.informationViewStyle(.informational)

ValuePicker(title: "Picker Title 2 lines (Custom Style: First line Second line)", isRequired: self.isRequired,
options: self.valueOptions, selectedIndex: self.$selectedIndex0, isTrackingLiveChanges: self.isTrackingLiveChanges, controlState: self.stateArray[self.stateIndex])
.valuePickerStyle(CustomValuePickerStyle())
}
}.navigationTitle("Value Picker")
.ifApply(self.isCustomTheming) {
$0.onAppear { self.customTheming(Color.brown, Color.red) }
}
.ifApply(!self.isCustomTheming) {
$0.onAppear { self.resetTheming() }
}
}

func valueForState(state: ControlState) -> String {
var stateString = ""
switch state {
case .disabled:
stateString = "Disabled"
case .readOnly:
stateString = "Read Only"
default:
stateString = "Normal"
}
return stateString
}

func customTheming(_ expandedColor: Color, _ contractedColor: Color) {
self.resetTheming()

ThemeManager.shared.setColor(expandedColor, for: .primaryLabel, variant: .dark)
ThemeManager.shared.setColor(contractedColor, for: .tintColor, variant: .dark)
}

func resetTheming() {
StyleSheetSettings.reset()
ThemeManager.shared.setPalette(PaletteVersion.latest.palette)
}
}

struct ValuePickerExample_Previews: PreviewProvider {
static var previews: some View {
ValuePickerExample()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,33 @@ protocol _LoadingIndicatorViewComponent: _TitleComponent {
// sourcery: @Binding
var isPresented: Bool { get }
}

/// `ValuePicker` provides a title and value label with Fiori styling and a wheel-style `Picker`.
/// ## Usage
/// ```swift
/// let valueOptions :[AttributedString] = ["1", "20", "300"]
/// @State var selectedIndex: Int = 0
/// @State var isRequired = false
/// @State var stateIndex: Int = 0
/// @State var isTrackingLiveChanges = true
/// @State var showsErrorMessage = false
// ValuePicker(title: "Picker Title(Default Style)", isRequired: self.isRequired, options: self.valueOptions, selectedIndex: self.$selectedIndex, isTrackingLiveChanges: self.isTrackingLiveChanges).informationView(isPresented: self.$showsErrorMessage, description: AttributedString("Please choose one available data")).informationViewStyle(.informational)
/// ```
// sourcery: CompositeComponent
protocol _ValuePickerComponent: _TitleComponent, _ValueLabelComponent, _MandatoryField, _OptionsComponent {
// sourcery: @Binding
/// The index for the selected value in the valueOptions.
var selectedIndex: Int { get }

/// When `isTrackingLiveChanges` is true, the value will be shown every time a selection is made. If it is set to false, the value will only be displayed when the value picker is collapsed. The default setting is true.
// sourcery: defaultValue = true
var isTrackingLiveChanges: Bool { get set }

/// This property indicates whether the picker is to always be displayed. The default is false.
// sourcery: defaultValue = false
var alwaysShowPicker: Bool { get set }

// sourcery: defaultValue = .normal
/// The `ControlState` of the view. Currently, `.disabled`, `.normal` and `.readOnly` are supported. The default is `normal`.
var controlState: ControlState { get }
}
143 changes: 143 additions & 0 deletions Sources/FioriSwiftUICore/_FioriStyles/ValuePickerStyle.fiori.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import FioriThemeManager
import Foundation
import SwiftUI

// Base Layout style
public struct ValuePickerBaseStyle: ValuePickerStyle {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@State var valueString: AttributedString = .init("")
@State var valuePickerVisible: Bool = false
@FocusState var isFocused: Bool

public func makeBody(_ configuration: ValuePickerConfiguration) -> some View {
VStack {
if self.dynamicTypeSize >= .accessibility3 {
self.configureMainStack(configuration, isVertical: true)
} else {
ViewThatFits {
self.configureMainStack(configuration, isVertical: false)
self.configureMainStack(configuration, isVertical: true)
}
}
if self.valuePickerVisible || configuration.alwaysShowPicker {
Divider()
.frame(height: 0.3)
.foregroundStyle(Color.preferredColor(.separatorOpaque))
self.showPicker(configuration)
}
}
}

func configureMainStack(_ configuration: ValuePickerConfiguration, isVertical: Bool) -> some View {
let mainStack = isVertical ? AnyLayout(VStackLayout(alignment: .leading, spacing: 3)) : AnyLayout(HStackLayout())
return mainStack {
HStack(spacing: 0) {
configuration.title
if configuration.isRequired {
configuration.mandatoryFieldIndicator
}
}
if !isVertical {
Spacer()
} else {
Divider().hidden()
}
ValueLabel(valueLabel: self.getValueString(configuration)).foregroundStyle(self.getValueLabelFontColor(configuration))
}
.accessibilityElement(children: .combine)
.contentShape(Rectangle())
.ifApply(configuration.controlState != .disabled && configuration.controlState != .readOnly) {
$0.onTapGesture(perform: {
self.valuePickerVisible.toggle()
if self.valuePickerVisible {
let oIndex = configuration.selectedIndex
if oIndex >= 0, oIndex <= configuration.options.count {
self.valueString = configuration.options[oIndex]
}
}
})
}
}

func getValueString(_ configuration: ValuePickerConfiguration) -> AttributedString {
let oIndex = configuration.selectedIndex
var value = self.valueString

let isTrackingLive = configuration.isTrackingLiveChanges || configuration.alwaysShowPicker || (!configuration.alwaysShowPicker && !self.valuePickerVisible)
if isTrackingLive, oIndex >= 0, oIndex <= configuration.options.count {
value = configuration.options[oIndex]
}
return value
}

func getValueLabelFontColor(_ configuration: ValuePickerConfiguration) -> Color {
if configuration.controlState == .disabled {
return .preferredColor(.quaternaryLabel)
} else if self.valuePickerVisible, !configuration.alwaysShowPicker {
return .preferredColor(.tintColor)
} else {
return .preferredColor(.primaryLabel)
}
}

func showPicker(_ configuration: ValuePickerConfiguration) -> some View {
let picker = Picker("", selection: configuration.$selectedIndex) {
ForEach(0 ..< configuration.options.count, id: \.self, content: { index in
Text(configuration.options[index])
.ifApply(configuration.controlState == .disabled) {
$0.foregroundStyle(Color.preferredColor(.quaternaryLabel))
}
})
}.focused(self.$isFocused)
return picker
}
}

// Default fiori styles
extension ValuePickerFioriStyle {
struct ContentFioriStyle: ValuePickerStyle {
func makeBody(_ configuration: ValuePickerConfiguration) -> some View {
ValuePicker(configuration)
.pickerStyle(.wheel)
.disabled((configuration.controlState == .disabled || configuration.controlState == .readOnly) ? true : false)
}
}

struct TitleFioriStyle: TitleStyle {
let valuePickerConfiguration: ValuePickerConfiguration

func makeBody(_ configuration: TitleConfiguration) -> some View {
Title(configuration)
.foregroundStyle(Color.preferredColor(self.valuePickerConfiguration.controlState == .disabled ? .quaternaryLabel : .primaryLabel))
.font(.fiori(forTextStyle: .subheadline, weight: .semibold))
.lineLimit(1)
}
}

struct ValueLabelFioriStyle: ValueLabelStyle {
let valuePickerConfiguration: ValuePickerConfiguration

func makeBody(_ configuration: ValueLabelConfiguration) -> some View {
ValueLabel(configuration)
.font(.fiori(forTextStyle: .body))
.lineLimit(1)
}
}

struct MandatoryFieldIndicatorFioriStyle: MandatoryFieldIndicatorStyle {
let valuePickerConfiguration: ValuePickerConfiguration

func makeBody(_ configuration: MandatoryFieldIndicatorConfiguration) -> some View {
MandatoryFieldIndicator(configuration)
.foregroundStyle(Color.preferredColor(self.valuePickerConfiguration.controlState == .disabled ? .quaternaryLabel : .primaryLabel))
}
}

struct OptionsFioriStyle: OptionsStyle {
let valuePickerConfiguration: ValuePickerConfiguration

func makeBody(_ configuration: OptionsConfiguration) -> some View {
Options(configuration)
}
}
}
Loading
Loading