1

I'm trying to set a certain size for a popover or to make it adapt its content

I tried to change the frame for the view from popover, but it does not seem to work

Button("Popover") {
        self.popover7.toggle()
}.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
         PopoverView().frame(width: 100, height: 100, alignment: .center)
}

I'd like to achieve this behaviour I found in Calendar app in iPad

enter image description here

Sorin Lica
  • 5,430
  • 7
  • 26
  • 60

3 Answers3

6

I got it to work on iOS with a custom UIViewRepresentable. Here is what the usage looks like:

struct Content: View {
    @State var open = false
    @State var popoverSize = CGSize(width: 300, height: 300)

    var body: some View {
        WithPopover(
            showPopover: $open,
            popoverSize: popoverSize,
            content: {
                Button(action: { self.open.toggle() }) {
                    Text("Tap me")
                }
        },
            popoverContent: {
                VStack {
                    Button(action: { self.popoverSize = CGSize(width: 300, height: 600)}) {
                        Text("Increase size")
                    }
                    Button(action: { self.open = false}) {
                        Text("Close")
                    }
                }
        })
    }
}

And here is a gist with the source for WithPopover

ccwasden
  • 7,848
  • 5
  • 39
  • 45
  • Doesn't work for me unfortunately... and it fails in preview: `reference to generic type 'WithPopover' requires arguments in <...>`. Also I get this: `Presenting view controllers on detached view controllers is discouraged` – krjw Mar 17 '20 at 14:20
  • Works fine for me in simulator, without failure. Took the whole content from the Gist and used it according to the snippet above. Preview does not work properly, just shows an empty screen when tapping the button. But no errors shown. Xcode Version 11.4 beta 3 (11N132i). – Hardy Mar 22 '20 at 09:31
  • @ccwasden forgot to include the adapativepresentationstyle func in the gist func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none // this is what forces popovers on iPhone } – Prasanth Jun 02 '20 at 11:31
  • This doesn't work. iOS15. simulator iPhone 12. Always takes a full screen – lopes710 Mar 14 '22 at 14:09
6

The solution by @ccwasden works very well. I extended his work by making it more "natural" in terms of SwiftUI. Also, this version utilizes sizeThatFits method, so you don't have to specify the size of the popover content.

struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    let content: () -> PopoverContent

    func body(content: Content) -> some View {
        content
            .background(
                Popover(
                    isPresented: self.$isPresented,
                    onDismiss: self.onDismiss,
                    content: self.content
                )
            )
    }
}

extension View {
    func popover<Content>(
        isPresented: Binding<Bool>,
        onDismiss: (() -> Void)? = nil,
        content: @escaping () -> Content
    ) -> some View where Content: View {
        ModifiedContent(
            content: self,
            modifier: PopoverViewModifier(
                isPresented: isPresented,
                onDismiss: onDismiss,
                content: content
            )
        )
    }
}

struct Popover<Content: View> : UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    @ViewBuilder let content: () -> Content

    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self, content: self.content())
    }

    func makeUIViewController(context: Context) -> UIViewController {
        return UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        context.coordinator.host.rootView = self.content()
        if self.isPresented, uiViewController.presentedViewController == nil {
            let host = context.coordinator.host
            host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max, height: Int.max))
            host.modalPresentationStyle = UIModalPresentationStyle.popover
            host.popoverPresentationController?.delegate = context.coordinator
            host.popoverPresentationController?.sourceView = uiViewController.view
            host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
            uiViewController.present(host, animated: true, completion: nil)
        }
    }

    class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
        let host: UIHostingController<Content>
        private let parent: Popover

        init(parent: Popover, content: Content) {
            self.parent = parent
            self.host = UIHostingController(rootView: content)
        }

        func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
            self.parent.isPresented = false
            if let onDismiss = self.parent.onDismiss {
                onDismiss()
            }
        }

        func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
            return .none
        }
    }
}
Kou Ariga
  • 469
  • 4
  • 8
  • 1
    Excellent improvement. Was about to do the same myself but then I saw your reply further down :) – Jensie Oct 27 '21 at 19:13
  • 1
    Thank you, @Jensie! I made a minor change to the updateUIViewController method. The previous version had an internal state inconsistency issue when multiple popovers are used in a single view. I hope it helps. – Kou Ariga Oct 28 '21 at 04:08
0

macOS-only

Here is how to change frame of popover dynamically... for simplicity it is w/o animation, it is up to you.

struct TestCustomSizePopover: View {
    @State var popover7 = false
    var body: some View {
        VStack {
            Button("Popover") {
                    self.popover7.toggle()
            }.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
                     PopoverView()
            }
        }.frame(width: 800, height: 600)
    }
}

struct PopoverView: View {
    @State var adaptableHeight = CGFloat(100)
    var body: some View {
        VStack {
                Text("Popover").padding()
                Button(action: {
                    self.adaptableHeight = 300
                }) {
                    Text("Button")
                }
            }
            .frame(width: 100, height: adaptableHeight)
    }
}
Asperi
  • 173,274
  • 14
  • 284
  • 455
  • 1
    Did you try the code? It's not working, the popover size does not change – Sorin Lica Nov 13 '19 at 13:52
  • Sure it is copy/paste from the test project. Xcode 11.2.1. Ahh.. pardon, I did it for MacOS target. I'll review for iOS a bit later. – Asperi Nov 13 '19 at 14:01
  • Yeah, setting the frame for a popover in MacOS works well, but on iOS the `frame` modifier has no effect on popovers – Sorin Lica Nov 13 '19 at 14:24
  • By my finding for now content tracks .frame correctly and has changed, but system popover window does not track content changes (on iOS). – Asperi Nov 13 '19 at 18:00
  • Does not seem like it’s currently possible to change the frame of a popover. I created bug report FB7465491, I suggest doing the same. – abellao Nov 25 '19 at 00:22