Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: 🎸 sort & filter enhancement part 2 #820

Merged
merged 9 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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: .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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,53 @@ struct CancellableResettableDialogForm<Title: View, CancelAction: View, ResetAct
}
}

struct CancellableResettableDialogNavigationForm<Title: View, CancelAction: View, ResetAction: View, ApplyAction: View, Components: View>: 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

Expand Down
30 changes: 27 additions & 3 deletions Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public enum SortFilterItem: Identifiable, Hashable {
///
/// 2. A section of view containing a SwiftUI Canlendar
case datetime(item: DateTimeItem, showsOnFilterFeedbackBar: Bool)

public var showsOnFilterFeedbackBar: Bool {
switch self {
case .picker(_, let showsOnFilterFeedbackBar):
Expand Down Expand Up @@ -311,9 +311,23 @@ 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 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 var displayMode: DisplayMode = .automatic

/// Available OptionListPicker modes. Use this enum to define picker mode to present.
public enum DisplayMode {
Copy link

Choose a reason for hiding this comment

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

Nesting Violation: Types should be nested at most 1 level deep (nesting)

Copy link

Choose a reason for hiding this comment

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

Nesting Violation: Types should be nested at most 1 level deep (nesting)

/// 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
Expand All @@ -325,6 +339,7 @@ public extension SortFilterItem {
self.showsValueForSingleSelected = showsValueForSingleSelected
self.icon = icon
self.itemLayout = itemLayout
self.displayMode = displayMode
}

mutating func onTap(option: String) {
Expand Down Expand Up @@ -389,6 +404,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
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -518,6 +519,21 @@ 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 = 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 {
// 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"
Expand Down
112 changes: 79 additions & 33 deletions Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -18,45 +32,75 @@ 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)
}
}
}
}
}
.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)
}

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)
}
}
}
.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)
}
}

Expand Down Expand Up @@ -90,18 +134,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
}
Expand All @@ -111,14 +156,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))
Expand Down
Loading
Loading