7

As the title says, is there any way I can detect (e.g. using a @State variable) when either any context menu is open, or the context menu of a specific view is open?

As a basic idea, I would like to print something if it is open. This does not work:

.contextMenu {
   print("open")
}

This also does not seem to work:

.contextMenu {
    EmptyView()
    .onAppear {
        print("open")
    }
}

How could i make this work?


Edit: Why i think its even possible to do it or at least possible to make it look like its possible: On instagram, one can see individual posts as a square only. However using long-press, a context menu opens, and now the post shape is different, but even more there is also a small title above it.. How would one do that? Did they modify the view when the context menu open or is the grid view the post is in before just hiding those details (true image shape + image title) but they are rendered already?

Screenshots:

enter image description here enter image description here

1

3 Answers 3

3
+50

A possible approach is to use simultaneous gesture for this purpose, like

Text("Demo Menu")
    .contextMenu(menuItems: {
        Button("Button") {}
    })
    .simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in
        print("Opened")
    })

Tested with Xcode 13.2 / iOS 15.2

Sign up to request clarification or add additional context in comments.

3 Comments

Technically your example seems to be doing what I showed in my example (i.e. printing something), However could I also use this simultaneousGesture to change the view that is displayed inside the context menu?
I was having big issues because SwiftUI does not handle well simultaneous modal views - I had a Sheet with menus and popovers that could not be dismissed. Your solution here helped me to find a workaround. Kinda ugly code when you need such a redundancy, but it finally solved my problem. Thanks.
If this is applied to an element within a ScrollView you can no longer scroll on the items.
3

I've been testing this. Here's what I found out:

As you observed, this does not call a function:

.contextMenu {
   print("open")
}

Putting an EmptyView() in a contextMenu also does not trigger a function. Since EmptyView() is literally an empty view, it never gets rendered on the View. Since it never appears, onAppear can never get triggered:

.contextMenu {
    EmptyView()
    .onAppear {
        print("open")
    }
}

A Spacer() element will trigger a function, but it also leads to the contextMenu having an empty row item, which looks bad:

 .contextMenu {
    Spacer().onAppear {
       print("open")
    }
}

empty contextMenu row

An empty Text element will trigger a function, but it once again leads to the contextMenu having an empty row item, which looks bad (same as above picture):

 .contextMenu {
    Text("").onAppear {
       print("open")
    }
}

As someone else pointed out, adding a simultaneousGesture will break scrolling:

.simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in
        print("Opened")
    })

The best solution?

Just put .onAppear on the first already-existing element in the contextMenu

.onAppear {
        print("open")
    }

Don't create a new element (EmptyView(), Spacer(), Text(""), etc)

3 Comments

Thanks for the elaborate research. If others can confirm this works best, I can approve this as chosen answer.
I like this onAppear approach much better than the simultaneousGesture, since that relies on a timer not directly synced with the time for context menus. It generally works but it appears the context menu is held in memory some how - if you quickly open and close it multiple times, only one onAppear is called for multiple opens.
The best solution works only once for the 1st time.
2

The previously mentioned solutions have some issues, namely onAppear is only called the first time the menu appears (and not for additional appearances) and onDisappear won't be called (it seems like the menu items are cached internally).

Here is a drop in solution:

import SwiftUI

extension View {
  
  /// A context menu with callbacks for onAppear and onDisappear
  func contextMenu(
    actions: [UIAction],
    onAppear: @escaping () -> Void,
    onDisappear: @escaping () -> Void
  ) -> some View {
    modifier(ContextMenuViewModifier(actions: actions, onAppear: onAppear, onDisappear: onDisappear))
  }
}

private struct ContextMenuViewModifier: ViewModifier {
  let actions: [UIAction]
  let onAppear: () -> Void
  let onDisappear: () -> Void
  
  func body(content: Content) -> some View {
    InteractiveView(content: { content }, actions: actions, onAppear: onAppear, onDisappear: onDisappear)
  }
}

private struct InteractiveView<Content: View>: UIViewRepresentable {
  typealias UIViewControllerType = UIView
  
  @ViewBuilder var content: Content
  
  let actions: [UIAction]
  let onAppear: () -> Void
  let onDisappear: () -> Void
  
  func makeCoordinator() -> Coordinator {
    return Coordinator(parent: self)
  }
  
  func makeUIView(context: Context) -> some UIView {
    let hostedView = UIHostingController(rootView: content)
    hostedView.view.backgroundColor = .clear
    hostedView.view.translatesAutoresizingMaskIntoConstraints = false
    
    context.coordinator.contextMenu = UIContextMenuInteraction(delegate: context.coordinator)
    hostedView.view.addInteraction(context.coordinator.contextMenu!)
    
    return hostedView.view
  }
  
  func updateUIView(_ uiView: UIViewType, context: Context) {
    //
  }
  
  class Coordinator: NSObject, UIContextMenuInteractionDelegate {
    var contextMenu: UIContextMenuInteraction!
    
    private let parent: InteractiveView
    
    init(parent: InteractiveView) {
      self.parent = parent
    }
    
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
      UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [self] suggestedActions in
        return UIMenu(title: "", children: parent.actions)
      })
    }
    
    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, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
      parent.onAppear()
    }
    
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
      parent.onDisappear()
    }
  }
}

You can use it like so:

view
  .contextMenu(
    actions: [
      UIAction(title: "Button") { action in
        // Your code here
      }
    ],
    onAppear: {
      print("Menu onAppear")
    },
    onDisappear: {
      print("Menu onDisappear")
    }
  )

Comments

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.