3

I want to implement same custom popup view when press long press gesture on a view as shown in the photo (from Tweeter App), so I can show a custom view and context menu at same time.

enter image description here

Witek Bobrowski
  • 3,749
  • 1
  • 20
  • 34
Ammar Ahmad
  • 534
  • 5
  • 24

2 Answers2

3

You need to make a custom ContextMenu using UIContextMenu from UIKit.

struct ContextMenuHelper<Content: View, Preview: View>: UIViewRepresentable {
    var content: Content
    var preview: Preview
    var menu: UIMenu
    var navigate: () -> Void
    init(content: Content, preview: Preview, menu: UIMenu, navigate: @escaping () -> Void) {
        self.content = content
        self.preview = preview
        self.menu = menu
        self.navigate = navigate
    }
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        view.backgroundColor = .clear
        let hostView = UIHostingController(rootView: content)
        hostView.view.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
            hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor)
        ]
        view.addSubview(hostView.view)
        view.addConstraints(constraints)
        let interaction = UIContextMenuInteraction(delegate: context.coordinator)
        view.addInteraction(interaction)
        return view
    }
    func updateUIView(_ uiView: UIView, context: Context) {
    }
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    class Coordinator: NSObject, UIContextMenuInteractionDelegate {
        var parent: ContextMenuHelper
        init(_ parent: ContextMenuHelper) {
            self.parent = parent
        }
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
            return UIContextMenuConfiguration(identifier: nil) {
                let previewController = UIHostingController(rootView: self.parent.preview)
                return previewController
            } actionProvider: { items in
                return self.parent.menu
            }
        }
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
            parent.navigate()
        }
    }
}

extension View {
    func contextMenu<Preview: View>(navigate: @escaping () -> Void = {}, @ViewBuilder preview: @escaping () -> Preview, menu: @escaping () -> UIMenu) -> some View {
        return CustomContextMenu(navigate: navigate, content: {self}, preview: preview, menu: menu)
    }
}

struct CustomContextMenu<Content: View, Preview: View>: View {
    var content: Content
    var preview: Preview
    var menu: UIMenu
    var navigate: () -> Void
    init(navigate: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content, @ViewBuilder preview: @escaping () -> Preview, menu: @escaping () -> UIMenu) {
        self.content = content()
        self.preview = preview()
        self.menu = menu()
        self.navigate = navigate
    }
    var body: some View {
        ZStack {
            content
                .overlay(ContextMenuHelper(content: content, preview: preview, menu: menu, navigate: navigate))
        }
    }
}

Usage:

.contextMenu(navigate: {
    UIApplication.shared.open(url) //User tapped the preview
}) {
    LinkView(link: url.absoluteString) //Preview
        .environment(\.managedObjectContext, viewContext)
        .accentColor(Color(hex: "59AF97"))
        .environmentObject(variables)
}menu: {
    let openUrl = UIAction(title: "Open", image: UIImage(systemName: "sidebar.left")) { _ in
        withAnimation() {
            UIApplication.shared.open(url)
        }
    }
    let menu = UIMenu(title: url.absoluteString, image: nil, identifier: nil, options: .displayInline, children: [openUrl]) //Menu
    return menu
}

For navigation:

add isActive: $navigate to your NavigationLink:

NavigationLink(destination: SomeView(), isActive: $navigate)

along with a new property:

@State var navigate = false

.contextMenu(navigate: {
    navigate.toggle() //User tapped the preview
}) {
    LinkView(link: url.absoluteString) //Preview
        .environment(\.managedObjectContext, viewContext)
        .accentColor(Color(hex: "59AF97"))
        .environmentObject(variables)
}menu: {
    let openUrl = UIAction(title: "Open", image: UIImage(systemName: "sidebar.left")) { _ in
        withAnimation() {
            UIApplication.shared.open(url)
        }
    }
    let menu = UIMenu(title: url.absoluteString, image: nil, identifier: nil, options: .displayInline, children: [openUrl]) //Menu
    return menu
}
Timmy
  • 4,098
  • 2
  • 14
  • 34
  • The custom Context Menu is perfect but I don't want to open url instead I want to show another custom view (just a simple view). – Ammar Ahmad Jan 21 '22 at 17:20
  • 2
    Yes I know you can replace it with whatever you want this is an example, check my updated answer! – Timmy Jan 21 '22 at 17:26
3

There is a new method in iOS 16 SDK (currently in beta) that allows for showing a preview directly from SwiftUI without the need of tapping into the UIKit.

contextMenu(menuItems:preview:)

Witek Bobrowski
  • 3,749
  • 1
  • 20
  • 34
  • This is an improvement, but as far as I can see, this unfortunately does not yet support a tap gesture or similar on the preview as would be possible with UIKit. – nylki Mar 28 '23 at 10:21