I've implemented a long press gesture recognizer on a table view cell that shows the UIMenuController. But when menu shows, the corresponding table view cell deselects. Before showing the menu, I call, as required, [self becomeFirstResponder]. I think that this call deselects the cell, but how to make it to stay selected while the UIMenuController is visible?
In your UITableViewDelegate, override tableView:willDeselectRowAtIndexPath: and return nil when you don’t want your row to deselect, according to the documentation.
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.
You say you are calling [self becomeFirstResponder] but your question does not indicate which object self is. I would guess self is your controller. You should be sending the becomeFirstResponder message to the UITableViewCell that is spawning the UIMenuController.
- (void)longPress:(UILongPressGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
TSTableViewCell *cell = (TSTableViewCell *)recognizer.view;
//This is your problem
[cell becomeFirstResponder];
UIMenuItem *flag = [[UIMenuItem alloc] initWithTitle:#"Flag" action:#selector(flag:)];
UIMenuItem *approve = [[UIMenuItem alloc] initWithTitle:#"Approve" action:#selector(approve:)];
UIMenuItem *deny = [[UIMenuItem alloc] initWithTitle:#"Deny" action:#selector(deny:)];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuItems:[NSArray arrayWithObjects:flag, approve, deny, nil]];
[menu setTargetRect:cell.frame inView:cell.superview];
[menu setMenuVisible:YES animated:YES];
}
}
I have found this solution:
- (void)handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer
{
if (longPressRecognizer.state == UIGestureRecognizerStateBegan) {
UITableViewCell *cell = (UITableViewCell *)longPressRecognizer.view;
[cell setSelected:YES];
...
}
}
Related
I'm trying to fix an issue for two days but I really don't know what to do. I tried several solutions but nothing helped.
This is the situation: I have a UIViewController and I've added a subclass of UITextView as subview.
-(void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
_textView = [[DPTextView alloc] initWithFrame:self.view.bounds];
[_textView setText:[NSString stringWithContentsOfFile:_path encoding:NSUTF8StringEncoding error:nil]];
_textView.editable = NO;
_textView.selectable = YES;
_textView.delegate = self;
_textView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:_textView];
//[self.view becomeFirstResponder];
//[self becomeFirstResponder];
//[_textView becomeFirstResponder];
}
When I open this UIViewController, I see the text. If I select the text, I don't see the UIMenuController. I've already overriden some methods of the subclass of UITextView (DPTextView), like the following:
-(BOOL)canBecomeFirstResponder {
return TRUE;
}
-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == #selector(copy:))
return YES;
return NO;
}
But it doesn't appear. After some time I've noticed that the UIMenuController appears if I follow some steps:
This controller is presented from another controller. Let's call them Controller1 (that has UISearchController and UITableView) and Controller2 (with DPTextView).
I tap the search bar into Controller1.
I dismiss the keyboard tapping "Cancel", next to the search bar.
I tap UITableViewCell that presents Controller2.
Following those steps, the UIMenuController appears when I select text into Controller2! I don't know really why! I thought something happened when pressing the search bar, like the controller becomes the first responder or something similar (and I've already tried to call "becomeFirstResponder", as you can read in the code above -- the commented lines of code).
Note that if I tap and hold UITableViewCell in Controller1, I should get UIMenuController too, because I've implemented those methods:
-(BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return TRUE;
}
-(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 {
if (action == #selector(copy:)) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[[UIPasteboard generalPasteboard] setString:cell.textLabel.text];
}
}
But it doesn't appear here too. It appears if after using the search controller, so the same situation.
Any suggestion? I don't know if this can help but Controller1 and Controller2 are presented modally.
I am displaying custom UIMenuController in my tableview like this
but issue is that it is displaying in the center I want to display it on top of label which is orange colored. For displaying on top of label I did this [menu setTargetRect:CGRectMake(10, 10, 0, 0) inView:self.lbl]; below is the entire code.
But if I am displaying the UIMenuController without UITableView setTargetRect works fine.
Why setTargetRect is not working with UITableView?
setTargetRect Doc says:
(a) This target rectangle (targetRect) is usually the bounding rectangle
of a selection. UIMenuController positions the editing menu above this
rectangle; if there is not enough space for the menu there, it
positions it below the rectangle. The menu’s pointer is placed at the
center of the top or bottom of the target rectangle as appropriate.
(b) Note that if you make the width or height of the target rectangle
zero, UIMenuController treats the target area as a line or point for
positioning (for example, an insertion caret or a single point).
(c) Once it is set, the target rectangle does not track the view; if the
view moves (such as would happen in a scroll view), you must update
the target rectangle accordingly.
What I am missing?
MyViewController
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 5;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TheCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cid" forIndexPath:indexPath];
cell.lbl.text = [NSString stringWithFormat:#"%ld", (long)indexPath.row];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
-(BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return YES;
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// required
}
MyCustomCell
- (void)awakeFromNib {
// Initialization code
UIMenuItem *testMenuItem = [[UIMenuItem alloc] initWithTitle:#"Test" action:#selector(test:)];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuItems: #[testMenuItem]];
[menu setTargetRect:CGRectMake(10, 10, 0, 0) inView:self.lbl];
[menu update];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(BOOL) canPerformAction:(SEL)action withSender:(id)sender {
return (action == #selector(copy:) || action == #selector(test:));
}
/// this methods will be called for the cell menu items
-(void) test: (id) sender {
NSLog(#"test");
}
-(void) copy:(id)sender {
UIMenuController *m = sender;
NSLog(#"copy");
}
1) First, please do this in your view controller's viewDidLoad method.
UIMenuItem *testMenuItem = [[UIMenuItem alloc] initWithTitle:#"Test"
action:#selector(test:)];
[[UIMenuController sharedMenuController] setMenuItems: #[testMenuItem]];
[[UIMenuController sharedMenuController] update];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(menuControllerWillShow:) name:UIMenuControllerWillShowMenuNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(menuControllerWillHide:) name:UIMenuControllerWillHideMenuNotification object:nil];
2) Then, add these two Methods. and set menuFrame inView to your cell
-(void)menuControllerWillShow:(NSNotification *)notification{
//Remove Will Show Notification to prevent
// "menuControllerWillShow" function to be called multiple times
[[NSNotificationCenter defaultCenter]removeObserver:self name:UIMenuControllerWillShowMenuNotification object:nil];
//Hide the Original Menu View
UIMenuController* menuController = [UIMenuController sharedMenuController];
CGSize size = menuController.menuFrame.size;
CGRect menuFrame;
menuFrame.origin.x = self.tableView.frame.origin.x;
menuFrame.size = size;
[menuController setMenuVisible:NO animated:NO];
//Modify its target rect and show it again to prevent glitchy appearance
[menuController setTargetRect:menuFrame inView:cell];
[menuController setMenuVisible:YES animated:YES];
}
-(void)menuControllerWillHide:(NSNotification *)notification{
//re-register menuControllerWillShow
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(menuControllerWillShow:)
name:UIMenuControllerWillShowMenuNotification object:nil];
}
3) Implement tableView Delegates and implement these methods.
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
cell = (TableViewCell*)[tableView cellForRowAtIndexPath:indexPath];
return YES;
}
-(BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return NO;
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// required
}
4) Setup the cells (subclassing UITableViewCell) with
-(BOOL) canPerformAction:(SEL)action withSender:(id)sender {
return (action == #selector(copy:) || action == #selector(test:)); } /// this methods will be called for the cell menu items
-(void) test: (id) sender {
NSLog(#"test"); }
-(void) copy:(id)sender {
NSLog(#"copy"); }
How can i slide programmatically SWTableViewCell.
//When i click in a cell i want to open the swtableviewcell
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
???
}
i have tried:
[[[SWTableViewCell alloc]init]didTransitionToState:2];
and in swtableviewcell.m
-(void)didTransitionToState:(UITableViewCellStateMask)state{
NSLog(#"state: %lu",state);
if (_cellState == kCellStateCenter)
{
[self.cellScrollView setContentOffset:[self contentOffsetForCellState:kCellStateRight] animated:YES];
[self.delegate swipeableTableViewCell:self scrollingToState:kCellStateRight];
}
}
but is not correct. Can you help me?
Thanks,
Mikel
I wanted to be able to show the utility buttons when a table view cell was selected so I added a method in SWTableViewCell.m
-(void)showUtilityButtonsAnimated:(BOOL)animated {
// Force the scroll back to run on the main thread because of weird scroll view bugs
dispatch_async(dispatch_get_main_queue(), ^{
[self.cellScrollView setContentOffset:CGPointMake(0, 0) animated:YES];
});
_cellState = kCellStateLeft;
if ([self.delegate respondsToSelector:#selector(swipeableTableViewCell:scrollingToState:)])
{
[self.delegate swipeableTableViewCell:self scrollingToState:kCellStateCenter];
}
}
and don't forget to add it to SWTableViewCell.h
- (void)showUtilityButtonsAnimated:(BOOL)animated;
Then call that method from within your -tableView:didSelectRowAtIndexPath: delegate method. For example:
if (indexPath.row == 0) {
SWTableViewCell *cell = (SWTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
[cell showUtilityButtonsAnimated:YES];
}
I have a tableview with custom UITableViewCell. Cell have a UISwitch controls. I have added cell into the table view controller with the same action for all switches. I have added tag value of UISwitch in cellForRowAtIndexPath method.
I want to determine what switch was changed, when user changed the switch status on/off. Here I am setting action on UISwitch button.
- (void)viewDidLoad
{
[super viewDidLoad];
cell.switchbtn.userInteractionEnabled=YES;
[cell.switchbtn setOn:YES];
[cell.switchbtn addTarget:self action:#selector(switchToggled:) forControlEvents: UIControlEventValueChanged];
[cell.contentView addSubview:cell.switchbtn];
}
Here I am setting tag value of uiswitch
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellidentifier=#"cellid";
cell=[tableView dequeueReusableCellWithIdentifier:cellidentifier];
if (!cell)
{
[[NSBundle mainBundle] loadNibNamed:#"cellid" owner:self options:nil];
}
cell.switchbtn.tag=indexPath.row;
NSLog(#"btn tag=%d",cell.switchbtn.tag);
return cell;
}
Here I am calling switchToggled: method to get the uiswitch status.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
{
[self switchToggled:cell.switchbtn];
}
I am gettting tag value and each time status is On .even i status is off.
- (void) switchToggled:(UISwitch *)sender {
UISwitch *mySwitch = (UISwitch *)sender;
NSLog(#"tag ==%#",mySwitch);
if ([mySwitch isOn]) {
NSLog(#"its on!");
} else {
NSLog(#"its off!");
}
}
Use CGpoint with direct Cell's property to identify which row is selected and that cell's Uiswitch's state you can identify easily even more accurate and this is the one method am using and it is working fine for me.
- (void)switchToggled:(id)sender
{
CGPoint switchPositionPoint = [sender convertPoint:CGPointZero toView:[self tableName]];
NSIndexPath *indexPath = [[self tableName] indexPathForRowAtPoint:switchPositionPoint];
BusinessHoursCell *cell = (BusinessHoursCell*)[self.tableName cellForRowAtIndexPath:indexPath];
int tag=indexPath.row;
if (tag==0)
{
if (cell.workingDaySwitch.on)
{
NSLog(#"its on!");
}
else
{
NSLog(#"its off!");
}
}
}
I'd use the following scheme:
In cellForRowAtIndexPath:.. set current indexPath to cell. Create protocol with method like - (void)cellAtIndexPath:(NSIndexPath *)path changedSwitchStateToState:(BOOL)state make controller delegate for cells. Cell handles switch state change by calling that delegate method.
In controller you should use something for dataSource(probably custom class) which stores objects which are used to configure the cell and has a method similar to - (void)changeSwitchStateToState:(BOOL)state atIndexPath:(NSIndexPath *)path where it gets corresponding object and updates state.
i am trying to use UIMenuController to perform a custom action on the table cell, from which the UIMenuController triggered by long time press.
I registered UILongPressGestureRecognizer in the viewDidLoad method in my subclass of UITableViewController and added custom item with #selector(handleMyAction).
- (void)viewDidLoad
{
[super viewDidLoad];
[self.refreshControl addTarget:self action:#selector(refreshView:) forControlEvents:UIControlEventValueChanged];
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPress:)];
longPressGesture.minimumPressDuration = .5;
longPressGesture.delegate = self;
[self.tableView addGestureRecognizer:longPressGesture];
}
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if(gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint point = [gestureRecognizer locationInView:self.tableView];
NSIndexPath * indexPath = [self.tableView indexPathForRowAtPoint:point];
if(indexPath == nil) return ;
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIMenuItem *it = [[UIMenuItem alloc] initWithTitle:#"My Action on this cell" action:#selector(handleMyAction:)];
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuItems:[NSArray arrayWithObjects:it, nil]];
[menu setTargetRect:cell.frame inView:cell.superview];
[menu setMenuVisible:YES animated:YES];
[self becomeFirstResponder];
}
}
I also override the
- (BOOL)canBecomeFirstResponder{
return YES;
}
When I press on one cell the context menu with the custom entry displays properly. BUT, the problem is how can I implement the method to handle the custom action, which should be performed on the tapped cell.
- (void)handleMyAction:(id)sender
{
NSLog(#"Action triggered, however need some way to refer the tapped cell");
}
Because the only information i can get in this method is the sender, which is the UIMenuController self, but i have no idea how can get the cell, on which the Menu triggered, so i can do further action regarding the cell itself.
Could some one help me on that?
Thanks.
Hai
Well you're currently adding the UIGestureRecognizer to the tableview itself. Why not add it to each cell instead (in cellForRowAtIndexPath when they are setup) ?
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.)
First declare a NSIndexPath type as a class variable.
#property (nonatomic, strong) NSIndexPath *savedIndexPathForThePressedCell;
Now in the Long press gesture recognizer function, get the TableViewCell using the recognizer view. Now save the IndexPath for the TableViewCell
- (void)longPressGestureFunction:(UILongPressGestureRecognizer *)recognizer
{
UITableViewCell *lTableViewCell = (UITableViewCell *)recognizer.view;
[lTableViewCell becomeFirstResponder];
/*Save the Indexpath of the cell pressed*/
self.savedIndexPathForThePressedCell = [mTableView indexPathForCell:lTableViewCell];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
UIMenuItem *MenuDelete = [[UIMenuItem alloc] initWithTitle:#"Delete" action:#selector(Delete:)];
UIMenuItem *MenuForward = [[UIMenuItem alloc] initWithTitle:#"Forward" action:#selector(Forward:)];
UIMenuItem *MenuAddToContacts = [[UIMenuItem alloc] initWithTitle:#"Add To Contacts" action:#selector(addToContacts:)];
mSharedMenu = [UIMenuController sharedMenuController];
[mSharedMenu setMenuItems:[NSArray arrayWithObjects: MenuDelete, MenuForward, nil]];
[mSharedMenu setMenuVisible:YES animated:YES];
}
Now in the Menu selector method, select the row based on the saved indexPath.
- (void)Delete:(id)sender {
// NSLog(#"\n Delete Selected \n");
[mTableView setEditing:YES animated:YES];
[mTableView selectRowAtIndexPath:self.savedIndexPathForThePressedCell animated:YES scrollPosition:UITableViewScrollPositionNone];
}
I hope this helps!!!