In this last post I‘ve proposed an OutlinePicker
for SwiftUI. But, to be honest, I am not quite satisfied with this solution. The selection view appears as a sheet in contrast to the standard picker and its layout deserves some more fine tuning.
The problem with the standard popover modifier in SwiftUI is that it’s mutated to a sheet presentation on iPhones. I know, there is this new modifier presentationCompactAdaptation(horizontal:vertical:)
but that’s only available since iOS16.4. In addition, I want to use a List
in the popover and that’s collapsed to a tiny square if you do not specify a frame size. To make a long explanation short: I prefer to use the nice utility view „WithPopover
“ for the presentation of my picker selection.
You will find this nice utility here but it has two problems. First of all it’s not updating properly when the popoverContent
is changed while being used by WithPopover
. This is fixed by some careful reader. Thank you for that.
The other problem is a quite penetrant warning by the OS that the presentation of a view from a detached view controller is discouraged. Well, it’s not obvious that the view controller of WithPopover
is detached from the view hierarchy. Anyway, the situation is getting worse since at least the beta version of XCode 15 / iOS 17 is stating that this will be a real exception in future releases. Not so nice.
So, if the view controller of WithPopover
is detached from the view hierarchy maybe we can find a view controller that is not. Well, there are these nice two extension which give the topmost view controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
extension UIWindow { static var key: UIWindow? { // replacement of keyWindow if #available(iOS 13, *) { // from: https://stackoverflow.com/questions/57134259/how-to-resolve-keywindow-was-deprecated-in-ios-13-0 return UIApplication .shared .connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } .last { $0.isKeyWindow } } else { return UIApplication.shared.keyWindow } } } extension UIViewController { // from https://stackoverflow.com/questions/26667009/get-top-most-uiviewcontroller static var topMostViewController: UIViewController? { var topMostViewController = UIWindow.key?.rootViewController // the starting vc while let presentedViewController = topMostViewController?.presentedViewController { topMostViewController = presentedViewController // loop over all presented vcs } return topMostViewController // and return the last/topmost one } } |
The first one is a replacement of the old keyWindow
property and the second one returns the topmost view controller for that. So, if we present the popover from that view controller the warning is gone. Here is my modified version of WithPopover
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// from: https://gist.github.com/wassupdoc/7c47efa3fe480fa9a70a95ac89448bde struct WithPopover<Content: View, PopoverContent: View>: View { @Binding var showPopover: Bool var popoverSize: CGSize? = nil let content: () -> Content let popoverContent: () -> PopoverContent var body: some View { content() .background( Wrapper(showPopover: $showPopover, popoverSize: popoverSize, popoverContent: popoverContent) .frame(maxWidth: .infinity, maxHeight: .infinity) ) } struct Wrapper<MyPopoverContent: View> : UIViewControllerRepresentable { @Binding var showPopover: Bool let popoverSize: CGSize? let popoverContent: () -> MyPopoverContent func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper<MyPopoverContent>>) -> WrapperViewController<MyPopoverContent> { return WrapperViewController( popoverSize: popoverSize, popoverContent: popoverContent) { self.showPopover = false } } func updateUIViewController(_ uiViewController: WrapperViewController<MyPopoverContent>, context: UIViewControllerRepresentableContext<Wrapper<MyPopoverContent>>) { uiViewController.updateSize(popoverSize) if showPopover { uiViewController.showPopover() } else { uiViewController.hidePopover() } if let hostingController = uiViewController.popoverVC as? UIHostingController<MyPopoverContent> { hostingController.rootView = popoverContent() } } } class WrapperViewController<MyPopoverContent: View>: UIViewController, UIPopoverPresentationControllerDelegate { var popoverSize: CGSize? let popoverContent: () -> MyPopoverContent let onDismiss: () -> Void var popoverVC: UIViewController? required init?(coder: NSCoder) { fatalError("") } init(popoverSize: CGSize?, popoverContent: @escaping () -> MyPopoverContent, onDismiss: @escaping() -> Void) { self.popoverSize = popoverSize self.popoverContent = popoverContent self.onDismiss = onDismiss super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() } func showPopover() { guard popoverVC == nil, let topMostViewController = Self.topMostViewController else { return } let vc = UIHostingController(rootView: popoverContent()) if let size = popoverSize { vc.preferredContentSize = size } vc.modalPresentationStyle = UIModalPresentationStyle.popover if let popover = vc.popoverPresentationController { popover.sourceView = view popover.delegate = self } popoverVC = vc topMostViewController.present(vc, animated: true, completion: nil) // present from topmost VC } func hidePopover() { guard let vc = popoverVC, !vc.isBeingDismissed else { return } vc.dismiss(animated: true, completion: nil) popoverVC = nil } func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { popoverVC = nil self.onDismiss() } func updateSize(_ size: CGSize?) { self.popoverSize = size if let vc = popoverVC, let size = size { vc.preferredContentSize = size } } func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none // this is what forces popovers on iPhone } } } |
Lines 42-43 reflect the modifications from cloxnu and lines 71-72, 81 are my additions to present the popover from the topmost view controller.
In my next post I will present a modified version of NodeOutlineGroup
.