Skip to content

Instantly share code, notes, and snippets.

@SpectralDragon
Created April 8, 2020 19:22
Show Gist options
  • Save SpectralDragon/e1c01388db09752eac790ae23f1d4587 to your computer and use it in GitHub Desktop.
Save SpectralDragon/e1c01388db09752eac790ae23f1d4587 to your computer and use it in GitHub Desktop.
Simple way to implement preview context menu for SwiftUI
//
// ContentView.swift
// PreviewSwiftUI
//
// Created by v.prusakov on 4/8/20.
// Copyright © 2020 v.prusakov. All rights reserved.
//
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.contextMenu(PreviewContextMenu(destination: Text("Destination"), actionProvider: { items in
return UIMenu(title: "My Menu", children: items)
}))
}
}
}
// MARK: - Custom Menu Context Implementation
struct PreviewContextMenu<Content: View> {
let destination: Content
let actionProvider: UIContextMenuActionProvider?
init(destination: Content, actionProvider: UIContextMenuActionProvider? = nil) {
self.destination = destination
self.actionProvider = actionProvider
}
}
// UIView wrapper with UIContextMenuInteraction
struct PreviewContextView<Content: View>: UIViewRepresentable {
let menu: PreviewContextMenu<Content>
let didCommitView: () -> 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 {
return Coordinator(menu: self.menu, didCommitView: self.didCommitView)
}
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let menu: PreviewContextMenu<Content>
let didCommitView: () -> Void
init(menu: PreviewContextMenu<Content>, didCommitView: @escaping () -> Void) {
self.menu = menu
self.didCommitView = didCommitView
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
UIHostingController(rootView: self.menu.destination)
}, actionProvider: self.menu.actionProvider)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
animator.addCompletion(self.didCommitView)
}
}
}
// Add context menu modifier
extension View {
func contextMenu<Content: View>(_ menu: PreviewContextMenu<Content>) -> some View {
self.modifier(PreviewContextViewModifier(menu: menu))
}
}
struct PreviewContextViewModifier<V: View>: ViewModifier {
let menu: PreviewContextMenu<V>
@Environment(\.presentationMode) var mode
@State var isActive: Bool = false
func body(content: Content) -> some View {
Group {
if isActive {
menu.destination
} else {
content.overlay(PreviewContextView(menu: menu, didCommitView: { self.isActive = true }))
}
}
}
}
@acal11
Copy link

acal11 commented Dec 21, 2021

@jalvini did you ever manage to resolve this?

@robinst
Copy link

robinst commented Apr 4, 2022

@jalvini @acal11 It looks like the white overlay can be fixed by adding these to Coordinator:

func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
    let parameters = UIPreviewParameters()
    parameters.backgroundColor = .clear
    return UITargetedPreview(view: interaction.view!, parameters: parameters)
}

func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
    let parameters = UIPreviewParameters()
    parameters.backgroundColor = .clear
    return UITargetedPreview(view: interaction.view!, parameters: parameters)
}

@karenxpn
Copy link

what if we have a button in the preview. how action should be triggered? cause now buttons are not working

@SpectralDragon
Copy link
Author

what if we have a button in the preview. how action should be triggered? cause now buttons are not working

I did it only for previewing :) You could not interact with it at the moment, but if you know how to improve this solution, you're welcome!

@trisapple
Copy link

trisapple commented May 15, 2022

The animation when long pressing does not appear but it is a good attempt and does not screw up my layout like other methods! Would appreciate an update to the code where the animation can appear just like Apple's implementation!

@artit7m
Copy link

artit7m commented Aug 24, 2024

This one works good also adding buttons underneath the destination view:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!")
                .contextMenu(PreviewContextMenu(destination: Text("Destination").padding(),
                                                 editAction: {
                                                    print("Edit action")
                                                 },
                                                 deleteAction: {
                                                    print("Delete action")
                                                 }))
        }
    }
}

// MARK: - Custom Menu Context Implementation

struct PreviewContextMenu<Content: View> {
    let destination: Content
    let editAction: () -> Void
    let deleteAction: () -> Void
    let actionProvider: UIContextMenuActionProvider?
    
    init(destination: Content, editAction: @escaping () -> Void, deleteAction: @escaping () -> Void, actionProvider: UIContextMenuActionProvider? = nil) {
        self.destination = destination
        self.editAction = editAction
        self.deleteAction = deleteAction
        self.actionProvider = actionProvider
    }
}

// UIView wrapper with UIContextMenuInteraction

struct PreviewContextView<Content: View>: UIViewRepresentable {
    
    let menu: PreviewContextMenu<Content>
    let didCommitView: () -> 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 {
        return Coordinator(menu: self.menu, didCommitView: self.didCommitView)
    }
    
    class Coordinator: NSObject, UIContextMenuInteractionDelegate {
        
        let menu: PreviewContextMenu<Content>
        let didCommitView: () -> Void
        
        init(menu: PreviewContextMenu<Content>, didCommitView: @escaping () -> Void) {
            self.menu = menu
            self.didCommitView = didCommitView
        }
        
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
            return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
                UIHostingController(rootView: self.menu.destination)
            }, actionProvider: { _ in
                let editAction = UIAction(title: "Edit", image: UIImage(systemName: "pencil")) { _ in
                    self.menu.editAction()
                }
                let deleteAction = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in
                    self.menu.deleteAction()
                }
                return UIMenu(title: "", children: [editAction, deleteAction])
            })
        }
        
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
            animator.addCompletion(self.didCommitView)
        }
        
        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
            let parameters = UIPreviewParameters()
            parameters.backgroundColor = .clear
            return UITargetedPreview(view: interaction.view!, parameters: parameters)
        }

        func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
            let parameters = UIPreviewParameters()
            parameters.backgroundColor = .clear
            return UITargetedPreview(view: interaction.view!, parameters: parameters)
        }
        
    }
}

// Add context menu modifier

extension View {
    func contextMenu<Content: View>(_ menu: PreviewContextMenu<Content>) -> some View {
        self.modifier(PreviewContextViewModifier(menu: menu))
    }
}

struct PreviewContextViewModifier<V: View>: ViewModifier {
    
    let menu: PreviewContextMenu<V>
    @Environment(\.presentationMode) var mode
    
    @State var isActive: Bool = false
    
    func body(content: Content) -> some View {
        Group {
            if isActive {
                VStack {
                    menu.destination
                    HStack {
                        Button("Edit") {
                            menu.editAction()
                        }
                        .padding()
                        
                        Button("Delete") {
                            menu.deleteAction()
                        }
                        .padding()
                    }
                }
            } else {
                content.overlay(PreviewContextView(menu: menu, didCommitView: { self.isActive = true }))
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment