swiftuiuikituihostingcontroller

Get sourceRect of a SwiftUI view for popover presentation in UIKit ViewController (via UIHostingController)


I'm using the following pattern:

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?


Solution

  • 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)
        }
    }