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?
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)
}
}
}
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With