Issue #809
Add a hidden overlay UIContextMenuInteraction
. Provide preview in previewProvider
and actions in actionProvider
. Use @ViewBuilder
to make declaring preview easy.
extension View {
func contextMenuWithPreview<Content: View>(
actions: [UIAction],
@ViewBuilder preview: @escaping () -> Content
) -> some View {
self.overlay(
InteractionView(
preview: preview,
menu: UIMenu(title: "", children: actions),
didTapPreview: {}
)
)
}
}
private struct InteractionView<Content: View>: UIViewRepresentable {
@ViewBuilder let preview: () -> Content
let menu: UIMenu
let didTapPreview: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
view.addInteraction(menuInteraction)
return view
}
func updateUIView(_ uiView: UIView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(
preview: preview(),
menu: menu,
didTapPreview: didTapPreview
)
}
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let preview: Content
let menu: UIMenu
let didTapPreview: () -> Void
init(preview: Content, menu: UIMenu, didTapPreview: @escaping () -> Void) {
self.preview = preview
self.menu = menu
self.didTapPreview = didTapPreview
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(
identifier: nil,
previewProvider: { [weak self] () -> UIViewController? in
guard let self = self else { return nil }
return UIHostingController(rootView: self.preview)
},
actionProvider: { [weak self] _ in
guard let self = self else { return nil }
return self.menu
}
)
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionCommitAnimating
) {
animator.addCompletion(self.didTapPreview)
}
}
}
How to use it
struct ContentView: View {
let deleteAction = UIAction(
title: "Delete",
image: UIImage(systemName: "delete.left"),
identifier: nil,
attributes: UIMenuElement.Attributes.destructive,
handler: { _ in print("Deleted") }
)
var body: some View {
NavigationView {
Text("Hello, World!")
.contextMenuWithPreview(actions: [deleteAction]) {
Text("Preview")
.border(Color.red)
}
}
}
}
preferredContentSize
You can use GeometryReader in background to get SwiftUI View size, or get intrinsicContentSize
in UIHostingController
private final class PreviewHostingController<Content: View>: UIHostingController<Content> {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let window = view.window {
preferredContentSize.width = window.frame.size.width - 32
}
let targetSize = view.intrinsicContentSize
preferredContentSize.height = targetSize.height
}
}
TargetedPreview
Since we add a dummy UIView as an overlay for our Preview, we should support targeted preview. First by taking snapshot of our SwiftUI View
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
Then construct UITargetedPreview
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
targedPreview
}
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
targedPreview
}
private var targedPreview: UITargetedPreview? {
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
return UITargetedPreview(view: UIImageView(image: snapshot), parameters: parameters)
}
Clear background color
Setting backgroundColor
to .clear
or any transparent color seems not allowed as UIContextMenu always shows a default white color.
view.backgroundColor = .clear