diff --git a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift index 9fac66d49..f8b6ee989 100644 --- a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift +++ b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift @@ -408,6 +408,7 @@ public extension SortFilterItem { struct PickerItem: Identifiable, Equatable { public let id: String public var name: String + public var title: String? public var value: [Int] public var workingValue: [Int] let originalValue: [Int] @@ -461,9 +462,29 @@ public extension SortFilterItem { case disable } - public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, barItemDisplayMode: BarItemDisplayMode = .name, isSearchBarHidden: Bool = false, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic, listEntriesSectionMode: ListEntriesSectionMode = .default, allowsDisplaySelectionCount: Bool = true, resetButtonConfiguration: FilterFeedbackBarResetButtonConfiguration = FilterFeedbackBarResetButtonConfiguration()) { + /// Create PickerItem for filter feedback. + /// When `displayMode` is `.filterFormCell`, the styles of options can be customized by some styles of FilterFormView, such as: + /// filterFormOptionAttributes, filterFormOptionMinHeight, filterFormOptionMinTouchHeight, filterFormOptionCornerRadius, filterFormOptionPadding, filterFormOptionTitleSpacing, filterFormOptionsItemSpacing, filterFormOptionsLineSpacing. + /// - Parameters: + /// - id: The unique identifier for PickerItem. + /// - name: Item name. + /// - title: Title label of the options. + /// - value: Item selected value. + /// - valueOptions: Item options. + /// - allowsMultipleSelection: A boolean value to indicate to allow multiple selections or not. + /// - allowsEmptySelection: A boolean value to indicate to allow empty selections or not. + /// - barItemDisplayMode: Name display mode for the bar. + /// - isSearchBarHidden: A boolean value to indicate to search bar hidden or not. + /// - icon: Icon at the leading side of the item. + /// - itemLayout: Options layout type when `displayMode` is `.filterFormCell`. + /// - displayMode: Options display mode. + /// - listEntriesSectionMode: List entries section mode when `displayMode` is `.list`. + /// - allowsDisplaySelectionCount: A boolean value to indicate to allow display selection count or not. + /// - resetButtonConfiguration: A configuration to customize the reset button. + public init(id: String = UUID().uuidString, name: String, title: String? = nil, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, barItemDisplayMode: BarItemDisplayMode = .name, isSearchBarHidden: Bool = false, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic, listEntriesSectionMode: ListEntriesSectionMode = .default, allowsDisplaySelectionCount: Bool = true, resetButtonConfiguration: FilterFeedbackBarResetButtonConfiguration = FilterFeedbackBarResetButtonConfiguration()) { self.id = id self.name = name + self.title = title self.value = value self.workingValue = value self.originalValue = value diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index 3b7336f15..1f34544ed 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -602,10 +602,22 @@ public protocol FilterFeedbackBarButtonModel {} // sourcery: virtualPropUpdateSearchListPickerHeight = "var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil" // sourcery: virtualPropBarItemFrame = "var barItemFrame: CGRect = .zero" public protocol OptionListPickerItemModel: OptionListPickerComponent { + // sourcery: default.value = nil + // sourcery: no_view + var title: String? { get set } + // sourcery: default.value = .fixed // sourcery: no_view var itemLayout: OptionListPickerItemLayoutType { get set } + // sourcery: default.value = true + // sourcery: no_view + var allowsMultipleSelection: Bool { get set } + + // sourcery: default.value = false + // sourcery: no_view + var allowsEmptySelection: Bool { get set } + // sourcery: default.value = nil // sourcery: no_view var onTap: ((_ index: Int) -> Void)? { get } diff --git a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift index 1c86d79fe..90f7439b0 100644 --- a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift @@ -28,12 +28,15 @@ public extension OptionListPickerItem { /// - value: Indexes for selected values. /// - valueOptions: The data for constructing the list picker. /// - hint: Hint message. + /// - title: Title label of the options. /// - itemLayout: Option item layout type. + /// - allowsMultipleSelection: A boolean value to indicate to allow multiple selections or not. + /// - allowsEmptySelection: A boolean value to indicate to allow empty selections or not. /// - barItemFrame: The frame of the item in FilterFeedbackBar, which toggle to show this view. /// - onTap: The closure when tap on item. /// - updateSearchListPickerHeight: The closure to update the parent view. - init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, barItemFrame: CGRect = .zero, onTap: ((_ index: Int) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { - self.init(value: value, valueOptions: valueOptions, hint: hint, itemLayout: itemLayout, onTap: onTap) + init(value: Binding<[Int]>, valueOptions: [String] = [], title: String? = nil, hint: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, allowsMultipleSelection: Bool = true, allowsEmptySelection: Bool = false, barItemFrame: CGRect = .zero, onTap: ((_ index: Int) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { + self.init(value: value, valueOptions: valueOptions, hint: hint, title: title, itemLayout: itemLayout, allowsMultipleSelection: allowsMultipleSelection, allowsEmptySelection: allowsEmptySelection, onTap: onTap) self.barItemFrame = barItemFrame self.updateSearchListPickerHeight = updateSearchListPickerHeight @@ -42,64 +45,21 @@ public extension OptionListPickerItem { extension OptionListPickerItem: View { public var body: some View { - if _itemLayout == .flexible { - self.generateFlexibleContent() - } else { - self.generateFixedContent() - } - } - - private func generateFixedContent() -> some View { - ScrollView(.vertical) { - Grid(horizontalSpacing: 16) { - ForEach(0 ..< Int(ceil(Double(_valueOptions.count) / 2.0)), id: \.self) { rowIndex in - GridRow { - FilterFeedbackBarButton( - icon: _value.wrappedValue.contains(rowIndex * 2) ? Image(systemName: "checkmark") : nil, - title: AttributedString(_valueOptions[rowIndex * 2]), - isSelected: _value.wrappedValue.contains(rowIndex * 2) - ) - .onTapGesture { - _onTap?(rowIndex * 2) - } - if rowIndex * 2 + 1 < _valueOptions.count { - FilterFeedbackBarButton( - icon: _value.wrappedValue.contains(rowIndex * 2 + 1) ? Image(systemName: "checkmark") : nil, - title: AttributedString(_valueOptions[rowIndex * 2 + 1]), - isSelected: _value.wrappedValue.contains(rowIndex * 2 + 1) - ) - .onTapGesture { - _onTap?(rowIndex * 2 + 1) - } - } - } - } - } - .background( - GeometryReader { geometry in - Color.clear - .onAppear { - self.updateSearchListPickerHeight?(self.calculateHeight(scrollViewContentHeight: geometry.size.height)) - } - } - ) - } - } - - private func generateFlexibleContent() -> some View { ScrollView(.vertical) { - OptionListPickerCustomLayout { - ForEach(0 ..< _valueOptions.count, id: \.self) { optionIndex in - FilterFeedbackBarButton( - icon: _value.wrappedValue.contains(optionIndex) ? Image(systemName: "checkmark") : nil, - title: AttributedString(_valueOptions[optionIndex]), - isSelected: _value.wrappedValue.contains(optionIndex) - ) - .onTapGesture { - _onTap?(optionIndex) - } + FilterFormView(title: { + if let title = _title, !title.isEmpty { + Text(title) + .font(.fiori(forTextStyle: .subheadline, weight: .semibold)) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + } else { + EmptyView() } + }, mandatoryFieldIndicator: { + EmptyView() + }, isRequired: false, options: _valueOptions.map { AttributedString($0) }, isEnabled: true, allowsMultipleSelection: _allowsMultipleSelection, allowsEmptySelection: _allowsEmptySelection, value: _value, buttonSize: _itemLayout == .flexible ? .flexible : .fixed, isSingleLine: false) { _ in } + .padding([.leading, .trailing], 16) + .filterFormOptionsLineSpacing(10) .background( GeometryReader { geometry in Color.clear @@ -173,49 +133,3 @@ extension OptionListPickerItem: View { Spacer() } } - -struct OptionListPickerCustomLayout: Layout { - func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { - guard let containerWidth = proposal.width else { - return .zero - } - var containerHeight = 0.0 - var currentRowX = 0.0 - let padding = UIDevice.current.userInterfaceIdiom != .phone ? 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 - CGFloat(padding * 2)) - if index == 0 { - containerHeight += subviewSize.height - } - if currentRowX + subviewWidth + padding > containerWidth - CGFloat(padding * 2) { - containerHeight += subviewSize.height - containerHeight += 6 - currentRowX = 0.0 - } - currentRowX += subviewWidth + 6.0 - } - return CGSize(width: containerWidth, height: containerHeight) - } - - 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 = 0.0 - let padding = UIDevice.current.userInterfaceIdiom != .phone ? 13.0 : 16.0 - for subview in subviews { - let subviewSize = subview.sizeThatFits(.unspecified) - let subviewWidth = min(subviewSize.width, containerWidth - CGFloat(padding * 2)) - if currentRowX + subviewWidth + padding > containerWidth - CGFloat(padding * 2) { - currentY += subviewSize.height - currentY += 6 - 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)) - } - currentRowX += subviewWidth + 6.0 - } - } -} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItemSubview.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItemSubview.swift index eb7033957..e60068b37 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItemSubview.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItemSubview.swift @@ -298,24 +298,36 @@ struct PickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - OptionListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, itemLayout: self.item.itemLayout, barItemFrame: self.barItemFrame) { index in - self.item.onTap(option: self.item.valueOptions[index]) - } updateSearchListPickerHeight: { height in - let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone - var calculateHeight = height - calculateHeight += isNotIphone ? 13 : 16 - calculateHeight += isNotIphone ? 50 : 56 - if !isNotIphone { - calculateHeight += UIEdgeInsets.getSafeAreaInsets().bottom + ScrollView(.vertical) { + FilterFormView(title: { + if let title = self.item.title, !title.isEmpty { + Text(title) + .font(.fiori(forTextStyle: .subheadline, weight: .semibold)) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + } else { + EmptyView() + } + }, mandatoryFieldIndicator: { + EmptyView() + }, isRequired: false, options: self.item.valueOptions.map { AttributedString($0) }, isEnabled: true, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection, value: self.$item.workingValue, buttonSize: self.item.itemLayout == .flexible ? .flexible : .fixed, isSingleLine: false) { _ in } - #if !os(visionOS) - calculateHeight += UIDevice.current.userInterfaceIdiom != .phone ? 55 : 0 - #else - calculateHeight += 95 - #endif - self.detentHeight = calculateHeight + .filterFormOptionsLineSpacing(10) + .background( + GeometryReader { geometry in + Color.clear + .onAppear { + self.detentHeight = self.calcluateFilterFormViewPopoverHeight(scrollViewContentHeight: geometry.size.height) + } + .setOnChange(of: geometry.frame(in: .global), action1: { _ in + self.detentHeight = self.calcluateFilterFormViewPopoverHeight(scrollViewContentHeight: geometry.size.height) + }) { _, _ in + self.detentHeight = self.calcluateFilterFormViewPopoverHeight(scrollViewContentHeight: geometry.size.height) + } + } + ) + .padding([.leading, .trailing], 16) + .padding(.bottom, 10) } - .padding([.leading, .trailing], 16) } .frame(height: self.detentHeight) .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in @@ -345,7 +357,7 @@ struct PickerMenuItem: View { ForEach(self.item.valueOptions.indices, id: \.self) { idx in if self.item.isOptionSelected(index: idx) { Button { - self.item.onTap(option: self.item.valueOptions[idx]) + self.item.optionOnTap(idx) self.item.apply() self.onUpdate() } label: { @@ -547,6 +559,26 @@ struct PickerMenuItem: View { return self.item.workingValue.isEmpty } } + + private func calcluateFilterFormViewPopoverHeight(scrollViewContentHeight: CGFloat) -> CGFloat { + let screenHeight = Screen.bounds.size.height + let safeAreaInset = UIEdgeInsets.getSafeAreaInsets() + var maxPopoverViewHeight = 0.0 + var calaulatePopoverViewHeight = scrollViewContentHeight + if UIDevice.current.userInterfaceIdiom != .phone { + if self.barItemFrame.arrowDirection() == .top { + maxPopoverViewHeight = screenHeight - self.barItemFrame.maxY - safeAreaInset.bottom - 30 + } else if self.barItemFrame.arrowDirection() == .bottom { + maxPopoverViewHeight = screenHeight - (screenHeight - self.barItemFrame.minY) + safeAreaInset.top + } + calaulatePopoverViewHeight += 50 + 70 + } else { + maxPopoverViewHeight = screenHeight - safeAreaInset.top - 30 + calaulatePopoverViewHeight += 56 + 20 + safeAreaInset.bottom + } + + return min(maxPopoverViewHeight, calaulatePopoverViewHeight) + } } private extension View { diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_FilterFeedbackBarItem+View.swift index af8deaf96..865cde204 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_FilterFeedbackBarItem+View.swift @@ -356,7 +356,7 @@ struct _PickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - OptionListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, itemLayout: self.item.itemLayout, barItemFrame: self.barItemFrame) { index in + OptionListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, title: self.item.title, itemLayout: self.item.itemLayout, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection, barItemFrame: self.barItemFrame) { index in self.item.onTap(option: self.item.valueOptions[index]) } updateSearchListPickerHeight: { height in let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone @@ -373,7 +373,6 @@ struct _PickerMenuItem: View { #endif self.detentHeight = calculateHeight } - .padding([.leading, .trailing], 16) } .frame(height: self.detentHeight) .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift index feacc6bde..483a3b816 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift @@ -8,20 +8,26 @@ public struct OptionListPickerItem { var _value: Binding<[Int]> var _valueOptions: [String] var _hint: String? = nil + var _title: String? = nil var _itemLayout: OptionListPickerItemLayoutType + var _allowsMultipleSelection: Bool + var _allowsEmptySelection: Bool var _onTap: ((_ index: Int) -> Void)? = nil var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil @State var _height: CGFloat = 0 var barItemFrame: CGRect = .zero 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) + self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, title: model.title, itemLayout: model.itemLayout, allowsMultipleSelection: model.allowsMultipleSelection, allowsEmptySelection: model.allowsEmptySelection, onTap: model.onTap) } - public init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, onTap: ((_ index: Int) -> Void)? = nil) { + public init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, title: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, allowsMultipleSelection: Bool = true, allowsEmptySelection: Bool = false, onTap: ((_ index: Int) -> Void)? = nil) { self._value = value self._valueOptions = valueOptions self._hint = hint + self._title = title self._itemLayout = itemLayout + self._allowsMultipleSelection = allowsMultipleSelection + self._allowsEmptySelection = allowsEmptySelection self._onTap = onTap } } diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift index edfe69ff3..9c1cd6d11 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift @@ -9,20 +9,20 @@ public struct SearchListPickerItem { var _valueOptions: [String] var _hint: String? = nil var _onTap: ((_ index: Int) -> Void)? = nil - @State var _searchViewCornerRadius: CGFloat = 18 - @State var _searchText: String = "" + var allowsDisplaySelectionCount: Bool = true + @State var _keyboardHeight: CGFloat = 0.0 var isSearchBarHidden: Bool = false + @State var _searchText: String = "" + var allowsMultipleSelection: Bool = false + let popoverWidth = 393.0 + var disableListEntriesSection: Bool = false @State var _height: CGFloat = 44 var allowsEmptySelection: Bool = false - var allowsDisplaySelectionCount: Bool = true - var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil - var uuidValueOptions: [[String: String]] = [] + @State var _searchViewCornerRadius: CGFloat = 18 var selectAll: ((Bool) -> ())? = nil - var allowsMultipleSelection: Bool = false - var disableListEntriesSection: Bool = false - let popoverWidth = 393.0 var barItemFrame: CGRect = .zero - @State var _keyboardHeight: CGFloat = 0.0 + var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil + var uuidValueOptions: [[String: String]] = [] 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/_SortFilterView+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SortFilterView+API.generated.swift index 54e9b67b6..8794e4cd5 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SortFilterView+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/_SortFilterView+API.generated.swift @@ -16,9 +16,9 @@ public struct _SortFilterView Void)? - @State var size: CGSize = .zero - @StateObject var context: SortFilterContext = SortFilterContext() let popoverWidth = 393.0 + @StateObject var context: SortFilterContext = SortFilterContext() + @State var size: CGSize = .zero private var isModelInit: Bool = false private var isCancelActionNil: Bool = false diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerItemModel+Extensions.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerItemModel+Extensions.generated.swift index 0ccb053d8..ae7dea8c0 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerItemModel+Extensions.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/Model+Extensions/OptionListPickerItemModel+Extensions.generated.swift @@ -3,10 +3,22 @@ import SwiftUI public extension OptionListPickerItemModel { - var itemLayout: OptionListPickerItemLayoutType { + var title: String? { + return nil + } + + var itemLayout: OptionListPickerItemLayoutType { return .fixed } + var allowsMultipleSelection: Bool { + return true + } + + var allowsEmptySelection: Bool { + return false + } + var onTap: ((_ index: Int) -> Void)? { return nil }