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

2 Answers 2

4

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
}
Sign up to request clarification or add additional context in comments.

3 Comments

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).
Yes I know you can replace it with whatever you want this is an example, check my updated answer!
Need to also add hostView.view.backgroundColor = .clear when initialising ContextMenuHelper. Otherwise, it adds a background and can mess up with, say, listRowBackground if used inside the list. Otherwise – great solution, thank you!
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:)

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.