From 1de32ac9615c419b7d594fdece8c22cbbc752212 Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Mon, 9 Dec 2024 17:41:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20[IOSSDKBUG-482]iPad?= =?UTF-8?q?=20popover=20FilterFeedback=20list=20jumping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/FioriPopoverSize.swift | 117 ++++++++++++++++++ .../FilterFeedbackBarItem+View.swift | 77 ++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 Sources/FioriSwiftUICore/Utils/FioriPopoverSize.swift diff --git a/Sources/FioriSwiftUICore/Utils/FioriPopoverSize.swift b/Sources/FioriSwiftUICore/Utils/FioriPopoverSize.swift new file mode 100644 index 000000000..403422995 --- /dev/null +++ b/Sources/FioriSwiftUICore/Utils/FioriPopoverSize.swift @@ -0,0 +1,117 @@ +import SwiftUI + +struct PopoverSizeModifier: ViewModifier { + @Binding var isPresented: Bool + var arrowEdge: Edge = .top + var popoverSize: CGSize? = nil + let popoverContent: () -> PopoverContent + + func body(content: Content) -> some View { + content + .background( + Wrapper(isPresented: self.$isPresented, popoverSize: self.popoverSize, popoverContent: self.popoverContent, arrowEdge: self.arrowEdge) + .frame(maxWidth: .infinity, maxHeight: .infinity) + ) + } + + struct Wrapper: UIViewControllerRepresentable { + @Binding var isPresented: Bool + let popoverSize: CGSize? + var popoverContent: () -> PopoverView + var arrowEdge: Edge = .top + + func makeUIViewController(context: UIViewControllerRepresentableContext>) -> WrapperViewController { + WrapperViewController( + popoverSize: self.popoverSize, + arrowEdge: self.arrowEdge, + popoverContent: self.popoverContent + ) { + self.isPresented = false + } + } + + func updateUIViewController(_ uiViewController: WrapperViewController, context: UIViewControllerRepresentableContext>) { + uiViewController.updateSize(self.popoverSize) + + if self.isPresented { + uiViewController.arrowEdge = self.arrowEdge + uiViewController.showPopover() + } else { + uiViewController.hidePopover() + } + if let hostingController = uiViewController.popoverVC as? UIHostingController { + hostingController.rootView = self.popoverContent() + } + } + } + + class WrapperViewController: UIViewController, UIPopoverPresentationControllerDelegate { + var popoverSize: CGSize? + var popoverContent: () -> PopoverView + let onDismiss: () -> Void + var arrowEdge: Edge = .top + + var popoverVC: UIViewController? + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("") } + + init(popoverSize: CGSize?, arrowEdge: Edge = .top, popoverContent: @escaping () -> PopoverView, onDismiss: @escaping () -> Void) { + self.popoverSize = popoverSize + self.popoverContent = popoverContent + self.onDismiss = onDismiss + self.arrowEdge = arrowEdge + super.init(nibName: nil, bundle: nil) + } + + func showPopover() { + guard self.popoverVC == nil else { return } + let vc = UIHostingController(rootView: popoverContent()) + if let size = popoverSize { vc.preferredContentSize = size } + vc.modalPresentationStyle = .popover + + if let popover = vc.popoverPresentationController { + popover.sourceView = view + switch self.arrowEdge { + case .top: + popover.permittedArrowDirections = .up + case .bottom: + popover.permittedArrowDirections = .down + case .leading: + popover.permittedArrowDirections = .left + case .trailing: + popover.permittedArrowDirections = .right + } + popover.delegate = self + } + + self.popoverVC = vc + self.present(vc, animated: true, completion: nil) + } + + func hidePopover() { + guard let vc = popoverVC, !vc.isBeingDismissed else { return } + vc.dismiss(animated: true, completion: nil) + self.popoverVC = nil + } + + func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { + self.popoverVC = nil + self.onDismiss() + } + + func updateSize(_ size: CGSize?) { + self.popoverSize = size + if let vc = popoverVC, let size { + vc.preferredContentSize = size + } + } + + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + if UIDevice.current.userInterfaceIdiom == .phone { + return .automatic + } + return .none + } + } +} diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index 4e4575192..0df575bcb 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -325,6 +325,14 @@ struct PickerMenuItem: View { @ViewBuilder var list: some View { + if UIDevice.current.userInterfaceIdiom == .phone { + self.phoneView() + } else { + self.padView() + } + } + + private func phoneView() -> 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() @@ -367,6 +375,7 @@ struct PickerMenuItem: View { } updateSearchListPickerHeight: { height in self.detentHeight = max(height, 88) } + .animation(.spring) .frame(maxHeight: UIDevice.current.userInterfaceIdiom != .phone ? self.detentHeight : nil) .padding(0) .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidShowNotification)) { notif in @@ -396,6 +405,74 @@ struct PickerMenuItem: View { }) } + private func padView() -> 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() + } + .modifier(PopoverSizeModifier(isPresented: self.$isSheetVisible, arrowEdge: self.barItemFrame.arrowDirection(), popoverSize: CGSize(width: self.popoverWidth, height: self.calculateDetentHeight()), popoverContent: { + 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: { + if self.item.resetButtonConfiguration.isHidden { + EmptyView() + } else { + _Action(actionText: self.item.resetButtonConfiguration.title, didSelectAction: { + if self.item.resetButtonConfiguration.type == .reset { + self.item.reset() + } else { + self.item.clearAll() + } + }) + .buttonStyle(ResetButtonStyle()) + .disabled(self.resetButtonDisable()) + } + } 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, isSearchBarHidden: self.item.isSearchBarHidden, disableListEntriesSection: self.item.disableListEntriesSection, allowsDisplaySelectionCount: self.item.allowsDisplaySelectionCount, barItemFrame: self.barItemFrame) { index in + self.item.onTap(option: self.item.valueOptions[index]) + } selectAll: { isAll in + self.item.selectAll(isAll) + } updateSearchListPickerHeight: { height in + self.detentHeight = max(height, 88) + } + .padding(0) + .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidShowNotification)) { notif in + let rect = (notif.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero + self._keyboardHeight = rect.height + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidHideNotification)) { _ in + self._keyboardHeight = 0 + } + } + })) + .ifApply(UIDevice.current.userInterfaceIdiom != .phone, content: { v in + v.background(GeometryReader { geometry in + Color.clear + .onAppear { + self.barItemFrame = geometry.frame(in: .global) + } + .setOnChange(of: geometry.frame(in: .global), action1: { newValue in + self.barItemFrame = newValue + }) { _, newValue in + self.barItemFrame = newValue + } + }) + }) + } + private func calculateDetentHeight() -> CGFloat { let isNotIphone = UIDevice.current.userInterfaceIdiom != .phone var height = self.detentHeight From d35d6a98d1333c40b819da6faf20a5e47293584d Mon Sep 17 00:00:00 2001 From: "Gu, Jiajun (external - Project)" Date: Tue, 10 Dec 2024 16:35:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=F0=9F=90=9B=20[IOSSDKBUG-482]iPad?= =?UTF-8?q?=20popover=20FilterFeedback=20list=20jumping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/SortFilter/FilterFeedbackBarItem+View.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index 0df575bcb..aa234dc75 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -375,7 +375,7 @@ struct PickerMenuItem: View { } updateSearchListPickerHeight: { height in self.detentHeight = max(height, 88) } - .animation(.spring) + .animation(.easeInOut) .frame(maxHeight: UIDevice.current.userInterfaceIdiom != .phone ? self.detentHeight : nil) .padding(0) .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidShowNotification)) { notif in @@ -407,6 +407,7 @@ struct PickerMenuItem: View { private func padView() -> some View { FilterFeedbackBarItem(leftIcon: icon(name: self.item.icon, isVisible: true), title: self.item.label, rightIcon: Image(systemName: "chevron.down"), isSelected: self.item.isChecked) + .contentShape(Rectangle()) .onTapGesture { self.isSheetVisible.toggle() }