diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index d15ab7c5c..5e76103a5 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -530,6 +530,8 @@ public protocol FilterFeedbackBarButtonModel: LeftIconComponent, TitleComponent // sourcery: add_env_props = "filterFeedbackBarStyle" // sourcery: generated_component_not_configurable // sourcery: virtualPropHeight = "@State var _height: CGFloat = 0" +// sourcery: virtualPropUpdateSearchListPickerHeight = "var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil" +// sourcery: virtualPropBarItemFrame = "var barItemFrame: CGRect = .zero" public protocol OptionListPickerItemModel: OptionListPickerComponent { // sourcery: default.value = .fixed // sourcery: no_view diff --git a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift index 972474b6f..39e3037c2 100644 --- a/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/OptionListPickerItem+View.swift @@ -22,6 +22,24 @@ public enum OptionListPickerItemLayoutType { case flexible } +public extension OptionListPickerItem { + /// create a filter picker which is used in FilterFeedbackBarItem + /// - Parameters: + /// - value: Indexes for selected values. + /// - valueOptions: The data for constructing the list picker. + /// - hint: Hint message. + /// - itemLayout: Option item layout type. + /// - 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) + + self.barItemFrame = barItemFrame + self.updateSearchListPickerHeight = updateSearchListPickerHeight + } +} + extension OptionListPickerItem: View { public var body: some View { if _itemLayout == .flexible { @@ -61,19 +79,11 @@ extension OptionListPickerItem: View { GeometryReader { geometry in Color.clear .onAppear { - let popverHeight = Screen.bounds.size.height - let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 8 : 16) * 2 - let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) * 2 - let safeAreaInset = self.getSafeAreaInsets() - let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - safeAreaInset.top - safeAreaInset.bottom - (UIDevice.current.userInterfaceIdiom != .phone ? 210 : 60) - self._height = min(geometry.size.height, maxScrollViewHeight) + self.updateSearchListPickerHeight?(self.calculateHeight(scrollViewContentHeight: geometry.size.height)) } } ) } - .ifApply(UIDevice.current.userInterfaceIdiom == .phone, content: { v in - v.frame(height: _height) - }) } private func generateFlexibleContent() -> some View { @@ -94,19 +104,14 @@ extension OptionListPickerItem: View { GeometryReader { geometry in Color.clear .onAppear { - let popverHeight = Screen.bounds.size.height - let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 8 : 16) * 2 - let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) * 2 - let safeAreaInset = self.getSafeAreaInsets() - let maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - safeAreaInset.top - safeAreaInset.bottom - (UIDevice.current.userInterfaceIdiom != .phone ? 210 : 60) - self._height = min(geometry.size.height, maxScrollViewHeight) + self.updateSearchListPickerHeight?(self.calculateHeight(scrollViewContentHeight: geometry.size.height)) + } + .onChange(of: geometry.size) { _ in + self.updateSearchListPickerHeight?(self.calculateHeight(scrollViewContentHeight: geometry.size.height)) } } ) } - .ifApply(UIDevice.current.userInterfaceIdiom == .phone, content: { v in - v.frame(height: _height) - }) } private func getSafeAreaInsets() -> UIEdgeInsets { @@ -119,6 +124,30 @@ extension OptionListPickerItem: View { } return keyWindow.safeAreaInsets } + + private func calculateHeight(scrollViewContentHeight: CGFloat) -> CGFloat { + let screenHeight = Screen.bounds.size.height + let safeAreaInset = self.getSafeAreaInsets() + var maxScrollViewHeight = screenHeight - self.additionalHeight() + if UIDevice.current.userInterfaceIdiom != .phone { + if self.barItemFrame.arrowDirection() == .top { + maxScrollViewHeight -= (self.barItemFrame.maxY + 80) + } else if self.barItemFrame.arrowDirection() == .bottom { + maxScrollViewHeight -= (screenHeight - self.barItemFrame.minY + 80) + safeAreaInset.bottom + 13 + } + } else { + maxScrollViewHeight -= (safeAreaInset.top + 30) + } + return min(scrollViewContentHeight, maxScrollViewHeight) + } + + private func additionalHeight() -> CGFloat { + let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone + var height = 0.0 + height += self.getSafeAreaInsets().bottom + (isNotIphone ? 13 : 16) + height += isNotIphone ? 50 : 56 + return height + } } /* diff --git a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift index e33f2f609..ccb06c078 100644 --- a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift @@ -2,9 +2,9 @@ import SwiftUI import UIKit public extension SearchListPickerItem { - /// create a list picker which used in FilterFeedbackBarItem + /// create a list picker which is used in FilterFeedbackBarItem /// - Parameters: - /// - value: Selected value indexs. + /// - value: Indexes for selected values. /// - valueOptions: The data for constructing the list picker. /// - hint: Hint message. /// - allowsMultipleSelection: A boolean value to indicate to allow multiple selections or not. @@ -14,7 +14,7 @@ public extension SearchListPickerItem { /// - updateSearchListPickerHeight: The closure to update the parent view. /// - disableListEntriesSection: A boolean value to indicate to disable entries section or not. /// - allowsDisplaySelectionCount: A boolean value to indicate to display selection count or not. - /// - barItemFrame: The frame of the bar item, which toggle to show this view. + /// - barItemFrame: The frame of the item in FilterFeedbackBar, which toggle to show this view. init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, isSearchBarHidden: Bool = false, disableListEntriesSection: Bool, allowsDisplaySelectionCount: Bool, barItemFrame: CGRect = .zero, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { self.init(value: value, valueOptions: valueOptions, hint: hint, onTap: onTap) diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index e4eac9e89..5d049d3de 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -73,7 +73,10 @@ struct SliderMenuItem: View { @State var detentHeight: CGFloat = 0 @State var barItemFrame: CGRect = .zero - + let popoverWidth = 393.0 + @Environment(\.dynamicTypeSize) var dynamicTypeSize + @State private var geometrySizeHeight: CGFloat = 0 + var onUpdate: () -> Void public init(item: Binding, onUpdate: @escaping () -> Void) { @@ -87,7 +90,7 @@ struct SliderMenuItem: View { self.isSheetVisible.toggle() } .popover(isPresented: self.$isSheetVisible, arrowEdge: self.barItemFrame.arrowDirection()) { - CancellableResettableDialogForm { + CancellableResettableDialogNavigationForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { @@ -111,14 +114,26 @@ struct SliderMenuItem: View { } components: { SliderPickerItem(value: Binding(get: { self.item.workingValue }, set: { self.item.workingValue = $0 }), formatter: self.item.formatter, minimumValue: self.item.minimumValue, maximumValue: self.item.maximumValue) - .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) - } - .readHeight() - .onPreferenceChange(HeightPreferenceKey.self) { height in - if let height { - self.detentHeight = height - } + .padding([.leading, .trailing], 16) + .background(GeometryReader { geometry in + Color.clear + .onAppear { + self.geometrySizeHeight = geometry.size.height + self.calculateDetentHeight() + } + .onChange(of: geometry.size) { newSize in + self.geometrySizeHeight = newSize.height + self.calculateDetentHeight() + } + }) + .onChange(of: self.dynamicTypeSize) { _ in + self.calculateDetentHeight() + } } + .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in + v.frame(width: self.popoverWidth) + }) + .frame(minHeight: self.detentHeight) .presentationDetents([.height(self.detentHeight)]) } .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in @@ -135,6 +150,46 @@ struct SliderMenuItem: View { }) }) } + + private func calculateDetentHeight() { + let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone + var calculateHeight = self.geometrySizeHeight + calculateHeight += isNotIphone ? 13 : 16 + calculateHeight += isNotIphone ? 50 : 56 + if !isNotIphone { + calculateHeight += UIEdgeInsets.getSafeAreaInsets().bottom + } + #if !os(visionOS) + calculateHeight += UIDevice.current.userInterfaceIdiom != .phone ? 45 : 0 + #else + calculateHeight += 85 + #endif + calculateHeight += self.dynamicTypeAddHeight() + self.detentHeight = calculateHeight + } + + private func dynamicTypeAddHeight() -> CGFloat { + switch self.dynamicTypeSize { + case .xLarge: + return 15 + case .xxLarge: + return 20 + case .xxxLarge: + return 25 + case .accessibility1: + return 30 + case .accessibility2: + return 35 + case .accessibility3: + return 40 + case .accessibility4: + return 45 + case .accessibility5: + return 55 + default: + return 0 + } + } } struct PickerMenuItem: View { @@ -179,7 +234,7 @@ struct PickerMenuItem: View { self.isSheetVisible.toggle() } .popover(isPresented: self.$isSheetVisible, arrowEdge: self.barItemFrame.arrowDirection()) { - CancellableResettableDialogForm { + CancellableResettableDialogNavigationForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { @@ -209,20 +264,29 @@ struct PickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - OptionListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, itemLayout: self.item.itemLayout) { index in + 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 + } + #if !os(visionOS) + calculateHeight += UIDevice.current.userInterfaceIdiom != .phone ? 55 : 0 + #else + calculateHeight += 95 + #endif + self.detentHeight = calculateHeight } - .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) + .padding([.leading, .trailing], 16) } + .frame(height: self.detentHeight) .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in - v.frame(minHeight: 155) + v.frame(width: self.popoverWidth) }) - .readHeight() - .onPreferenceChange(HeightPreferenceKey.self) { height in - if let height { - self.detentHeight = height - } - } .presentationDetents([.height(self.detentHeight)]) } .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in @@ -350,7 +414,14 @@ struct PickerMenuItem: View { height += 52 } } - height += UIDevice.current.userInterfaceIdiom != .phone ? 63 : 33 + if !isNotIphone { + height += UIEdgeInsets.getSafeAreaInsets().bottom + } + #if !os(visionOS) + height += UIDevice.current.userInterfaceIdiom != .phone ? 55 : 0 + #else + height += 75 + #endif if height > Screen.bounds.size.height - self.getSafeAreaInsets().top - 60 { return Screen.bounds.size.height / 2 } @@ -431,7 +502,7 @@ struct DateTimeMenuItem: View { self.isSheetVisible.toggle() } .popover(isPresented: self.$isSheetVisible, arrowEdge: self.barItemFrame.arrowDirection()) { - CancellableResettableDialogForm { + CancellableResettableDialogNavigationForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { @@ -466,7 +537,8 @@ struct DateTimeMenuItem: View { ) .labelsHidden() } - .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) + .padding([.leading, .trailing], 16) + .frame(minHeight: 40) DatePicker( self.item.label, @@ -475,19 +547,30 @@ struct DateTimeMenuItem: View { ) .datePickerStyle(.graphical) .labelsHidden() - .frame(width: UIDevice.current.userInterfaceIdiom != .phone ? self.popoverWidth - 13 : Screen.bounds.size.width - 16) + .frame(minHeight: 320) .clipped() } .frame(width: UIDevice.current.userInterfaceIdiom != .phone ? self.popoverWidth : Screen.bounds.size.width) + .frame(minHeight: 440) + .background(GeometryReader { geometry in + Color.clear + .onAppear { + let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone + var calculateHeight = geometry.size.height + calculateHeight += isNotIphone ? 13 : 16 + calculateHeight += isNotIphone ? 50 : 56 + if !isNotIphone { + calculateHeight += UIEdgeInsets.getSafeAreaInsets().bottom + } + calculateHeight += UIDevice.current.userInterfaceIdiom != .phone ? 55 : 0 + self.detentHeight = calculateHeight + } + }) } - .readHeight() - .onPreferenceChange(HeightPreferenceKey.self) { height in - DispatchQueue.main.async { - if let height { - self.detentHeight = height - } - } - } + .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in + v.frame(width: self.popoverWidth) + }) + .frame(minHeight: self.detentHeight) .presentationDetents([.height(self.detentHeight)]) } .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in @@ -572,6 +655,9 @@ struct StepperMenuItem: View { var onUpdate: () -> Void @State var stepperViewHeight: CGFloat = 110 + let popoverWidth = 393.0 + @Environment(\.dynamicTypeSize) var dynamicTypeSize + @State private var geometrySizeHeight: CGFloat = 0 public init(item: Binding, onUpdate: @escaping () -> Void) { self._item = item @@ -584,7 +670,7 @@ struct StepperMenuItem: View { self.isSheetVisible.toggle() } .popover(isPresented: self.$isSheetVisible, arrowEdge: self.barItemFrame.arrowDirection()) { - CancellableResettableDialogForm { + CancellableResettableDialogNavigationForm { SortFilterItemTitle(title: self.item.name) } cancelAction: { _Action(actionText: NSLocalizedString("Cancel", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: ""), didSelectAction: { @@ -641,17 +727,26 @@ struct StepperMenuItem: View { v.incrementActionStyle(.deactivate) } .frame(minHeight: self.stepperViewHeight) - .padding(0) - .sizeReader { s in - self.stepperViewHeight = max(self.stepperViewHeight, s.height) - } - } - .readHeight() - .onPreferenceChange(HeightPreferenceKey.self) { height in - if let height { - self.detentHeight = height + .background(GeometryReader { geometry in + Color.clear + .onAppear { + self.geometrySizeHeight = geometry.size.height + self.calculateDetentHeight() + } + .onChange(of: geometry.size) { newSize in + self.geometrySizeHeight = newSize.height + self.calculateDetentHeight() + } + }) + .onChange(of: self.dynamicTypeSize) { _ in + self.stepperViewHeight = 110 + self.dynamicTypeAddHeight() + self.calculateDetentHeight() } } + .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in + v.frame(width: self.popoverWidth) + }) + .frame(minHeight: self.detentHeight) .presentationDetents([.height(self.detentHeight)]) } .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in @@ -668,6 +763,46 @@ struct StepperMenuItem: View { }) }) } + + private func calculateDetentHeight() { + let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone + var calculateHeight = self.geometrySizeHeight + calculateHeight += isNotIphone ? 13 : 16 + calculateHeight += isNotIphone ? 50 : 56 + if !isNotIphone { + calculateHeight += UIEdgeInsets.getSafeAreaInsets().bottom + } + #if !os(visionOS) + calculateHeight += UIDevice.current.userInterfaceIdiom != .phone ? 45 : 0 + #else + calculateHeight += 85 + #endif + calculateHeight += self.dynamicTypeAddHeight() + self.detentHeight = calculateHeight + } + + private func dynamicTypeAddHeight() -> CGFloat { + switch self.dynamicTypeSize { + case .xLarge: + return 15 + case .xxLarge: + return 20 + case .xxxLarge: + return 25 + case .accessibility1: + return 30 + case .accessibility2: + return 35 + case .accessibility3: + return 40 + case .accessibility4: + return 45 + case .accessibility5: + return 55 + default: + return 0 + } + } } struct FullCFGMenuItem: View { @@ -749,6 +884,19 @@ extension CGRect { } } +extension UIEdgeInsets { + static func getSafeAreaInsets() -> UIEdgeInsets { + guard let keyWindow = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) + .flatMap({ $0 as? UIWindowScene })?.windows + .first(where: \.isKeyWindow) + else { + return .zero + } + return keyWindow.safeAreaInsets + } +} + #Preview { VStack { Spacer() diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift index 94bc93b72..2b9d7b204 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/OptionListPickerItem+API.generated.swift @@ -11,6 +11,8 @@ public struct OptionListPickerItem { var _itemLayout: OptionListPickerItemLayoutType var _onTap: ((_ index: Int) -> Void)? = nil @State var _height: CGFloat = 0 + var barItemFrame: CGRect = .zero + var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil 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) }