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.