Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor: 💡 [HCPSDKFIORIUIKIT-2891]FilterFeedbackbar FilterForm #988

Merged
23 changes: 22 additions & 1 deletion Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
120 changes: 17 additions & 103 deletions Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading