react-native-ios-context-menu icon indicating copy to clipboard operation
react-native-ios-context-menu copied to clipboard

Customize menu action item

Open SamuelScheit opened this issue 2 years ago • 10 comments

Thank you for creating this amazing package 🚀

I'm currently trying to add custom buttons to the context-menu, because not every emoji reaction should be a separate action menu item. That's why I've made a custom preview render, which renders a small bar with pressables.

However when I try to click it the onPress handles don't get called. I've tried to use onPressMenuPreview instead, however the page x & y position of the pressed item don't get passed to the event handler, only the generic RNIContextMenuView tag is passed.

Is there any way to either customize an action menu item or make the buttons in the preview pressable?

SamuelScheit avatar Jan 02 '22 15:01 SamuelScheit

Hey, unfortunately this is a limitation of the UIMenu API 😔

This is actually something I want too, but TLDR the preview doesn't receive any touch events once it's being previewed.

The one in the iMessage app (Reactions), and Safari app (webpage previews), uses private API's.

As soon as apple exposes the public API, I'll add it in haha (maybe if we all file a radar, they'll finally add it)

Note: the last time I tried making the menu preview interactive was in 2019 though, maybe something has changed internally to allow this

Here is the summary:

  • The previews are handled via UIContextMenuContentPreviewProvider — it returns a view controller (it contains the view you want to show in the preview)
  • But once the context menu interaction begins, the view + view controller no longer receive touch events (they are no longer interactive)
  • I even tried setting view. userInteractionEnabled = true when the context menu interaction begins but no luck haha

Edit: I'm currently sick, but I'll try tinkering with the API's again later — I'll close this issue if I can't get it to work

dominicstop avatar Jan 03 '22 06:01 dominicstop

Ah I wish Apple would just open this awesome API...thanks for looking into it @dominicstop

nandorojo avatar Jan 03 '22 22:01 nandorojo

Ok not much we can do about, but thank you for your effort. Get well soon @dominicstop

SamuelScheit avatar Jan 04 '22 13:01 SamuelScheit

@nandorojo @Flam3rboy hey sorry for the late response haha here are results of my testing ✨

Debug Layout: Screen Shot 2022-01-06 at 6 30 20 PM

(lldb) po self.previewController?.next
▿ Optional<UIResponder>
  - some : <UIViewController: 0x7f7a6f90e100>

(lldb) po self.previewController?.next?.next
▿ Optional<UIResponder>
  - some : <UITransitionView: 0x7f7a73016ab0; frame = (0 0; 375 667); alpha = 0; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600002f3dea0>>
  • UITransitionView is internal, not published in the SDK

(lldb) po self.previewController?.next?.next?.next
▿ Optional<UIResponder>
  - some : <UIWindow: 0x7f7a6f90d1e0; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000265f2d0>; layer = <UIWindowLayer: 0x6000026539c0>>
  • Climb responder chain...
  • UIViewController (Preview Content) -> UITransitionView (Internal) -> UIWindow (Main Window)

(lldb) po self.previewController?.presentationController
▿ Optional<UIPresentationController>
  - some : <_UIContextMenuPresentationController: 0x7f7a72365200>

(lldb) po self.previewController?.presentingViewController
▿ Optional<UIViewController>
  - some : <UIViewController: 0x7f7a6f90e100>
  
(lldb) po self.previewController?.presentationController
▿ Optional<UIPresentationController>
  - some : <_UIContextMenuPresentationController: 0x7f7a72365200>

(lldb) po self.previewController?.presentingViewController
▿ Optional<UIViewController>
  - some : <UIViewController: 0x7f7a6f90e100>
  • This is how you get the parent view controller of the preview

(lldb) po self.previewController?.presentingViewController?.view
▿ Optional<UIView>
  - some : <RCTRootView: 0x7f7a6ef3ee90; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x6000028043a0>>

(lldb) po self.window?.rootViewController?.view
▿ Optional<UIView>
  - some : <RCTRootView: 0x7f7a6ef3ee90; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x6000028043a0>>

(lldb) po self.previewController?.presentingViewController?.view === self.window?.rootViewController?.view
true
  • the preview is being presented using the current window's root view controller
  • Thus, the container view that's holding the preview content should be one of it's subviews...

(lldb) po (self.previewController?.next?.next as? UIView)?.gestureRecognizers
nil

(lldb) po (self.previewController?.next?.next as? UIView)
▿ Optional<UIView>
  - some : <UITransitionView: 0x7f7a73016ab0; frame = (0 0; 375 667); alpha = 0; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600002f3dea0>>
  • userInteractionEnabled is disabled, enabling it does nothing...
  • TLDR: What needs to be done is to become responsible for handling the gestures for the preview controller, but it is not exposed...
  • The moment I try to use the private API's, app store connect might reject it (I can do some swizzling, or drop down to Obj-C to use them)
  • So lets try a different approach — I mentioned earlier that the container view that's holding the preview content should be one of it's subviews...

(lldb) po self.window?.subviews
▿ Optional<Array<UIView>>
  ▿ some : 4 elements
    - 0 : <_UITouchFallbackView: 0x7fe89f62f8b0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002c69fe0>>
    - 1 : <UITransitionView: 0x7fe89d00a320; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002855620>>
    - 2 : <UITransitionView: 0x7fe89d0837b0; frame = (0 0; 375 667); alpha = 0; autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600002c50520>>
    - 3 : <_UIContextMenuContainerView: 0x7fe89d07e6d0; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000248d7d0>; layer = <CALayer: 0x600002c505a0>>
  • _UIContextMenuContainerView is the view that's holding our preview content (along with the context menu buttons)

(lldb) po self.window?.subviews.first { ($0.gestureRecognizers?.count ?? 0) > 0 }
▿ Optional<UIView>
  - some : <_UIContextMenuContainerView: 0x7fe89d07e6d0; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000248d7d0>; layer = <CALayer: 0x600002c505a0>>
  
  • _UIContextMenuContainerView is internal, so we can't cast to it... so we search for the view that has a bunch of gesture recoginizer
    • We could filter it via it's internal class name using reflection (e.g. NSStringFromClass), but referencing internal classes will get out app flag by app review haha

(lldb) po self.window?.subviews.first { ($0.gestureRecognizers?.count ?? 0) > 0 }?.gestureRecognizers
▿ Optional<Array<UIGestureRecognizer>>
  ▿ some : 3 elements
    - 0 : <UIPanGestureRecognizer: 0x7fc6021ae860 (com.apple.UIKit.PreviewPlatterPan); state = Possible; enabled = NO; cancelsTouchesInView = NO; view = <_UIContextMenuContainerView 0x7fc60225ec90>; target= <(action=_handlePanGesture:, target=<_UIContextMenuPanController 0x6000034407e0>)>>
    - 1 : <UITapGestureRecognizer: 0x7fc6021aea00 (com.apple.UIKit.ContextMenuDismissalTap); state = Possible; enabled = NO; view = <_UIContextMenuContainerView 0x7fc60225ec90>; target= <(action=_handleDismissalTapGesture:, target=<_UIContextMenuPresentationController 0x7fc6022068b0>)>>
    - 2 : <_UIContextMenuActionScrubbingHandoffGestureRecognizer: 0x7fc6021aeb20 (com.apple.UIKit.ContextMenuActionPanHandoff); state = Possible; enabled = NO; cancelsTouchesInView = NO; view = <_UIContextMenuContainerView 0x7fc60225ec90>; target= <(action=_handleActionHandoffGesture:, target=<_UIContextMenuPresentationController 0x7fc6022068b0>)>>
    
po self.window?.subviews.first { ($0.gestureRecognizers?.count ?? 0) > 0 }?.gestureRecognizers?.forEach { $0.isEnabled = false }
  • Even after disabling the gesture recognizers, the preview content still dont receive any touch events...


Summary: interactive context menu's are still not possible in iOS 15 😔 (if you have any more ideas I can try, please let me know haha, but for now, I'll be closing the issue — if i find another way, I'll update this)

Note: I could not find any resources on the internet for making context menu previews interactive, so the following are just based on my tinkering haha

dominicstop avatar Jan 06 '22 10:01 dominicstop

its very rough, but here's a proof of concept haha

basically this lets you attach an auxiliary view on top of the edge of the context menu preview

https://user-images.githubusercontent.com/18517029/148428709-cfa2f805-95a2-4ebe-984c-487d68f8c17b.mov

dominicstop avatar Jan 06 '22 18:01 dominicstop

This is awesome, let me know if you add it to the library 🚀

SamuelScheit avatar Jan 16 '22 22:01 SamuelScheit

@Flam3rboy i posted a preview on twitter

i'm still designing an API around it, but i'm not sure if the app store would allow this though haha

dominicstop avatar Jan 17 '22 00:01 dominicstop

@Flam3rboy i posted a preview on twitter

i'm still designing an API around it, but i'm not sure if the app store would allow this though haha

Hi, did u add it's into library?

fukemy avatar Jun 16 '22 09:06 fukemy

Yeah, see renderAuxiliaryPreview

nandorojo avatar Jun 18 '22 20:06 nandorojo

ok tks worked

fukemy avatar Aug 08 '22 03:08 fukemy

@dominicstop Hi! Thanks for your library! Can you advice how to render Auxiliary Preview using native code - Swift, maybe alhorithm.

tmbiOS avatar Aug 25 '22 22:08 tmbiOS