From b84cc941cbe0eb3468a0ef52fca54c9fba784516 Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Fri, 11 Oct 2024 16:05:31 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20sort=20&=20filter=20?= =?UTF-8?q?enhancement=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add listpick --- .../SortFilter/SortFilterExample.swift | 2 +- .../SortFilterView+Extensions.swift | 6 + .../CancellableResettableForm.swift | 47 +++++ .../DataTypes/SortFilter+DataType.swift | 181 +++++++++++++++++- .../Models/ModelDefinitions.swift | 15 ++ .../Views/OptionListPickerItem+View.swift | 106 ++++++---- .../OptionSearchListPickerItem+View.swift | 99 ++++++++++ .../FilterFeedbackBarItem+View.swift | 134 ++++++++++++- .../_SortFilterCFGItemContainer.swift | 20 ++ .../_SortFilterMenuItemContainer.swift | 2 + .../OptionListPickerItem+API.generated.swift | 2 +- ...onSearchListPickerItem+API.generated.swift | 28 +++ ...nSearchListPickerItem+View.generated.swift | 34 ++++ ...PickerItemModel+Extensions.generated.swift | 9 + .../en.lproj/FioriSwiftUICore.strings | 5 + 15 files changed, 646 insertions(+), 44 deletions(-) create mode 100644 Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift create mode 100644 Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift index 9a72c8d95..602c98cf7 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift @@ -6,7 +6,7 @@ struct SortFilterExample: View { [ .switch(item: .init(name: "Favorite", value: true, icon: "heart.fill"), showsOnFilterFeedbackBar: true), .switch(item: .init(name: "Tagged", value: nil, icon: "tag"), showsOnFilterFeedbackBar: false), - .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Rejected"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible), showsOnFilterFeedbackBar: true) + .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible, listPickerMode: .automatic), showsOnFilterFeedbackBar: true) ], [ .picker(item: .init(name: "Priority", value: [0], valueOptions: ["High", "Medium", "Low"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "filemenu.and.cursorarrow"), showsOnFilterFeedbackBar: true), diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift index 42b7dd3c4..402cf0225 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift @@ -14,6 +14,8 @@ extension View { return self.json(item: v) case .switch(let v, _): return self.json(item: v) + case .listPicker(let v, _): + return self.json(item: v) } } @@ -32,4 +34,8 @@ extension View { func json(item: SortFilterItem.SwitchItem) -> String { "{name: \(item.name), value: \(String(describing: item.value))}" } + + func json(item: SortFilterItem.ListPickerItem) -> String { + "{name: \(item.name), value: \(item.value)}" + } } diff --git a/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift b/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift index 06404f26f..c34e34a8d 100644 --- a/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift +++ b/Sources/FioriSwiftUICore/Components/CancellableResettableForm.swift @@ -43,6 +43,53 @@ struct CancellableResettableDialogForm: View { + let title: Title + + let components: Components + + var cancelAction: CancelAction + var resetAction: ResetAction + var applyAction: ApplyAction + + public init(@ViewBuilder title: () -> Title, + @ViewBuilder cancelAction: () -> CancelAction, + @ViewBuilder resetAction: () -> ResetAction, + @ViewBuilder applyAction: () -> ApplyAction, + @ViewBuilder components: () -> Components) + { + self.title = title() + self.cancelAction = cancelAction() + self.resetAction = resetAction() + self.applyAction = applyAction() + self.components = components() + } + + var body: some View { + NavigationStack { + VStack(spacing: UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) { + self.components.background(Color.preferredColor(.secondaryGroupedBackground)) + self.applyAction + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + self.title + } + ToolbarItem(placement: .topBarLeading) { + self.cancelAction + } + ToolbarItem(placement: .topBarTrailing) { + self.resetAction + } + } + } + .frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 375 : Screen.bounds.size.width) + .padding([.bottom], UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) + .background(Color.preferredColor(.chromeSecondary)) + } +} + struct ApplyButtonStyle: PrimitiveButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool diff --git a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift index 973262812..e3f04cc22 100644 --- a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift +++ b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift @@ -16,6 +16,8 @@ public enum SortFilterItem: Identifiable, Hashable { return item.id case .datetime(let item, _): return item.id + case .listPicker(let item, _): + return item.id } } @@ -58,6 +60,8 @@ public enum SortFilterItem: Identifiable, Hashable { /// 2. A section of view containing a SwiftUI Canlendar case datetime(item: DateTimeItem, showsOnFilterFeedbackBar: Bool) + case listPicker(item: ListPickerItem, showsOnFilterFeedbackBar: Bool) + public var showsOnFilterFeedbackBar: Bool { switch self { case .picker(_, let showsOnFilterFeedbackBar): @@ -70,6 +74,8 @@ public enum SortFilterItem: Identifiable, Hashable { return showsOnFilterFeedbackBar case .datetime(_, let showsOnFilterFeedbackBar): return showsOnFilterFeedbackBar + case .listPicker(_, let showsOnFilterFeedbackBar): + return showsOnFilterFeedbackBar } } @@ -101,6 +107,11 @@ public enum SortFilterItem: Identifiable, Hashable { hasher.combine(item.originalValue) hasher.combine(item.workingValue) hasher.combine(item.value) + case .listPicker(let item, _): + hasher.combine(item.id) + hasher.combine(item.originalValue) + hasher.combine(item.workingValue) + hasher.combine(item.value) } } } @@ -206,6 +217,26 @@ extension SortFilterItem { } } + var listPicker: ListPickerItem { + get { + switch self { + case .listPicker(let item, _): + return item + default: + fatalError("Unexpected value \(self)") + } + } + + set { + switch self { + case .listPicker(_, let showsOnFilterFeedbackBar): + self = .listPicker(item: newValue, showsOnFilterFeedbackBar: showsOnFilterFeedbackBar) + default: + fatalError("Unexpected value \(self)") + } + } + } + var isChanged: Bool { switch self { case .picker(let item, _): @@ -218,6 +249,8 @@ extension SortFilterItem { return item.isChanged case .slider(let item, _): return item.isChanged + case .listPicker(let item, _): + return item.isChanged } } @@ -233,6 +266,8 @@ extension SortFilterItem { return item.isOriginal case .slider(let item, _): return item.isOriginal + case .listPicker(let item, _): + return item.isOriginal } } @@ -253,6 +288,9 @@ extension SortFilterItem { case .slider(var item, _): item.cancel() self.slider = item + case .listPicker(var item, _): + item.cancel() + self.listPicker = item } } @@ -273,6 +311,9 @@ extension SortFilterItem { case .slider(var item, _): item.reset() self.slider = item + case .listPicker(var item, _): + item.reset() + self.listPicker = item } } @@ -293,6 +334,9 @@ extension SortFilterItem { case .slider(var item, _): item.apply() self.slider = item + case .listPicker(var item, _): + item.apply() + self.listPicker = item } } } @@ -311,9 +355,11 @@ public extension SortFilterItem { public let allowsEmptySelection: Bool public var showsValueForSingleSelected: Bool = true public let icon: String? + /// itemLayout is used when listPickerMode is filterFormCell, otherwise is ignored. public var itemLayout: OptionListPickerItemLayoutType = .fixed + public var listPickerMode: OptionListPickerMode = .automatic - public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed) { + public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, listPickerMode: OptionListPickerMode = .automatic) { self.id = id self.name = name self.value = value @@ -325,6 +371,7 @@ public extension SortFilterItem { self.showsValueForSingleSelected = showsValueForSingleSelected self.icon = icon self.itemLayout = itemLayout + self.listPickerMode = listPickerMode } mutating func onTap(option: String) { @@ -389,6 +436,15 @@ public extension SortFilterItem { self.workingValue.contains(index) } + mutating func selectAll(_ isAll: Bool) { + self.workingValue.removeAll() + if isAll { + for i in 0 ..< self.valueOptions.count { + self.workingValue.append(i) + } + } + } + var isChecked: Bool { !self.value.isEmpty } @@ -582,4 +638,127 @@ public extension SortFilterItem { self.workingValue == self.originalValue } } + + /// Data structure for filter feedback, option list picker, + struct ListPickerItem: Identifiable, Equatable { + public let id: String + public var name: String + public var value: [Int] + public var workingValue: [Int] + let originalValue: [Int] + + var valueOptions: [String] + public let allowsMultipleSelection: Bool + public let allowsEmptySelection: Bool + public var showsValueForSingleSelected: Bool = true + public let icon: String? + + public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil) { + self.id = id + self.name = name + self.value = value + self.workingValue = value + self.originalValue = value + self.valueOptions = valueOptions + self.allowsMultipleSelection = allowsMultipleSelection + self.allowsEmptySelection = allowsEmptySelection + self.showsValueForSingleSelected = showsValueForSingleSelected + self.icon = icon + } + + mutating func onTap(option: String) { + guard let index = valueOptions.firstIndex(of: option) else { return } + if self.workingValue.contains(index) { + if self.workingValue.count > 1 { + self.workingValue = self.workingValue.filter { $0 != index } + } else { + if self.allowsEmptySelection { + self.workingValue = [] + } else { + self.workingValue = index == 1 ? [0] : [1] + } + } + } else { + if self.allowsMultipleSelection { + self.workingValue.append(index) + } else { + self.workingValue = [index] + } + } + } + + mutating func optionOnTap(_ index: Int) { + if self.workingValue.contains(index) { + if self.workingValue.count > 1 { + self.workingValue = self.workingValue.filter { $0 != index } + } else { + if self.allowsEmptySelection { + self.workingValue = [] + } else { + self.workingValue = index == 1 ? [0] : [1] + } + } + } else { + if self.allowsMultipleSelection { + self.workingValue.append(index) + } else { + self.workingValue = [index] + } + } + } + + mutating func cancel() { + self.workingValue = self.value.map { $0 } + } + + mutating func reset() { + self.workingValue = self.originalValue.map { $0 } + } + + mutating func apply() { + self.value = self.workingValue.map { $0 } + } + + func isOptionSelected(_ option: String) -> Bool { + guard let idx = valueOptions.firstIndex(of: option) else { return false } + return self.workingValue.contains(idx) + } + + func isOptionSelected(index: Int) -> Bool { + self.workingValue.contains(index) + } + + mutating func selectAll(_ isAll: Bool) { + self.workingValue.removeAll() + if isAll { + for i in 0 ..< self.valueOptions.count { + self.workingValue.append(i) + } + } + } + + var isChecked: Bool { + !self.value.isEmpty + } + + var label: String { + if self.allowsMultipleSelection, self.value.count >= 1 { + if self.value.count == 1, self.showsValueForSingleSelected { + return self.valueOptions[self.value[0]] + } else { + return "\(self.name) (\(self.value.count))" + } + } else { + return self.name + } + } + + var isChanged: Bool { + self.value != self.workingValue + } + + var isOriginal: Bool { + self.workingValue == self.originalValue + } + } } diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index ed60a9d48..ae308e650 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -508,6 +508,7 @@ public protocol FilterFeedbackBarButtonModel: LeftIconComponent, TitleComponent // sourcery: add_env_props = "filterFeedbackBarStyle" // sourcery: generated_component_not_configurable +// sourcery: virtualPropHeight = "@State var _height: CGFloat = 0" public protocol OptionListPickerItemModel: OptionListPickerComponent { // sourcery: default.value = .fixed // sourcery: no_view @@ -518,6 +519,20 @@ public protocol OptionListPickerItemModel: OptionListPickerComponent { var onTap: ((_ index: Int) -> Void)? { get } } +// sourcery: add_env_props = "filterFeedbackBarStyle" +// sourcery: generated_component_not_configurable +// sourcery: virtualPropHeight = "@State var _height: CGFloat = 0" +// sourcery: virtualPropSearchText = "@State var _searchText: String = """ +// sourcery: virtualPropSearchViewCornerRadius = "@State var _searchViewCornerRadius: CGFloat = 18" +// sourcery: virtualPropSelectAll = "var selectAll: ((Bool) -> ())? = nil" +// sourcery: virtualPropAllowsMultipleSelection = "var allowsMultipleSelection: Bool = false" +// sourcery: virtualPropAllowsEmptySelection = "var allowsEmptySelection: Bool = false" +public protocol OptionSearchListPickerItemModel: OptionListPickerComponent { + // sourcery: default.value = nil + // sourcery: no_view + var onTap: ((_ index: Int) -> Void)? { get } +} + // sourcery: add_env_props = "filterFeedbackBarStyle" // sourcery: generated_component_not_configurable // sourcery: add_env_props = "fioriToggleStyle" diff --git a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift index 19a9ea8b3..f4e4b3166 100644 --- a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift @@ -1,4 +1,18 @@ import SwiftUI +import UIKit + +struct StatusBar { + private init() {} + + static var height: CGFloat { + #if os(visionOS) + 44 // default statusBar height for visionOS + #else + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return 0 } + return windowScene.statusBarManager?.statusBarFrame.height ?? 0 + #endif + } +} /// Available OptionListPickerItem layout types. Use this enum to define item layout type to present. public enum OptionListPickerItemLayoutType { @@ -18,45 +32,69 @@ extension OptionListPickerItem: View { } private func generateFixedContent() -> some View { - Grid(horizontalSpacing: 16) { - ForEach(0 ..< Int(ceil(Double(_valueOptions.count) / 2.0)), id: \.self) { rowIndex in - GridRow { - FilterFeedbackBarButton( - leftIcon: _value.wrappedValue.contains(rowIndex * 2) ? Image(systemName: "checkmark") : nil, - title: _valueOptions[rowIndex * 2], - isSelected: _value.wrappedValue.contains(rowIndex * 2) - ) - .onTapGesture { - _onTap?(rowIndex * 2) - } - if rowIndex * 2 + 1 < _valueOptions.count { + ScrollView(.vertical) { + Grid(horizontalSpacing: 16) { + ForEach(0 ..< Int(ceil(Double(_valueOptions.count) / 2.0)), id: \.self) { rowIndex in + GridRow { FilterFeedbackBarButton( - leftIcon: _value.wrappedValue.contains(rowIndex * 2 + 1) ? Image(systemName: "checkmark") : nil, - title: _valueOptions[rowIndex * 2 + 1], - isSelected: _value.wrappedValue.contains(rowIndex * 2 + 1) + leftIcon: _value.wrappedValue.contains(rowIndex * 2) ? Image(systemName: "checkmark") : nil, + title: _valueOptions[rowIndex * 2], + isSelected: _value.wrappedValue.contains(rowIndex * 2) ) .onTapGesture { - _onTap?(rowIndex * 2 + 1) + _onTap?(rowIndex * 2) + } + if rowIndex * 2 + 1 < _valueOptions.count { + FilterFeedbackBarButton( + leftIcon: _value.wrappedValue.contains(rowIndex * 2 + 1) ? Image(systemName: "checkmark") : nil, + title: _valueOptions[rowIndex * 2 + 1], + isSelected: _value.wrappedValue.contains(rowIndex * 2 + 1) + ) + .onTapGesture { + _onTap?(rowIndex * 2 + 1) + } } } } } } + .frame(height: _height) + .modifier(FioriIntrospectModifier(introspection: { scrollView in + DispatchQueue.main.async { + let popverHeight = Screen.bounds.size.height - StatusBar.height + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 + self._height = min(scrollView.contentSize.height, maxScrollViewHeight) + } + })) } private func generateFlexibleContent() -> some View { - OptionListPickerCustomLayout { - ForEach(0 ..< _valueOptions.count, id: \.self) { optionIndex in - FilterFeedbackBarButton( - leftIcon: _value.wrappedValue.contains(optionIndex) ? Image(systemName: "checkmark") : nil, - title: _valueOptions[optionIndex], - isSelected: _value.wrappedValue.contains(optionIndex) - ) - .onTapGesture { - _onTap?(optionIndex) + ScrollView(.vertical) { + OptionListPickerCustomLayout { + ForEach(0 ..< _valueOptions.count, id: \.self) { optionIndex in + FilterFeedbackBarButton( + leftIcon: _value.wrappedValue.contains(optionIndex) ? Image(systemName: "checkmark") : nil, + title: _valueOptions[optionIndex], + isSelected: _value.wrappedValue.contains(optionIndex) + ) + .onTapGesture { + _onTap?(optionIndex) + } } } } + .frame(height: _height) + .modifier(FioriIntrospectModifier(introspection: { scrollView in + DispatchQueue.main.async { + let popverHeight = Screen.bounds.size.height - StatusBar.height + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 + self._height = min(scrollView.contentSize.height, maxScrollViewHeight) + } + })) } } @@ -90,18 +128,19 @@ struct OptionListPickerCustomLayout: Layout { return .zero } var containerHeight = 0.0 - var currentRowX = 16.0 + var currentRowX = 0.0 + let padding = UIDevice.current.userInterfaceIdiom == .pad ? 13.0 : 16.0 for index in 0 ..< subviews.count { let subview = subviews[index] let subviewSize = subview.sizeThatFits(.unspecified) - let subviewWidth = min(subviewSize.width, containerWidth) + let subviewWidth = min(subviewSize.width, containerWidth - CGFloat(padding * 2)) if index == 0 { containerHeight += subviewSize.height } - if currentRowX + subviewWidth + 16.0 > containerWidth { + if currentRowX + subviewWidth + padding > containerWidth - CGFloat(padding * 2) { containerHeight += subviewSize.height containerHeight += 6 - currentRowX = 16.0 + currentRowX = 0.0 } currentRowX += subviewWidth + 6.0 } @@ -111,14 +150,15 @@ struct OptionListPickerCustomLayout: Layout { func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { guard let containerWidth = proposal.width else { return } var currentY: CGFloat = bounds.minY - var currentRowX = 16.0 + var currentRowX = 0.0 + let padding = UIDevice.current.userInterfaceIdiom == .pad ? 13.0 : 16.0 for subview in subviews { let subviewSize = subview.sizeThatFits(.unspecified) - let subviewWidth = min(subviewSize.width, containerWidth) - if currentRowX + subviewWidth + 16.0 > containerWidth { + let subviewWidth = min(subviewSize.width, containerWidth - CGFloat(padding * 2)) + if currentRowX + subviewWidth + padding > containerWidth - CGFloat(padding * 2) { currentY += subviewSize.height currentY += 6 - currentRowX = 16.0 + currentRowX = 0.0 subview.place(at: CGPoint(x: currentRowX, y: currentY), proposal: ProposedViewSize(width: subviewWidth, height: subviewSize.height)) } else { subview.place(at: CGPoint(x: currentRowX, y: currentY), proposal: ProposedViewSize(width: subviewWidth, height: subviewSize.height)) diff --git a/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift new file mode 100644 index 000000000..ba7dd9369 --- /dev/null +++ b/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift @@ -0,0 +1,99 @@ +import SwiftUI +import UIKit + +public extension OptionSearchListPickerItem { + init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil) { + self.init(value: value, valueOptions: valueOptions, hint: hint, onTap: onTap) + + self.allowsMultipleSelection = allowsMultipleSelection + self.allowsEmptySelection = allowsEmptySelection + self.selectAll = selectAll + } +} + +extension OptionSearchListPickerItem: View { + public var body: some View { + VStack(spacing: 0) { + if allowsMultipleSelection { + if _value.count != _valueOptions.count || allowsEmptySelection { + self.selectAllView() + } + } else if _value.count == _valueOptions.count { + self.selectAllView() + } + + Divider().edgesIgnoringSafeArea(.all) + List { + ForEach(_valueOptions.filter { _searchText.isEmpty || $0.localizedStandardContains(_searchText) }, id: \.self) { item in + let isSelected = self.isItemSelected(item) + HStack { + Text(item) + .lineLimit(1) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + .font(.fiori(forTextStyle: .body, weight: .regular)) + Spacer() + if isSelected { + Image(systemName: "checkmark") + .foregroundColor(.preferredColor(.tintColor)) + } + } + .frame(maxWidth: .infinity) + .padding(0) + .contentShape(Rectangle()) + .onTapGesture { + guard let index = findIndex(of: item) else { + return + } + _onTap?(index) + } + } + } + .listStyle(PlainListStyle()) + .frame(maxWidth: .infinity) + .scrollContentBackground(.hidden) + .padding(0) + .searchable(text: $_searchText, placement: .automatic) + } + } + + private func selectAllView() -> some View { + HStack { + Text(NSLocalizedString("All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + .font(.fiori(forTextStyle: .subheadline, weight: .regular)) + Spacer() + Button(action: { + selectAll?(_value.count != _valueOptions.count) + }) { + Text(_value.count == _valueOptions.count ? NSLocalizedString("Deselect All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") : NSLocalizedString("Select All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + } + } + .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) + .padding([.top, .bottom], 8) + } + + private func isItemSelected(_ item: String) -> Bool { + guard let index = findIndex(of: item) else { + return false + } + return _value.wrappedValue.contains(index) + } + + private func findIndex(of item: String) -> Int? { + for (index, value) in _valueOptions.enumerated() { + if value == item { + return index + } + } + return nil + } +} + +#Preview { + VStack { + Spacer() + OptionSearchListPickerItem(value: Binding<[Int]>(get: { [0, 1, 2] }, set: { print($0) }), valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review review", "Accepted", "Rejected"], hint: nil) + .frame(width: 375) + Spacer() + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index 0031060f1..9a49444b5 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -35,6 +35,18 @@ private extension View { } } +/// Available OptionListPicker modes. Use this enum to define picker mode to present. +public enum OptionListPickerMode { + /// Decided by options count + case automatic + /// FilterFormCell + case filterFormCell + /// Menu + case menu + /// List + case list +} + struct FilterFeedbackMenuItem: View { @Binding var item: SortFilterItem.PickerItem var onUpdate: () -> Void @@ -77,7 +89,7 @@ struct SliderMenuItem: View { .onTapGesture { self.isSheetVisible.toggle() } - .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { CancellableResettableDialogForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { @@ -129,10 +141,21 @@ struct PickerMenuItem: View { } var body: some View { - if self.item.valueOptions.count > 4 { + switch self.item.listPickerMode { + case .automatic: + if self.item.valueOptions.count > 8 { + self.list + } else if self.item.valueOptions.count > 4, self.item.valueOptions.count <= 8 { + self.button + } else { + self.menu + } + case .filterFormCell: self.button - } else { + case .menu: self.menu + case .list: + self.list } } @@ -142,7 +165,7 @@ struct PickerMenuItem: View { .onTapGesture { self.isSheetVisible.toggle() } - .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { CancellableResettableDialogForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { @@ -206,6 +229,46 @@ struct PickerMenuItem: View { } } } + + @ViewBuilder + var list: some View { + FilterFeedbackBarItem(leftIcon: icon(name: self.item.icon, isVisible: true), title: self.item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: self.item.isChecked) + .onTapGesture { + self.isSheetVisible.toggle() + } + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { + CancellableResettableDialogNavigationForm { + SortFilterItemTitle(title: self.item.name) + } cancelAction: { + _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.cancel() + self.isSheetVisible.toggle() + }) + .buttonStyle(CancelButtonStyle()) + } resetAction: { + _Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.reset() + }) + .buttonStyle(ResetButtonStyle()) + .disabled(self.item.isOriginal) + } applyAction: { + _Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.apply() + self.onUpdate() + self.isSheetVisible.toggle() + }) + .buttonStyle(ApplyButtonStyle()) + } components: { + OptionSearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in + self.item.onTap(option: self.item.valueOptions[index]) + } selectAll: { isAll in + self.item.selectAll(isAll) + } + .padding(0) + } + .presentationDetents([.large]) + } + } } struct HeightPreferenceKey: PreferenceKey { @@ -254,7 +317,7 @@ struct DateTimeMenuItem: View { .onTapGesture { self.isSheetVisible.toggle() } - .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { CancellableResettableDialogForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { @@ -306,8 +369,10 @@ struct DateTimeMenuItem: View { } .readHeight() .onPreferenceChange(HeightPreferenceKey.self) { height in - if let height { - self.detentHeight = height + DispatchQueue.main.async { + if let height { + self.detentHeight = height + } } } .presentationDetents([.height(self.detentHeight)]) @@ -370,6 +435,59 @@ struct SwitchMenuItem: View { } } +struct ListPickerMenuItem: View { + @Binding var item: SortFilterItem.ListPickerItem + var onUpdate: () -> Void + + @State var isSheetVisible = false + + @State var detentHeight: CGFloat = 0 + + public init(item: Binding, onUpdate: @escaping () -> Void) { + self._item = item + self.onUpdate = onUpdate + } + + var body: some View { + FilterFeedbackBarItem(leftIcon: icon(name: self.item.icon, isVisible: true), title: self.item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: self.item.isChecked) + .onTapGesture { + self.isSheetVisible.toggle() + } + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { + CancellableResettableDialogNavigationForm { + SortFilterItemTitle(title: self.item.name) + } cancelAction: { + _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.cancel() + self.isSheetVisible.toggle() + }) + .buttonStyle(CancelButtonStyle()) + } resetAction: { + _Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.reset() + }) + .buttonStyle(ResetButtonStyle()) + .disabled(self.item.isOriginal) + } applyAction: { + _Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { + self.item.apply() + self.onUpdate() + self.isSheetVisible.toggle() + }) + .buttonStyle(ApplyButtonStyle()) + } components: { + OptionSearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in + self.item.onTap(option: self.item.valueOptions[index]) + } selectAll: { isAll in + self.item.selectAll(isAll) + } + .padding(0) + } + .presentationDetents([.large]) + } + } +} + struct FullCFGMenuItem: View { @Environment(\.sortFilterMenuItemFullConfigurationButton) var fullCFGButton @@ -389,7 +507,7 @@ struct FullCFGMenuItem: View { .onTapGesture { self.isSheetVisible.toggle() } - .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { + .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { SortFilterView( title: { if let title = fullCFGButton.name { diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift index 9ddb7fac9..109619b55 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift @@ -38,6 +38,8 @@ extension _SortFilterCFGItemContainer: View { case .datetime: self.datetimePicker(row: r, column: c) .frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 375 : Screen.bounds.size.width) + case .listPicker: + self.listPicker(row: r, column: c) } } } @@ -196,4 +198,22 @@ extension _SortFilterCFGItemContainer: View { // .frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 375 - 13 * 2: UIScreen.main.bounds.size.width) } } + + func listPicker(row r: Int, column c: Int) -> some View { + VStack { + HStack { + Text(self._items[r][c].listPicker.name) + .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) + .foregroundColor(Color.preferredColor(.primaryLabel)) + Spacer() + } + OptionSearchListPickerItem( + value: Binding<[Int]>(get: { self._items[r][c].listPicker.workingValue }, set: { self._items[r][c].listPicker.workingValue = $0 }), + valueOptions: self._items[r][c].listPicker.valueOptions, + onTap: { index in + self._items[r][c].listPicker.onTap(option: self._items[r][c].listPicker.valueOptions[index]) + } + ) + } + } } diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift index 7870cd123..908f433ff 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift @@ -39,6 +39,8 @@ extension _SortFilterMenuItemContainer: View { SliderMenuItem(item: Binding(get: { self._items[r][c].slider }, set: { self._items[r][c].slider = $0 }), onUpdate: self.onUpdate) case .datetime: DateTimeMenuItem(item: Binding(get: { self._items[r][c].datetime }, set: { self._items[r][c].datetime = $0 }), onUpdate: self.onUpdate) + case .listPicker: + ListPickerMenuItem(item: Binding(get: { self._items[r][c].listPicker }, set: { self._items[r][c].listPicker = $0 }), onUpdate: self.onUpdate) } } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift index a3e3bd04b..94bc93b72 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift @@ -10,7 +10,7 @@ public struct OptionListPickerItem { var _hint: String? = nil var _itemLayout: OptionListPickerItemLayoutType var _onTap: ((_ index: Int) -> Void)? = nil - + @State var _height: CGFloat = 0 public init(model: OptionListPickerItemModel) { self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, itemLayout: model.itemLayout, onTap: model.onTap) } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift new file mode 100644 index 000000000..757d717dd --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift @@ -0,0 +1,28 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public struct OptionSearchListPickerItem { + @Environment(\.filterFeedbackBarStyle) var filterFeedbackBarStyle + + var _value: Binding<[Int]> + var _valueOptions: [String] + var _hint: String? = nil + var _onTap: ((_ index: Int) -> Void)? = nil + @State var _searchViewCornerRadius: CGFloat = 18 + var selectAll: ((Bool) -> ())? = nil + var allowsMultipleSelection: Bool = false + @State var _height: CGFloat = 0 + @State var _searchText: String = "" + var allowsEmptySelection: Bool = false + public init(model: OptionSearchListPickerItemModel) { + self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, onTap: model.onTap) + } + + public init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, onTap: ((_ index: Int) -> Void)? = nil) { + self._value = value + self._valueOptions = valueOptions + self._hint = hint + self._onTap = onTap + } +} diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift new file mode 100644 index 000000000..aa80703ed --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift @@ -0,0 +1,34 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift` +//TODO: Implement OptionSearchListPickerItem `View` body + +/// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible +/// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` +/// to declare a wrapped property +/// e.g.: `// sourcery: add_env_props = ["horizontalSizeClass"]` + +/* +import SwiftUI + +// FIXME: - Implement Fiori style definitions + +// FIXME: - Implement OptionSearchListPickerItem View body + +extension OptionSearchListPickerItem: View { + public var body: some View { + <# View body #> + } +} + +// FIXME: - Implement OptionSearchListPickerItem specific LibraryContentProvider + +@available(iOS 14.0, macOS 11.0, *) +struct OptionSearchListPickerItemLibraryContent: LibraryContentProvider { + @LibraryContentBuilder + var views: [LibraryItem] { + LibraryItem(OptionSearchListPickerItem(model: LibraryPreviewData.Person.laurelosborn), + category: .control) + } +} +*/ diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift new file mode 100644 index 000000000..ab1ca677f --- /dev/null +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift @@ -0,0 +1,9 @@ +// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT +import SwiftUI + +public extension OptionSearchListPickerItemModel { + var onTap: ((_ index: Int) -> Void)? { + return nil + } +} diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index 8424bd53b..c28d928be 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -253,3 +253,8 @@ "warnings" = "warnings"; /* XBUT: banner message type desc in plural form, errors */ "errors" = "errors"; + +/* XBUT: The \"Select All\" button title on the List Picker header */ +"Select All" = "Select All"; +/* XBUT: The \"Deselect All\" button title on the List Picker header */ +"Deselect All" = "Deselect All"; From 58440d5d8146cd5d918513c7f8f68e7174edf0dc Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Fri, 11 Oct 2024 16:51:37 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20sort=20&=20filter=20?= =?UTF-8?q?enhancement=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/OptionSearchListPickerItem+View.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift index ba7dd9369..bdd6077b3 100644 --- a/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift @@ -2,6 +2,15 @@ import SwiftUI import UIKit public extension OptionSearchListPickerItem { + /// create a list picker which used in FilterFeedbackBarItem + /// - Parameters: + /// - value: Selected value indexs. + /// - valueOptions: The data for constructing the list picker. + /// - hint: Hint message. + /// - allowsMultipleSelection: A boolean value to indicate to allow multiple selections or not. + /// - allowsEmptySelection: A boolean value to indicate to allow empty selection or not. + /// - onTap: The closure when tap on item. + /// - selectAll: The closure when click 'Select All' button. init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil) { self.init(value: value, valueOptions: valueOptions, hint: hint, onTap: onTap) @@ -80,12 +89,7 @@ extension OptionSearchListPickerItem: View { } private func findIndex(of item: String) -> Int? { - for (index, value) in _valueOptions.enumerated() { - if value == item { - return index - } - } - return nil + _valueOptions.firstIndex(where: { $0 == item }) } } From f953c7c29650c7a4cad64fc4ac03b1d9fc9e15b7 Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Fri, 11 Oct 2024 17:51:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20sort=20&=20filter=20?= =?UTF-8?q?enhancement=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_localization/en.lproj/FioriSwiftUICore.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index c28d928be..1c1d50b3a 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -258,3 +258,7 @@ "Select All" = "Select All"; /* XBUT: The \"Deselect All\" button title on the List Picker header */ "Deselect All" = "Deselect All"; +/* XBUT: Reset action of filter feedback bar, see https://experience.sap.com/fiori-design-ios/article/filter-feedback-bar/#behavior-and-interaction */ +"Reset" = "Reset"; +/* XBUT: Apply action of filter feedback bar, see https://experience.sap.com/fiori-design-ios/article/filter-feedback-bar/#behavior-and-interaction */ +"Apply" = "Apply"; From 4db8bef70a20eb04fbd5ee56d1f96ebc931e5ab7 Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Tue, 15 Oct 2024 13:40:22 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20sort=20&=20filter=20?= =?UTF-8?q?enhancement=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SortFilter/SortFilterExample.swift | 2 +- .../DataTypes/SortFilter+DataType.swift | 20 +++++++++++++++---- .../Models/ModelDefinitions.swift | 2 +- ....swift => SearchListPickerItem+View.swift} | 6 +++--- .../FilterFeedbackBarItem+View.swift | 18 +++-------------- .../_SortFilterCFGItemContainer.swift | 2 +- ... SearchListPickerItem+API.generated.swift} | 10 +++++----- .../SearchableListView+API.generated.swift | 2 +- ...SearchListPickerItem+View.generated.swift} | 14 ++++++------- ...ickerItemModel+Extensions.generated.swift} | 2 +- 10 files changed, 39 insertions(+), 39 deletions(-) rename Sources/FioriSwiftUICore/Views/{OptionSearchListPickerItem+View.swift => SearchListPickerItem+View.swift} (92%) rename Sources/FioriSwiftUICore/_generated/ViewModels/API/{OptionSearchListPickerItem+API.generated.swift => SearchListPickerItem+API.generated.swift} (90%) rename Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/{OptionSearchListPickerItem+View.generated.swift => SearchListPickerItem+View.generated.swift} (62%) rename Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/{OptionSearchListPickerItemModel+Extensions.generated.swift => SearchListPickerItemModel+Extensions.generated.swift} (78%) diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift index 602c98cf7..ef8d1c2a3 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift @@ -6,7 +6,7 @@ struct SortFilterExample: View { [ .switch(item: .init(name: "Favorite", value: true, icon: "heart.fill"), showsOnFilterFeedbackBar: true), .switch(item: .init(name: "Tagged", value: nil, icon: "tag"), showsOnFilterFeedbackBar: false), - .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible, listPickerMode: .automatic), showsOnFilterFeedbackBar: true) + .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible, displayMode: .automatic), showsOnFilterFeedbackBar: true) ], [ .picker(item: .init(name: "Priority", value: [0], valueOptions: ["High", "Medium", "Low"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "filemenu.and.cursorarrow"), showsOnFilterFeedbackBar: true), diff --git a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift index e3f04cc22..89c8cad0f 100644 --- a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift +++ b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift @@ -357,9 +357,21 @@ public extension SortFilterItem { public let icon: String? /// itemLayout is used when listPickerMode is filterFormCell, otherwise is ignored. public var itemLayout: OptionListPickerItemLayoutType = .fixed - public var listPickerMode: OptionListPickerMode = .automatic - - public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, listPickerMode: OptionListPickerMode = .automatic) { + public var displayMode: DisplayMode = .automatic + + /// Available OptionListPicker modes. Use this enum to define picker mode to present. + public enum DisplayMode { + /// Decided by options count + case automatic + /// FilterFormCell + case filterFormCell + /// Menu + case menu + /// List + case list + } + + public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic) { self.id = id self.name = name self.value = value @@ -371,7 +383,7 @@ public extension SortFilterItem { self.showsValueForSingleSelected = showsValueForSingleSelected self.icon = icon self.itemLayout = itemLayout - self.listPickerMode = listPickerMode + self.displayMode = displayMode } mutating func onTap(option: String) { diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index ae308e650..2fd076ab3 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -527,7 +527,7 @@ public protocol OptionListPickerItemModel: OptionListPickerComponent { // sourcery: virtualPropSelectAll = "var selectAll: ((Bool) -> ())? = nil" // sourcery: virtualPropAllowsMultipleSelection = "var allowsMultipleSelection: Bool = false" // sourcery: virtualPropAllowsEmptySelection = "var allowsEmptySelection: Bool = false" -public protocol OptionSearchListPickerItemModel: OptionListPickerComponent { +public protocol SearchListPickerItemModel: OptionListPickerComponent { // sourcery: default.value = nil // sourcery: no_view var onTap: ((_ index: Int) -> Void)? { get } diff --git a/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift similarity index 92% rename from Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift rename to Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift index bdd6077b3..b1427288c 100644 --- a/Sources/FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift @@ -1,7 +1,7 @@ import SwiftUI import UIKit -public extension OptionSearchListPickerItem { +public extension SearchListPickerItem { /// create a list picker which used in FilterFeedbackBarItem /// - Parameters: /// - value: Selected value indexs. @@ -20,7 +20,7 @@ public extension OptionSearchListPickerItem { } } -extension OptionSearchListPickerItem: View { +extension SearchListPickerItem: View { public var body: some View { VStack(spacing: 0) { if allowsMultipleSelection { @@ -96,7 +96,7 @@ extension OptionSearchListPickerItem: View { #Preview { VStack { Spacer() - OptionSearchListPickerItem(value: Binding<[Int]>(get: { [0, 1, 2] }, set: { print($0) }), valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review review", "Accepted", "Rejected"], hint: nil) + SearchListPickerItem(value: Binding<[Int]>(get: { [0, 1, 2] }, set: { print($0) }), valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review review", "Accepted", "Rejected"], hint: nil) .frame(width: 375) Spacer() } diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index 9a49444b5..d6334857d 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -35,18 +35,6 @@ private extension View { } } -/// Available OptionListPicker modes. Use this enum to define picker mode to present. -public enum OptionListPickerMode { - /// Decided by options count - case automatic - /// FilterFormCell - case filterFormCell - /// Menu - case menu - /// List - case list -} - struct FilterFeedbackMenuItem: View { @Binding var item: SortFilterItem.PickerItem var onUpdate: () -> Void @@ -141,7 +129,7 @@ struct PickerMenuItem: View { } var body: some View { - switch self.item.listPickerMode { + switch self.item.displayMode { case .automatic: if self.item.valueOptions.count > 8 { self.list @@ -259,7 +247,7 @@ struct PickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - OptionSearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in + SearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in self.item.onTap(option: self.item.valueOptions[index]) } selectAll: { isAll in self.item.selectAll(isAll) @@ -476,7 +464,7 @@ struct ListPickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - OptionSearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in + SearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in self.item.onTap(option: self.item.valueOptions[index]) } selectAll: { isAll in self.item.selectAll(isAll) diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift index 109619b55..68e38ff28 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift @@ -207,7 +207,7 @@ extension _SortFilterCFGItemContainer: View { .foregroundColor(Color.preferredColor(.primaryLabel)) Spacer() } - OptionSearchListPickerItem( + SearchListPickerItem( value: Binding<[Int]>(get: { self._items[r][c].listPicker.workingValue }, set: { self._items[r][c].listPicker.workingValue = $0 }), valueOptions: self._items[r][c].listPicker.valueOptions, onTap: { index in diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift similarity index 90% rename from Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift index 757d717dd..68ab58a35 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionSearchListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift @@ -2,20 +2,20 @@ // DO NOT EDIT import SwiftUI -public struct OptionSearchListPickerItem { +public struct SearchListPickerItem { @Environment(\.filterFeedbackBarStyle) var filterFeedbackBarStyle var _value: Binding<[Int]> var _valueOptions: [String] var _hint: String? = nil var _onTap: ((_ index: Int) -> Void)? = nil - @State var _searchViewCornerRadius: CGFloat = 18 - var selectAll: ((Bool) -> ())? = nil var allowsMultipleSelection: Bool = false - @State var _height: CGFloat = 0 @State var _searchText: String = "" var allowsEmptySelection: Bool = false - public init(model: OptionSearchListPickerItemModel) { + @State var _searchViewCornerRadius: CGFloat = 18 + var selectAll: ((Bool) -> ())? = nil + @State var _height: CGFloat = 0 + public init(model: SearchListPickerItemModel) { self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, onTap: model.onTap) } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchableListView+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchableListView+API.generated.swift index 29c5ada4b..6a16d441d 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchableListView+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchableListView+API.generated.swift @@ -12,9 +12,9 @@ public struct SearchableListView { let _cancelAction: CancelActionView let _doneAction: DoneActionView - var dataHandler: (() -> ())? = nil var isTopLevel: Bool = true var contentView: AnyView? = nil + var dataHandler: (() -> ())? = nil private var isModelInit: Bool = false private var isCancelActionNil: Bool = false diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SearchListPickerItem+View.generated.swift similarity index 62% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SearchListPickerItem+View.generated.swift index aa80703ed..65db096aa 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/OptionSearchListPickerItem+View.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Boilerplate/SearchListPickerItem+View.generated.swift @@ -1,7 +1,7 @@ // Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT -//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/OptionSearchListPickerItem+View.swift` -//TODO: Implement OptionSearchListPickerItem `View` body +//TODO: Copy commented code to new file: `FioriSwiftUICore/Views/SearchListPickerItem+View.swift` +//TODO: Implement SearchListPickerItem `View` body /// - Important: to make `@Environment` properties (e.g. `horizontalSizeClass`), internally accessible /// to extensions, add as sourcery annotation in `FioriSwiftUICore/Models/ModelDefinitions.swift` @@ -13,21 +13,21 @@ import SwiftUI // FIXME: - Implement Fiori style definitions -// FIXME: - Implement OptionSearchListPickerItem View body +// FIXME: - Implement SearchListPickerItem View body -extension OptionSearchListPickerItem: View { +extension SearchListPickerItem: View { public var body: some View { <# View body #> } } -// FIXME: - Implement OptionSearchListPickerItem specific LibraryContentProvider +// FIXME: - Implement SearchListPickerItem specific LibraryContentProvider @available(iOS 14.0, macOS 11.0, *) -struct OptionSearchListPickerItemLibraryContent: LibraryContentProvider { +struct SearchListPickerItemLibraryContent: LibraryContentProvider { @LibraryContentBuilder var views: [LibraryItem] { - LibraryItem(OptionSearchListPickerItem(model: LibraryPreviewData.Person.laurelosborn), + LibraryItem(SearchListPickerItem(model: LibraryPreviewData.Person.laurelosborn), category: .control) } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SearchListPickerItemModel+Extensions.generated.swift similarity index 78% rename from Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift rename to Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SearchListPickerItemModel+Extensions.generated.swift index ab1ca677f..58d6c7e66 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionSearchListPickerItemModel+Extensions.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/SearchListPickerItemModel+Extensions.generated.swift @@ -2,7 +2,7 @@ // DO NOT EDIT import SwiftUI -public extension OptionSearchListPickerItemModel { +public extension SearchListPickerItemModel { var onTap: ((_ index: Int) -> Void)? { return nil } From 81916981a0adc4dcf60ce6c9ce17b5c6d875ca44 Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Wed, 16 Oct 2024 16:54:09 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20sort=20&=20filter=20?= =?UTF-8?q?enhancement=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SortFilter/SortFilterExample.swift | 2 +- .../SortFilterView+Extensions.swift | 6 - .../DataTypes/SortFilter+DataType.swift | 169 +----------------- .../Models/ModelDefinitions.swift | 3 +- .../Views/OptionListPickerItem+View.swift | 42 +++-- .../Views/SearchListPickerItem+View.swift | 25 ++- .../FilterFeedbackBarItem+View.swift | 59 +----- .../_SortFilterCFGItemContainer.swift | 90 +++++++--- .../_SortFilterMenuItemContainer.swift | 2 - .../SearchListPickerItem+API.generated.swift | 7 +- 10 files changed, 131 insertions(+), 274 deletions(-) diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift index ef8d1c2a3..d0c8290be 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterExample.swift @@ -6,7 +6,7 @@ struct SortFilterExample: View { [ .switch(item: .init(name: "Favorite", value: true, icon: "heart.fill"), showsOnFilterFeedbackBar: true), .switch(item: .init(name: "Tagged", value: nil, icon: "tag"), showsOnFilterFeedbackBar: false), - .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .flexible, displayMode: .automatic), showsOnFilterFeedbackBar: true) + .picker(item: .init(name: "JIRA Status", value: [0], valueOptions: ["Received", "Started", "Hold", "Transfer", "Completed", "Pending Review Pending Pending Pending Pending Pending", "Accepted Medium", "Pending Medium", "Completed Medium"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "clock", itemLayout: .fixed, displayMode: .automatic), showsOnFilterFeedbackBar: true) ], [ .picker(item: .init(name: "Priority", value: [0], valueOptions: ["High", "Medium", "Low"], allowsMultipleSelection: true, allowsEmptySelection: true, showsValueForSingleSelected: false, icon: "filemenu.and.cursorarrow"), showsOnFilterFeedbackBar: true), diff --git a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift index 402cf0225..42b7dd3c4 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/SortFilter/SortFilterView+Extensions.swift @@ -14,8 +14,6 @@ extension View { return self.json(item: v) case .switch(let v, _): return self.json(item: v) - case .listPicker(let v, _): - return self.json(item: v) } } @@ -34,8 +32,4 @@ extension View { func json(item: SortFilterItem.SwitchItem) -> String { "{name: \(item.name), value: \(String(describing: item.value))}" } - - func json(item: SortFilterItem.ListPickerItem) -> String { - "{name: \(item.name), value: \(item.value)}" - } } diff --git a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift index 89c8cad0f..33473f377 100644 --- a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift +++ b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift @@ -16,8 +16,6 @@ public enum SortFilterItem: Identifiable, Hashable { return item.id case .datetime(let item, _): return item.id - case .listPicker(let item, _): - return item.id } } @@ -59,9 +57,7 @@ public enum SortFilterItem: Identifiable, Hashable { /// /// 2. A section of view containing a SwiftUI Canlendar case datetime(item: DateTimeItem, showsOnFilterFeedbackBar: Bool) - - case listPicker(item: ListPickerItem, showsOnFilterFeedbackBar: Bool) - + public var showsOnFilterFeedbackBar: Bool { switch self { case .picker(_, let showsOnFilterFeedbackBar): @@ -74,8 +70,6 @@ public enum SortFilterItem: Identifiable, Hashable { return showsOnFilterFeedbackBar case .datetime(_, let showsOnFilterFeedbackBar): return showsOnFilterFeedbackBar - case .listPicker(_, let showsOnFilterFeedbackBar): - return showsOnFilterFeedbackBar } } @@ -107,11 +101,6 @@ public enum SortFilterItem: Identifiable, Hashable { hasher.combine(item.originalValue) hasher.combine(item.workingValue) hasher.combine(item.value) - case .listPicker(let item, _): - hasher.combine(item.id) - hasher.combine(item.originalValue) - hasher.combine(item.workingValue) - hasher.combine(item.value) } } } @@ -217,26 +206,6 @@ extension SortFilterItem { } } - var listPicker: ListPickerItem { - get { - switch self { - case .listPicker(let item, _): - return item - default: - fatalError("Unexpected value \(self)") - } - } - - set { - switch self { - case .listPicker(_, let showsOnFilterFeedbackBar): - self = .listPicker(item: newValue, showsOnFilterFeedbackBar: showsOnFilterFeedbackBar) - default: - fatalError("Unexpected value \(self)") - } - } - } - var isChanged: Bool { switch self { case .picker(let item, _): @@ -249,8 +218,6 @@ extension SortFilterItem { return item.isChanged case .slider(let item, _): return item.isChanged - case .listPicker(let item, _): - return item.isChanged } } @@ -266,8 +233,6 @@ extension SortFilterItem { return item.isOriginal case .slider(let item, _): return item.isOriginal - case .listPicker(let item, _): - return item.isOriginal } } @@ -288,9 +253,6 @@ extension SortFilterItem { case .slider(var item, _): item.cancel() self.slider = item - case .listPicker(var item, _): - item.cancel() - self.listPicker = item } } @@ -311,9 +273,6 @@ extension SortFilterItem { case .slider(var item, _): item.reset() self.slider = item - case .listPicker(var item, _): - item.reset() - self.listPicker = item } } @@ -334,9 +293,6 @@ extension SortFilterItem { case .slider(var item, _): item.apply() self.slider = item - case .listPicker(var item, _): - item.apply() - self.listPicker = item } } } @@ -650,127 +606,4 @@ public extension SortFilterItem { self.workingValue == self.originalValue } } - - /// Data structure for filter feedback, option list picker, - struct ListPickerItem: Identifiable, Equatable { - public let id: String - public var name: String - public var value: [Int] - public var workingValue: [Int] - let originalValue: [Int] - - var valueOptions: [String] - public let allowsMultipleSelection: Bool - public let allowsEmptySelection: Bool - public var showsValueForSingleSelected: Bool = true - public let icon: String? - - public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, showsValueForSingleSelected: Bool = true, icon: String? = nil) { - self.id = id - self.name = name - self.value = value - self.workingValue = value - self.originalValue = value - self.valueOptions = valueOptions - self.allowsMultipleSelection = allowsMultipleSelection - self.allowsEmptySelection = allowsEmptySelection - self.showsValueForSingleSelected = showsValueForSingleSelected - self.icon = icon - } - - mutating func onTap(option: String) { - guard let index = valueOptions.firstIndex(of: option) else { return } - if self.workingValue.contains(index) { - if self.workingValue.count > 1 { - self.workingValue = self.workingValue.filter { $0 != index } - } else { - if self.allowsEmptySelection { - self.workingValue = [] - } else { - self.workingValue = index == 1 ? [0] : [1] - } - } - } else { - if self.allowsMultipleSelection { - self.workingValue.append(index) - } else { - self.workingValue = [index] - } - } - } - - mutating func optionOnTap(_ index: Int) { - if self.workingValue.contains(index) { - if self.workingValue.count > 1 { - self.workingValue = self.workingValue.filter { $0 != index } - } else { - if self.allowsEmptySelection { - self.workingValue = [] - } else { - self.workingValue = index == 1 ? [0] : [1] - } - } - } else { - if self.allowsMultipleSelection { - self.workingValue.append(index) - } else { - self.workingValue = [index] - } - } - } - - mutating func cancel() { - self.workingValue = self.value.map { $0 } - } - - mutating func reset() { - self.workingValue = self.originalValue.map { $0 } - } - - mutating func apply() { - self.value = self.workingValue.map { $0 } - } - - func isOptionSelected(_ option: String) -> Bool { - guard let idx = valueOptions.firstIndex(of: option) else { return false } - return self.workingValue.contains(idx) - } - - func isOptionSelected(index: Int) -> Bool { - self.workingValue.contains(index) - } - - mutating func selectAll(_ isAll: Bool) { - self.workingValue.removeAll() - if isAll { - for i in 0 ..< self.valueOptions.count { - self.workingValue.append(i) - } - } - } - - var isChecked: Bool { - !self.value.isEmpty - } - - var label: String { - if self.allowsMultipleSelection, self.value.count >= 1 { - if self.value.count == 1, self.showsValueForSingleSelected { - return self.valueOptions[self.value[0]] - } else { - return "\(self.name) (\(self.value.count))" - } - } else { - return self.name - } - } - - var isChanged: Bool { - self.value != self.workingValue - } - - var isOriginal: Bool { - self.workingValue == self.originalValue - } - } } diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index 2fd076ab3..68d5fd067 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -521,10 +521,11 @@ public protocol OptionListPickerItemModel: OptionListPickerComponent { // sourcery: add_env_props = "filterFeedbackBarStyle" // sourcery: generated_component_not_configurable -// sourcery: virtualPropHeight = "@State var _height: CGFloat = 0" +// sourcery: virtualPropHeight = "@State var _height: CGFloat = 44" // sourcery: virtualPropSearchText = "@State var _searchText: String = """ // sourcery: virtualPropSearchViewCornerRadius = "@State var _searchViewCornerRadius: CGFloat = 18" // sourcery: virtualPropSelectAll = "var selectAll: ((Bool) -> ())? = nil" +// sourcery: virtualPropUpdateSearchListPickerHeight = "var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil" // sourcery: virtualPropAllowsMultipleSelection = "var allowsMultipleSelection: Bool = false" // sourcery: virtualPropAllowsEmptySelection = "var allowsEmptySelection: Bool = false" public protocol SearchListPickerItemModel: OptionListPickerComponent { diff --git a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift index f4e4b3166..3b7a2a749 100644 --- a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift @@ -57,17 +57,20 @@ extension OptionListPickerItem: View { } } } + .background( + GeometryReader { geometry in + Color.clear + .onAppear { + let popverHeight = Screen.bounds.size.height - StatusBar.height + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 + self._height = min(geometry.size.height, maxScrollViewHeight) + } + } + ) } .frame(height: _height) - .modifier(FioriIntrospectModifier(introspection: { scrollView in - DispatchQueue.main.async { - let popverHeight = Screen.bounds.size.height - StatusBar.height - let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 - let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 - let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 - self._height = min(scrollView.contentSize.height, maxScrollViewHeight) - } - })) } private func generateFlexibleContent() -> some View { @@ -84,17 +87,20 @@ extension OptionListPickerItem: View { } } } + .background( + GeometryReader { geometry in + Color.clear + .onAppear { + let popverHeight = Screen.bounds.size.height - StatusBar.height + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 + self._height = min(geometry.size.height, maxScrollViewHeight) + } + } + ) } .frame(height: _height) - .modifier(FioriIntrospectModifier(introspection: { scrollView in - DispatchQueue.main.async { - let popverHeight = Screen.bounds.size.height - StatusBar.height - let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 - let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 - let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 120 - self._height = min(scrollView.contentSize.height, maxScrollViewHeight) - } - })) } } diff --git a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift index b1427288c..1826f2223 100644 --- a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift @@ -11,12 +11,14 @@ public extension SearchListPickerItem { /// - allowsEmptySelection: A boolean value to indicate to allow empty selection or not. /// - onTap: The closure when tap on item. /// - selectAll: The closure when click 'Select All' button. - init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil) { + /// - updateSearchListPickerHeight: The closure to update the parent view. + init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { self.init(value: value, valueOptions: valueOptions, hint: hint, onTap: onTap) self.allowsMultipleSelection = allowsMultipleSelection self.allowsEmptySelection = allowsEmptySelection self.selectAll = selectAll + self.updateSearchListPickerHeight = updateSearchListPickerHeight } } @@ -57,8 +59,29 @@ extension SearchListPickerItem: View { } } } + .modifier(FioriIntrospectModifier { scrollView in + DispatchQueue.main.async { + let popverHeight = Screen.bounds.size.height - StatusBar.height + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - 52 - 56 - 120 + + self._height = UIDevice.current.userInterfaceIdiom != .phone ? min(scrollView.contentSize.height, 396) : min(min(scrollView.contentSize.height, maxScrollViewHeight), 396) + var isSelectAllViewShow = false + if allowsMultipleSelection { + if _value.count != _valueOptions.count || allowsEmptySelection { + isSelectAllViewShow = true + } + } else if _value.count == _valueOptions.count { + isSelectAllViewShow = true + } + updateSearchListPickerHeight?(isSelectAllViewShow ? self._height + 44 : self._height) + } + }) .listStyle(PlainListStyle()) .frame(maxWidth: .infinity) + .frame(minWidth: UIDevice.current.userInterfaceIdiom != .phone ? 393 : nil) + .frame(height: self._height) .scrollContentBackground(.hidden) .padding(0) .searchable(text: $_searchText, placement: .automatic) diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index d6334857d..5dac4b0b4 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -251,9 +251,15 @@ struct PickerMenuItem: View { self.item.onTap(option: self.item.valueOptions[index]) } selectAll: { isAll in self.item.selectAll(isAll) + } updateSearchListPickerHeight: { height in + self.detentHeight = height + 52 + 56 + 70 } .padding(0) + Spacer() } + .frame(maxWidth: .infinity) + .frame(minWidth: UIDevice.current.userInterfaceIdiom != .phone ? 393 : nil) + .frame(height: UIDevice.current.userInterfaceIdiom != .phone ? self.detentHeight : nil) .presentationDetents([.large]) } } @@ -423,59 +429,6 @@ struct SwitchMenuItem: View { } } -struct ListPickerMenuItem: View { - @Binding var item: SortFilterItem.ListPickerItem - var onUpdate: () -> Void - - @State var isSheetVisible = false - - @State var detentHeight: CGFloat = 0 - - public init(item: Binding, onUpdate: @escaping () -> Void) { - self._item = item - self.onUpdate = onUpdate - } - - var body: some View { - FilterFeedbackBarItem(leftIcon: icon(name: self.item.icon, isVisible: true), title: self.item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: self.item.isChecked) - .onTapGesture { - self.isSheetVisible.toggle() - } - .popover(isPresented: self.$isSheetVisible, attachmentAnchor: .point(.bottom)) { - CancellableResettableDialogNavigationForm { - SortFilterItemTitle(title: self.item.name) - } cancelAction: { - _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { - self.item.cancel() - self.isSheetVisible.toggle() - }) - .buttonStyle(CancelButtonStyle()) - } resetAction: { - _Action(actionText: NSLocalizedString("Reset", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { - self.item.reset() - }) - .buttonStyle(ResetButtonStyle()) - .disabled(self.item.isOriginal) - } applyAction: { - _Action(actionText: NSLocalizedString("Apply", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { - self.item.apply() - self.onUpdate() - self.isSheetVisible.toggle() - }) - .buttonStyle(ApplyButtonStyle()) - } components: { - SearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection) { index in - self.item.onTap(option: self.item.valueOptions[index]) - } selectAll: { isAll in - self.item.selectAll(isAll) - } - .padding(0) - } - .presentationDetents([.large]) - } - } -} - struct FullCFGMenuItem: View { @Environment(\.sortFilterMenuItemFullConfigurationButton) var fullCFGButton diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift index 68e38ff28..3f8fbfecb 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift @@ -38,8 +38,6 @@ extension _SortFilterCFGItemContainer: View { case .datetime: self.datetimePicker(row: r, column: c) .frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 375 : Screen.bounds.size.width) - case .listPicker: - self.listPicker(row: r, column: c) } } } @@ -110,14 +108,38 @@ extension _SortFilterCFGItemContainer: View { .foregroundColor(Color.preferredColor(.primaryLabel)) Spacer() } - OptionListPickerItem( - value: Binding<[Int]>(get: { self._items[r][c].picker.workingValue }, set: { self._items[r][c].picker.workingValue = $0 }), - valueOptions: self._items[r][c].picker.valueOptions, - itemLayout: self._items[r][c].picker.itemLayout, - onTap: { index in + switch self._items[r][c].picker.displayMode { + case .automatic: + if self._items[r][c].picker.valueOptions.count > 8 { + SearchListPickerItem( + value: Binding<[Int]>(get: { self._items[r][c].picker.workingValue }, set: { self._items[r][c].picker.workingValue = $0 }), + valueOptions: self._items[r][c].picker.valueOptions, + allowsMultipleSelection: self._items[r][c].picker.allowsMultipleSelection, + allowsEmptySelection: self._items[r][c].picker.allowsEmptySelection + ) { index in + self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[index]) + } selectAll: { isAll in + self._items[r][c].picker.selectAll(isAll) + } + } else { + self.filterFormCell(row: r, column: c) + } + case .filterFormCell: + self.filterFormCell(row: r, column: c) + case .menu: + self.filterFormCell(row: r, column: c) + case .list: + SearchListPickerItem( + value: Binding<[Int]>(get: { self._items[r][c].picker.workingValue }, set: { self._items[r][c].picker.workingValue = $0 }), + valueOptions: self._items[r][c].picker.valueOptions, + allowsMultipleSelection: self._items[r][c].picker.allowsMultipleSelection, + allowsEmptySelection: self._items[r][c].picker.allowsEmptySelection + ) { index in self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[index]) + } selectAll: { isAll in + self._items[r][c].picker.selectAll(isAll) } - ) + } } } @@ -199,21 +221,47 @@ extension _SortFilterCFGItemContainer: View { } } - func listPicker(row r: Int, column c: Int) -> some View { - VStack { - HStack { - Text(self._items[r][c].listPicker.name) - .font(.fiori(forTextStyle: .subheadline, weight: .bold, isItalic: false, isCondensed: false)) - .foregroundColor(Color.preferredColor(.primaryLabel)) - Spacer() + private func filterFormCell(row r: Int, column c: Int) -> some View { + OptionListPickerItem( + value: Binding<[Int]>(get: { self._items[r][c].picker.workingValue }, set: { self._items[r][c].picker.workingValue = $0 }), + valueOptions: self._items[r][c].picker.valueOptions, + itemLayout: self._items[r][c].picker.itemLayout, + onTap: { index in + self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[index]) } - SearchListPickerItem( - value: Binding<[Int]>(get: { self._items[r][c].listPicker.workingValue }, set: { self._items[r][c].listPicker.workingValue = $0 }), - valueOptions: self._items[r][c].listPicker.valueOptions, - onTap: { index in - self._items[r][c].listPicker.onTap(option: self._items[r][c].listPicker.valueOptions[index]) + ) + } + + private func menuView(row r: Int, column c: Int) -> some View { + HStack { + Menu { + ForEach(self._items[r][c].picker.valueOptions.indices, id: \.self) { idx in + if self._items[r][c].picker.isOptionSelected(index: idx) { + Button { + self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[idx]) + self._items[r][c].picker.apply() + } label: { + Label { Text(self._items[r][c].picker.valueOptions[idx]) } icon: { Image(fioriName: "fiori.accept") } + } + } else { + Button(self._items[r][c].picker.valueOptions[idx]) { + self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[idx]) + self._items[r][c].picker.apply() + } + } } - ) + } label: { + FilterFeedbackBarItem(leftIcon: self.icon(name: self._items[r][c].picker.icon, isVisible: true), title: self._items[r][c].picker.label, isSelected: self._items[r][c].picker.isChecked) + } + } + } + + private func icon(name: String?, isVisible: Bool) -> Image? { + if isVisible { + if let name { + return Image(systemName: name) + } } + return nil } } diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift index 405502a44..106098b6d 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterMenuItemContainer.swift @@ -39,8 +39,6 @@ extension _SortFilterMenuItemContainer: View { SliderMenuItem(item: Binding(get: { self._items[r][c].slider }, set: { self._items[r][c].slider = $0 }), onUpdate: self.onUpdate) case .datetime: DateTimeMenuItem(item: Binding(get: { self._items[r][c].datetime }, set: { self._items[r][c].datetime = $0 }), onUpdate: self.onUpdate) - case .listPicker: - ListPickerMenuItem(item: Binding(get: { self._items[r][c].listPicker }, set: { self._items[r][c].listPicker = $0 }), onUpdate: self.onUpdate) } } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift index 68ab58a35..e0aa1387e 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift @@ -9,12 +9,13 @@ public struct SearchListPickerItem { var _valueOptions: [String] var _hint: String? = nil var _onTap: ((_ index: Int) -> Void)? = nil - var allowsMultipleSelection: Bool = false @State var _searchText: String = "" var allowsEmptySelection: Bool = false - @State var _searchViewCornerRadius: CGFloat = 18 var selectAll: ((Bool) -> ())? = nil - @State var _height: CGFloat = 0 + @State var _height: CGFloat = 44 + var allowsMultipleSelection: Bool = false + var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil + @State var _searchViewCornerRadius: CGFloat = 18 public init(model: SearchListPickerItemModel) { self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, onTap: model.onTap) }