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

fix: 🐛 [IOSSDKBUG-482]iPad popover FilterFeedback list jumping #931

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions Sources/FioriSwiftUICore/Utils/FioriPopoverSize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import SwiftUI

struct PopoverSizeModifier<PopoverContent: View>: 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<PopoverView: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let popoverSize: CGSize?
var popoverContent: () -> PopoverView
var arrowEdge: Edge = .top

func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper<PopoverView>>) -> WrapperViewController<PopoverView> {
WrapperViewController(
popoverSize: self.popoverSize,
arrowEdge: self.arrowEdge,
popoverContent: self.popoverContent
) {
self.isPresented = false
}
}

func updateUIViewController(_ uiViewController: WrapperViewController<PopoverView>, context: UIViewControllerRepresentableContext<Wrapper<PopoverView>>) {
uiViewController.updateSize(self.popoverSize)

if self.isPresented {
uiViewController.arrowEdge = self.arrowEdge
uiViewController.showPopover()
} else {
uiViewController.hidePopover()
}
if let hostingController = uiViewController.popoverVC as? UIHostingController<PopoverView> {
hostingController.rootView = self.popoverContent()
}
}
}

class WrapperViewController<PopoverView: View>: 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,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()
Expand Down Expand Up @@ -375,6 +383,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
Expand Down Expand Up @@ -404,6 +413,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
Expand Down
Loading