I'm trying to have the advantage of the UITextView with Data Detector Types inside a TableViewCell that is itself clickable.
The only thing is, I need the UITextView's links to be clickable, so userInteractionEnabled = YES, unfortunately this will prevent any touch going through to the UITableViewCell.
Of course the UITextView can't be edited, I also subclassed it refusing it to be first responder to avoid selecting text in it.
The only thing I need, is detecting that if a user touch the UITextView, check if it was a link, if it is then opening the link, otherwise redirect the touch on the UITableViewCell.
any idea how can this be done ?
much like the twitter App (we can either click on the row, or on the link...)
I know that this question has been asked a while ago, but the behaviour is still very much needed for some app to have a clickable Cell with UIDataDetectors.
So here's the UITextView subclass I made up to fit this particular behaviour in a UITableView
-(id) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.delegate = self;
}
return self;
}
- (BOOL)canBecomeFirstResponder {
return NO;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UIView *obj = self;
do {
obj = obj.superview;
} while (![obj isKindOfClass:[UITableViewCell class]]);
UITableViewCell *cell = (UITableViewCell*)obj;
do {
obj = obj.superview;
} while (![obj isKindOfClass:[UITableView class]]);
UITableView *tableView = (UITableView*)obj;
NSIndexPath *indePath = [tableView indexPathForCell:cell];
[[tableView delegate] tableView:tableView didSelectRowAtIndexPath:indePath];
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
return YES;
}
You can modify this to fit your needs...
Hope it helps someone.
Related
I am working on a app using TableView now i am facing an issue listed below.
Inside my TableView there is UITextView on it, that MUST be selectable, but not editable (because I need to use and proceed links).
My issue is:
when I tap on a link as everybody does, it doesn't work. I need to hold it a bit longer to make it work. I thought that it is because of "Selectable" property brings in a Double Tap Gesture recognizer, so my textView checks if there is a second tap, but I don't know how to find and remove only double tap recognizer.
What should I do?
Thank you.
Have you considered replacing the TextView with a UIWebView, and just do a loadHTMLString function?
This way when you tap on a link, it will open instantly? You can even have a UIWebView delegate and do what you want when the link is pressed(Custom UIWebView instead of auto opening in safari etc)
You've to handle tap event.. Through this code
tapGesture.numberOfTapsRequired = 1
OR
To do this, you will need to embed one in your UITableViewCell. But there's no need to create a custom cell. Here is the basic idea of what you will want to do:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
UITextView *comment = [[UITextView alloc] initWithFrame:CGRectMake(cell.frame.origin.x, cell.frame.origin.y, cell.frame.size.width, tableView.rowHeight)];
comment.editable = NO;
comment.delegate = self;
[cell.contentView addSubview:comment];
[comment release];
}
return cell;
}
You will, of course, need to set your rowHeight if you don't want the standard 44pt height that comes with the cell. And if you want actual cells, you'll need to add your own logic so that only the cell you want is a textView, but this is the basic idea. The rest is yours to customize to your fitting. Hope this helps
EDIT: to bypass the textView to get to your cell, there are two ways to go about this.
1) you can make a custom textView class and overwrite touchesBegan to send the message to super:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}
this will send the touch events to its superview, which would be your tableView. Considering you didn't want to make custom UITableViewCells, I imagine you probably don't want to make a custom textView class either. Which leads me to option two.
2) when creating the textView, remove comment.editable = NO;. We need to keep it editable, but will fix that in a delegate method.
In your code, you will want to insert a textView delegate method and we'll do all our work from there:
EDIT: changing this code to use with a UITableViewController
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
// this method is called every time you touch in the textView, provided it's editable;
NSIndexPath *indexPath = [self.tableView indexPathForCell:textView.superview.superview];
// i know that looks a bit obscure, but calling superview the first time finds the contentView of your cell;
// calling it the second time returns the cell it's held in, which we can retrieve an index path from;
// this is the edited part;
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
// this programmatically selects the cell you've called behind the textView;
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
// this selects the cell under the textView;
return NO; // specifies you don't want to edit the textView;
}
If that's not what you wanted, just let me know and we'll get you sorted out
Finding and Removing Double Tap Gesture recognizer
Objective C
- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]])
{
[(UITapGestureRecognizer *)gestureRecognizer setNumberOfTapsRequired:1];
gestureRecognizer.enabled = NO;
}
}
Swift
func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer)
{
if gestureRecognizer.isKindOfClass(UITapGestureRecognizer)
{
(gestureRecognizer as! UITapGestureRecognizer).numberOfTapsRequired = 1
gestureRecognizer.enabled = false
}
}
I'm creating a new UIViewController (call it MyViewController), and adding the view (MyView) as a subview of a TableViewCell. Within MyView , there's a button. That button is created programmatically during MyViewController's init, as such:
-(id)initWithFrame:(CGRect)frame {
self = [super init];
if(self) {
[self.view setFrame:frame];
_yesButton = [[UIButton alloc] initWithFrame:CGRectMake(frame.size.width-150, 40, 140, 30)];
[_yesButton setTitle:#"Yeah!" forState:UIControlStateNormal];
[_yesButton addTarget:self action:#selector(didClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_yesButton];
}
return self;
}
Now, seems straightforward. It displays properly, everything looks great in the simulator.
But when I click on the "_yesButton" within MyView, I get a crash with this error:
-[_UITableViewCellSeparatorView didClick]: unrecognized selector sent to instance 0x7b7f7ec0
What? When did "_UITableViewCellSeparatorView" come into the equation? I specifically told the _yesButton to set the Target to "self", so the selector should be sent to MyViewController, right? I could even imagine it getting tripped up and sending it to the UITableViewCell, since MyView is embedded within a TableViewCell, but why a SeperatorView?
Can anyone tell me how to get my _yesButton to send the call back to the MyViewController that it's being created within? And for bonus points, can you explain how "_UITableViewCellSeparatorView" became a thing in this conversation at all?
Edit: Here's how I'm building the cell in the TableView, and adding MyView to it. Note that I'm deliberately not using dequeuing for this row, although that might change if it's the source of the problem.
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"MyViewCell"];
MyViewController *myViewController = [[MyViewController alloc] initWithFrame:cell.contentView.frame];
[cell.contentView addSubview:myViewController.view];
return cell;
And the didClick method is currently empty, (it never even gets there, so I haven't gotten that far in writing it), but it's currently defined within MyViewController simply as:
-(void)didClick {
}
Solution #1
Actually this is because you MyViewController is not retain by ARC. The dealloc method id being called. Add ans instance of your controller in your UITableViewController will fix the issue and make ARC retains your controller.
Solution #2
Try something like this.
Create a custom UITableViewCell :
MyCustomCell.h :
#import <UIKit/UIKit.h>
#interface MyCustomCell : UITableViewCell
#property (strong, nonatomic) UIButton *yesButton;
-(id)initWithFrame:(CGRect)frame;
#end
MyCustomCell.m :
#import "MyCustomCell.h"
#implementation MyCustomCell
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(id)initWithFrame:(CGRect)frame {
self = [super init];
if(self) {
[self.view setFrame:frame];
_yesButton = [[UIButton alloc] initWithFrame:CGRectMake(frame)];
[_yesButton setTitle:#"Yeah!" forState:UIControlStateNormal];
[self.contentView addSubview:_yesButton];
}
return self;
}
#end
In your UITableViewControllerin the viewDidLoad function :
[self.tableView registerClass:[MyCustomCell class] forCellReuseIdentifier:CellIdentifier];
Then in your tableView:cellForRowAtIndexPath:
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifierr forIndexPath:indexPath];
if (cell == nil) {
cell = [[MyCustomCell alloc] initWithFrame:frame];
[cell.yesButton addTarget:self action:#selector(didClick) forControlEvents:UIControlEventTouchUpInside];
}
And now implement the following in your UITableViewController:
-(void)didClick {
// The following identify the in which cell the action has been triggered
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
if (indexPath != nil)
{
// Do whatever you want for the given cell
}
}
I think that you're adopting a wrong approach.
The view cells are not living in memory like views. We may say that once the drawing done, it does not exists anymore.
For all cells operations, you should set you TableViewController as target, and handle the click method in this controller.
Only this controller has a global knowledge of the data in the cells and the cells formats.
You can set the tag of the button to the row index to know what row has been clicked.
your didClick selector is also not correct. It should have this form
-(void)didClick:(id)sender
{
NSInteger clickedRow = ((UIButton*)sender).tag
}
And so be connected like this:
[_yesButton addTarget:<YourTableController> action:#selector(didClick:) forControlEvents:UIControlEventTouchUpInside];
The problem is that the instance you thing to be kind of MyViewController, it is no. It is instead UITableViewCellSeparatorView.
The biggest error, is that you are trying to init a view controller by a method which is not supported from the class UIViewController, but is instead part of UIView:
- (id)initWithFrame:(CGRect)frame
I suggest you to review it before going ahead.
I am unclear where I should add the UIGestureRecognizer code to corresponding subviews of a UITableViewCell. I have read all the related questions I could find. Right now my cells and cell's subviewsare generated inside of cellForRowAtIndexPath. I have tried to add the Gesture inside of cellForRowAtIndexPath with this:
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[mySubview addGestureRecognizer:tapGesture];
tapGesture.cancelsTouchesInView = YES;
tapGesture.delegate = self;
However, this detects nothing. To verify my UIGesture recognizer is working I have used the above code on the tableView itself, and it does register touches as expected. Furthermore, when the tableView has the above gesture attached the below code is also being called as expected:
-(BOOL) gestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
NSLog(#"shouldRevceiveTouch");
return YES;
}
- (BOOL)gestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UITapGestureRecognizer *)otherGestureRecognizer
{
NSLog(#"simultaneously");
return YES;
}
I have tried to remove the GestureRecognizer from the tableView and inside of cellForRowAtIndexPath I have tried to attach the GestureRecognizer to the cell itself, any of its subviews, nothing else gets a touch detected. (None of the above code is triggered)
Clearly I am adding the GestureRecognizer incorrectly. Where/When would be an appropriate location/time to add the GestureRecognizer?
Thank you.
I've done similar thing, but it was UILongPressGestureRecognizer. I think there is no big difference (because all touches are received by UITableView). I've added gesture recognizer in controllers viewDidLoad method (NOT IN cell).
- (void) tableViewLongPress:(UILongPressGestureRecognizer *)gestureRecognizer {
CGPoint p = [gestureRecognizer locationInView:self.messageTableView];
NSIndexPath *indexPath = [self.messageTableView indexPathForRowAtPoint:p];
if (indexPath == nil)
NSLog(#"long press on table view but not on a row");
else {
UITableViewCell *cell = [self.messageTableView cellForRowAtIndexPath:indexPath];
CGPoint pointInCell = [cell convertPoint:p fromView:self.messageTableView];
}
}
You can change Long press to regular one and try it yourself
I needed to detect touches on different subviews inside my cell. also handling iOS 9's UITableViewCellContentView.
First I overrided touchesBegan inside the my custom UITableViewCell
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:touch.view];
// Imagine I have 2 labels inside my cell
CGPoint convertedPoint = [self.firstLabel convertPoint:point fromView:touch.view];
if ([self.firstLabel pointInside:convertedPoint withEvent:nil]) {
// Touched first label
return;
}
convertedPoint = [self.secondLabel convertPoint:point fromView:touch.view];
if ([self.secondLabel pointInside:convertedPoint withEvent:nil]) {
// Touched second label
return;
}
// no labels touched, call super which will call didSelectRowAtIndexPath
[super touchesBegan:touches withEvent:event];
}
And to fix support in iOS 9 we should override awakeFromNib or just disable the cell user intercations somehwere else if cell is not in Storyboard / xib:
- (void)awakeFromNib {
// Initialization code
self.contentView.userInteractionEnabled = NO;
}
of course we shouldn't forget to set our label user interactions enabled.
Not sure exactly what you are trying to do. If you just want to detect if the user taps on a cell within the table then you don't need to implement a gesture recognizer. Just implement the delegate method below to detect when a row from the table has been selected then process the elements of the row such as getting the subview, etc.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Do all my cool tap related stuff here for example, get the row that was tapped:
UITableViewCell *cell= [tableView cellForRowAtIndexPath:indexPath];
// get your subview (assume its a UIImageView) from cell - one way to do it below
UIImageView photo = (UIImageView *)[cell.contentView viewWithTag:PHOTO_TAG];
}
If you describe your problem a little further then perhaps I can offer additional suggestions.
I am using static cells in iOS7 storyboard. The cells have UIButtons in it and they in-turn call the "selectRowAtIndexPath" method.
This is not a consistent behavior and happens only when I switch between the cells.
The two cells here that have the problem have a common superclass. Here's the code:
#implementation StudentMenuMultipleOptionsTableViewCell
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
if (!selected) {
for (UIView *view in self.contentView.subviews) {
if ([view isKindOfClass:[BlackBackgroundSelectedButton class]]) {
BlackBackgroundSelectedButton *button = (BlackBackgroundSelectedButton *)view;
button.selected = NO;
[button setWhite];
}
}
}
}
#end
Quite tricky.
You need to make sure that you are not using the same UIView for multiple cells. The view is resized as soon as it's used once and will create problem with the sizes.
The solution is to create a separate background view for each cell like so:
I am using custom cell with XIB. Now I scroll the table so the cell having the textFiled will be invisible. In this condition if i return the textField (means I call the [textField resignFirstResponder]) The app will crash and giving the error like
[UITableViewCell _didChangeToFirstResponder:]: message sent to deallocated instance
0xe05aa20.
Please let me know if you have any idea about this.
You should check couple of things:
1) In cellForRowAtIndexPath delegate, when you use dequeueReusableCellWithIdentifier you should set endEditing to YES for this view something like:
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"identifier"];
[cell setEndEditing:YES];
if(!cell)
{
// Rest of the logic goes here
}
2) Don't forget to set UITableViewDelegate to self which should represent a live class. So when delegate function calls, class exists.
You need to check whether your TextField is first responder. If it was then you can resign it safely
Hide Keyboard for myTextField
if ([myTextField isFirstResponder]) {
[myTextField resignFirstResponder];
}
To resign First Responder for any subview in a view
for (UIView *aSubView in self.view.subviews) {
if ([aSubView isFirstResponder]) {
[aSubView resignFirstResponder];
//break;
}
}
Use Following method to resolve issue of resignFirstResponder :
[self resignFirstResonder:self.view];
- (BOOL)resignFirstResonder:(UIView *)textView
{
keyBoardShow = NO;
if(badgeHasFullInfo == YES)
{
if (textView.isFirstResponder)
{
[textView resignFirstResponder];
return YES;
}
for (UIView *subView in textView.subviews)
{
if ([self findAndResignFirstResonder:subView])
{
return YES;
}
}
}
return NO;
}