Janik Spies
Janik Spies

Reputation: 427

Correctly set the PopOverViewController in SwiftUI

In my iOS App I've got the problem that the App crashes on iPad when I want to show a UIActivityViewController. I've found out that this happens because of the PopoverPresentationController and that I should set it to my view or button.

My code looks like this:

@State var alert = ActionSheet(title: Text("Error"))

var body: some View {
    VStack() {
        Button(action: {
        self.showSheet.toggle()
        }) {
        Image(systemName: "ellipsis")
        }
        .actionSheet(isPresented: $showSheet) {
            self.alert
        }
    }.onAppear(){
        alert = ActionSheet(
            title: Text("Auftrag"),
            buttons: [
                .cancel(Text("Close")),
                .default(Text("Share")) { openActionSheet() },
        )
    }
}

func openActionSheet() {
    let contextString = "TEST"

    let print = UIMarkupTextPrintFormatter(markupText: contextString)
            
    let render = UIPrintPageRenderer()
    render.addPrintFormatter(print, startingAtPageAt: 0)

    let page = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4, 72 dpi
    render.setValue(page, forKey: "paperRect")
    render.setValue(page, forKey: "printableRect")

    let pdfData = NSMutableData()
    UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)

    for i in 0..<render.numberOfPages {
    UIGraphicsBeginPDFPage();
        render.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
    }
    UIGraphicsEndPDFContext();

    let av = UIActivityViewController(activityItems: [pdfData], applicationActivities: nil)

    av.popoverPresentationController?.sourceView = view
    
    UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}

The line av.popoverPresentationController?.sourceView = view is what prevents the crash in all the other questions I've found.
In my case I get the error Cannot find 'view' in scope.

Can someone please explain me why?

Upvotes: 3

Views: 1500

Answers (2)

Manabu Nakazawa
Manabu Nakazawa

Reputation: 2345

Your own answer has a problem where you need to calculate from the screen size and key window, which can vary depending on the environment.

Instead, I suggest placing a hidden UIView onto the button for sourceView using this:


struct SourceViewModifier: ViewModifier {
    var sourceView: UIView
    
    init(_ sourceView: UIView) {
        self.sourceView = sourceView
    }
    
    func body(content: Content) -> some View {
        content
            .background {
                GeometryReader {
                    sourceView.frame = $0.frame(in: CoordinateSpace.global)
                    sourceView.bounds = $0.frame(in: CoordinateSpace.local)
                    return HiddenView(view: sourceView)
                }
            }
    }
    
    private struct HiddenView: UIViewRepresentable {
        var view: UIView
        
        func makeUIView(context: Context) -> some UIView {
            view
        }
        
        func updateUIView(_ uiView: UIViewType, context: Context) {
            view.isHidden = true
        }
    }
}

extension View {
    func sourceView(_ sourceView: UIView) -> some View {
        self.modifier(SourceViewModifier(sourceView))
    }
}

Usage:

@State private var buttonSourceView = UIView()

var body: some View {
    VStack {
        Button { ... } label: { ... }
            ...
            .sourceView(buttonSourceView) // ← Add this
    }.onAppear {
        alert = ActionSheet(
            ...
            buttons: [
                ...,
                .default(Text("Share")) {
                    openActionSheet(sender: buttonSourceView)
                },
            ]
        )
    }
}

private func openActionSheet(sender: UIView) {
    ...
    let av = UIActivityViewController(...)
    av.popoverPresentationController?.sourceView = sender
    av.popoverPresentationController?.sourceRect = sender.bounds
    ...
}

Upvotes: 0

Janik Spies
Janik Spies

Reputation: 427

You can specify the av.popoverPresentationController.sourceView like this:

    if UIDevice.current.userInterfaceIdiom == .pad {
        av.popoverPresentationController?.sourceView = UIApplication.shared.windows.first
        av.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2.1, y: UIScreen.main.bounds.height / 2.3, width: 200, height: 200)
    }

Upvotes: 2

Related Questions