How to show a custom UIMenuItem for a UITableViewCell?
As far as I understand there are two main problems:
1) you expect tableView canPerformAction:
to support custom selectors while the documentation says it supports only two of UIResponderStandardEditActions
(copy and/or paste);
2) there's no need for the part || action == @selector(test:)
as you are adding the custom menu options by initializing menuItems
property. For this items selectors the check will be automatical.
What you can do to get the custom menu item displayed and work is:
1) Fix the table view delegate methods with
a)
UIMenuItem *testMenuItem = [[UIMenuItem alloc] initWithTitle:@"Test" action:@selector(test:)];
[[UIMenuController sharedMenuController] setMenuItems: @[testMenuItem]];
[[UIMenuController sharedMenuController] update];
b)
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
-(BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return (action == @selector(copy:));
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// required
}
2) Setup the cells (subclassing UITableViewCell
) with
-(BOOL) canPerformAction:(SEL)action withSender:(id)sender {
return (action == @selector(copy:) || action == @selector(test:));
}
-(BOOL)canBecomeFirstResponder {
return YES;
}
/// this methods will be called for the cell menu items
-(void) test: (id) sender {
}
-(void) copy:(id)sender {
}
///////////////////////////////////////////////////////
UIMenuItem not showing in table
- Your
test
function needs to be in theUITableViewCell
subclass. - You need to implement
canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool
in thatUITableViewCell
subclass and returnreturn action == #selector(test)
- In
UIMenuController.shared.menuItems = [UIMenuItem(title: "Test", action: #selector(test))]
change#selector(test)
to#selector(YourCellSubclass.test)
. - Keep the UITableViewDelegate methods you have in your view controller, and change
|| action == #selector(test)
to|| action == #selector(YourCellSubclass.test)
EDIT:
Adding working example.
ViewController:
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
UIMenuController.shared.menuItems = [UIMenuItem(title: "Test", action: #selector(MyCell.test))]
UIMenuController.shared.update()
tableView.register(MyCell.self, forCellReuseIdentifier: "my")
// Do any additional setup after loading the view, typically from a nib.
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath) as! MyCell
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
return action == #selector(MyCell.test)
}
override func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) {
// needs to be here
}
}
Cell:
class MyCell: UITableViewCell {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(test)
}
@objc func test() {
print("works")
}
}
show UIMenuController in UITableViewCell, grouped style
Yes!
Call [[UIMenuController sharedMenuController] setMenuVisible:YES animated:ani]
(where ani
is a BOOL
determining whether the controller should be animated) from within - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
( UITableView's delegate method)
Edit: The 'copy' command on the UIMenuController
will not by default copy the detailTextLabel.text
text. However, there is a workaround. Add the following code into your class.
-(void)copy:(id)sender {
[[UIPasteboard generalPasteboard] setString:detailTextLabel.text];
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if(action == @selector(copy:)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
Showing UIMenuController deselects UITableViewCell
A simpler way of implementing this is using the specific UITableViewDelegate methods for dealing with UIMenuController.
But first, to make the cell stay selected, store the value of the cell presenting the menu in your class:
NSIndexPath *_editingIndexPath;
Then implement the UITableViewDelegateMethods:
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCustomTableViewCell *cell = (MyCustomTableViewCell *) [_tableView cellForRowAtIndexPath:indexPath];
_editingIndexPath = indexPath;
cell.showingMenu = YES;
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:)) {
return YES;
}
return NO;
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
if (action == @selector(copy:))
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
if (cell && [cell isKindOfClass:[MessageConversationCell class]])
{
[UIPasteboard generalPasteboard].string = cell.textLabel.text;
}
}
}
The code above will take care of showing a "copy" menu on the cell after a long press.
Now, if you want the cell to stay selected while the menu is displayed:
Add a @property in your custom cell named "showingMenu" (note that this property was already set in the first block of code in this answer).
@property (nonatomic, assign) BOOL showingMenu;
Add (or modified if already present) the following method to your custom cell. This will take care of keeping the cell highlighted after the menu tried to unhighlight it (you may implement your own logic for highlighting a cell, in that case put it in the first branch of the if conditional):
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
if (_showingMenu)
{
[super setHighlighted:YES]
}
else
{
[super setHighlighted:highlighted];
}
}
Add an observer to be notified when the menu is going to be presented. This goes into the view controller, NOT in the custom cell:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didShowEditMenu:) name:UIMenuControllerDidShowMenuNotification object:nil];
Add on the view controller the method to be called when the menu is displayed:
- (void)didShowEditMenu:(NSNotification *)not {
[_tableView selectRowAtIndexPath:_editingIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
MyCustomTableViewCell *cell = (MyCustomTableViewCell*)[_conversationTableView cellForRowAtIndexPath:_editingIndexPath];
cell.showingMenu = NO;
}
And don't forget to remove the observer when no longer needed:
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIMenuControllerDidShowMenuNotification object:nil];
This will show a menu when a cell is long pressed, and keep the cell selected until the menu disappears, either because an option was chosen or because the user tapped somewhere else. It works pretty much like Whatsapp works when you select a message.
How to get the tapped table view cell in custom action in UIMenuController
Thanks valheru. I find a "nice" approach to achieve that:)
Step one: In MyTableViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPressGesture.minimumPressDuration = .5;
longPressGesture.delegate = self;
[self.view addGestureRecognizer:longPressGesture];
}
which register the gesture recognizer of the long press on the table view controller.
- (BOOL)canBecomeFirstResponder
{
return YES;
}
which allows MyTableViewController response the long press and popup the context menu.
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if(gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint point = [gestureRecognizer locationInView:self.tableView];
NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:point];
if(indexPath == nil) return ;
MyCell *cell = (MyCell *)[self.tableView cellForRowAtIndexPath:indexPath];
UIMenuItem *determine = [[UIMenuItem alloc] initWithTitle:@"My Action on this cell" action:@selector(handleMyAction:)];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuItems:[NSArray arrayWithObjects:determine, nil]];
[menu setTargetRect:cell.frame inView:cell.superview];
[menu setMenuVisible:YES animated:YES];
[cell becomeFirstResponder]; //here set the cell as the responder of the menu action
cell.delegate = self;// this is optional, if you don't want to implement logic in cell class
}
}
create the UIMenuController and popup when i long press the cell.
-(void)handleMyAction: (UITableViewCell *)cell
{
NSLog(@"%@", cell);
}
this function will be called later from the cell which is pressed.
Step two: Create a subclass of UITableViewCell named MyCell
In MyCell.h defines the table view controller the cell belongs as the delegate of the cell. And the callback function when the menu entry clicked
@property (nonatomic, strong) MyTableViewController *delegate;
-(void)handleMyAction:(id)sender;
in MyCell.m
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if(action == @selector(handleMyAction:))
{
return YES;
}
return NO;
}
allows the MyCell to be the first responder and response the handleMyAction action by clicking on menu entry.
-(void)handleMyAction:(id)sender
{
[self.delegate handleMyAction:self]; //it's a coincidence both functions have the same name:)
}
this is the definition of the callback function, which will be called when click on the menu entry, and it in turn call the handMyAction function in the delegate of the cell (MyTableViewController, where the logic regarding the cell could be implemented.)
Related Topics
How to Set the Title of a Uibutton as Left-Aligned
Lldb (Swift): Casting Raw Address into Usable Type
Cocoapods - 'Pod Install' Takes Forever
How to Draw a Transparent Uitoolbar or Uinavigationbar in iOS7
Uiview Animatewithduration Doesn't Animate Cornerradius Variation
Uibarbuttonitem in Navigation Bar Programmatically
Objective C - Assign, Copy, Retain
Loading a Reusable Uitableviewcell from a Nib
The Code Signature Version Is No Longer Supported
How to Use Subscript and Superscript in Swift
Xmppframework - Implement Group Chat (Muc)
How to Add a Toolbar Above the Keyboard
Rsa Implementations in Objective C
In iPhone App How to Detect the Screen Resolution of the Device