I'm using the following pattern:

  • UIViewController
  • that has UIHostingController as immediate child
  • and of course the UIHostingController view added as subview of the main VC and anchoring to the edges
lazy var hostingVC = UIHostingController(rootView: EDWrapperView(exportDestinationVC: self))

override func viewDidLoad() {
    super.viewDidLoad()
    self.addChild(hostingVC)
    self.view.addSubview(hostingVC.view)
    
    hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
    
    hostingVC.view.backgroundColor = .systemGroupedBackground
    
    self.view.topAnchor.constraint(equalTo: hostingVC.view.topAnchor).isActive = true
    self.view.bottomAnchor.constraint(equalTo: hostingVC.view.bottomAnchor).isActive = true
    self.view.leadingAnchor.constraint(equalTo: hostingVC.view.leadingAnchor).isActive = true
    self.view.trailingAnchor.constraint(equalTo: hostingVC.view.trailingAnchor).isActive = true
    
    configureNavigation()
    
    // Theme
    ThemeManager.shared.registerForThemeChanges(observer: self)
}

You can see that the SwiftUIView has a reference to the UIKitVC for calling functions (it kinda acts as a ViewModel).

In that very same UIViewController, I have a function that presents a popover, which requires a sourceView and a sourceRect.

func showActivityVC(sourceRect: CGRect) {
    let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    if let popOver = activityVC.popoverPresentationController {
        popOver.sourceView = self.view! // Pass the UIVC view as sourceView
        popOver.sourceRect = sourceRect // Source rect has been computed by GeometryReader in SwiftUI. But its not exactly at the right position
    }
}

In the SwiftUIView, the button looks like this:

List {
    // Export button to iOS Share sset
    Section(header: Text("Export")) {
        GeometryReader { geometry in
            Button(action: {
                let insideFrame = geometry.frame(in: .global) // Get rect in the global view, which should be the same as the UIViewController view
                self.exportDestinationVC.presentIOSShareSheet(sourceRect: insideFrame)
            }, label: {
                HStack(alignment: .firstTextBaseline) {
                    Spacer()
                    Image(systemName: "square.and.arrow.up")
                    Text("Export")
                    Spacer()
                }
            })
            .contentShape(Rectangle())
            .position(x: geometry.size.width/2.0, y: geometry.size.height/2.0)
        }
    }
}

I use a GeometryReader to get the frame of the button in the global view, which should be the same as the UIViewController view. It works, but not exactly, the popover does not point exactly where it should, there are some inaccuracies that comes from I don't know where.

How can I get the exact sourceRect of the SwiftUI button inside the SwiftUI view?

2

There are 2 best solutions below

0
On BEST ANSWER

Here is a simpler working solution, without needing a Coordinator. Meanwhile, it allows setting the title, font, and changing the color dynamically.

struct ButtonWithSourceView: UIViewRepresentable {
    var title: String
    var font: UIFont
    @Binding var color: UIColor
    var action: (UIButton) -> Void

    func makeUIView(context: Self.Context) -> UIButton {
        let uiButton = UIButton()
    
        uiButton.setTitle(title, for: .normal)
        uiButton.titleLabel?.font = font
        uiButton.setTitleColor(color, for: .normal)
        let uiAction = UIAction() { _ in
            action(uiButton)
        }
        uiButton.addAction(uiAction, for: .touchUpInside)
    
        return uiButton
    }

    func updateUIView(_ uiView: UIButton, context: Self.Context) {
        uiView.setTitleColor(color, for: .normal)
    }
}
3
On

I too had the same issue, geometry reader wasn't providing the correct y-value of the button and popover was flying away during an orientation change.

So, I did the following: I created a UIViewRepresentable(say MyButton) instance for UIbutton, made a custom coordinator which held the UIbutton.

MyButton had a callback((UIButton) -> Void), when MyButton was tapped, I returned the UIbutton from the coordinator in that callback and set it as the sourceView for my popover, then presented it and it worked like a charm.

struct MyButton: UIViewRepresentable {
var action: (UIButton) -> Void

func makeUIView(context: Self.Context) -> UIButton {
    let uiButton = UIButton()
    context.coordinator.uiButton = uiButton
    context.coordinator.addTarget()
    
    return uiButton
}

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

func updateUIView(_ uiView: UIButton, context: Self.Context) {}

class Coordinator: NSObject {
    var parent: MyButton
    var uiButton = UIButton()

    init(_ uiView: MyButton) {
        self.parent = uiView
    }
    
    func addTarget() {
        uiButton.addTarget(self, action: #selector(tapped), for: .touchUpInside)
    }
    
    @objc func tapped() {
        self.parent.action(uiButton)
    }
}
}