I have a UITextView in a custom UITableViewCell. The textview works properly (scrolls, shows text, etc.) but I need the users to be able to tap the table cell and go to another screen. Right now, if you tap the edges of the table cell (i.e. outside the UItextView) the next view is properly called. But clearly inside the uitextview the touches are being captured and not forwarded to the table cell.
I found a post that talked about subclassing UITextView to forward the touches. I tried that without luck. The implementation is below. I'm wondering if maybe a) the super of my textview isn't the uitableviewcell and thus I need to pass the touch some other way or b) If the super is the uitableviewcell if I need to pass something else? Any help would be much appreciated.
#import "ScrollableTextView.h"
#implementation ScrollableTextView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (parentScrollView) {
[parentScrollView touchesBegan:touches withEvent:event];
}
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
if (parentScrollView) {
[parentScrollView touchesCancelled:touches withEvent:event];
}
[super touchesCancelled:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (parentScrollView) {
[parentScrollView touchesEnded:touches withEvent:event];
}
[super touchesEnded:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (parentScrollView) {
[parentScrollView touchesMoved:touches withEvent:event];
}
[super touchesMoved:touches withEvent:event];
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
#end
Try [theTextView setUserInteractionEnabled:NO]; If the user needs to be able to edit the contents of the TextView, then you might have a design problem here.
Swift 3 : theTextView.isUserInteractionEnabled = false
Storyboard : tick the "User Interaction Enabled" checkbox.
I know that this question has been asked 5 years 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.
The problem with your solution is that if you put UITextView inside UITableViewCell, its superview won't be the actual cell. There's even a slight difference between iOS 7 and iOS 8 on the cell's view structure. What you need to do is drill down (or drill up) through the hierarchy to get UITableViewCell instance.
I am using and modifying #TheSquad's while loop to get the UITableViewCell, and assign it to a property. Then override those touch methods, use the cell's touches method whenever needed, and just use super's touch method's implementations to get the default behaviour.
// set the cell as property
#property (nonatomic, assign) UITableViewCell *superCell;
- (UITableViewCell *)superCell {
if (!_superCell) {
UIView *object = self;
do {
object = object.superview;
} while (![object isKindOfClass:[UITableViewCell class]] && (object != nil));
if (object) {
_superCell = (UITableViewCell *)object;
}
}
return _superCell;
}
#pragma mark - Touch overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.superCell) {
[self.superCell touchesBegan:touches withEvent:event];
} else {
[super touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.superCell) {
[self.superCell touchesMoved:touches withEvent:event];
} else {
[super touchesMoved:touches withEvent:event];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.superCell) {
[self.superCell touchesEnded:touches withEvent:event];
} else {
[super touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.superCell) {
[self.superCell touchesEnded:touches withEvent:event];
} else {
[super touchesCancelled:touches withEvent:event];
}
}
The answers above don't solve the problem if you have links in the UITextView and want them to work as usual when user taps a link, and pass the tap to the cell if user taps regular text. With the proposed method cell will be "selected" in both cases.
Here are some possible solutions:
https://stackoverflow.com/a/59010352/11448489 - add a tap gesture recognizer to the cell, and set it to require UITextInteractionNameLinkTap recognizer failure. The problem is that UITextInteractionNameLinkTap string is from internal Apple API and can change. Also, we still have to directly call delegate's didSelectRowAtIndexPath, so the cell won't be animated.
Implement override of touchesEnded in the text view. In it perform some selector after delay of at least 0.4s. In the text view delegate cancel this perform request if an interaction with url happened:
class TappableTextView: UITextView, UITextViewDelegate {
var tapHandler: (() -> Void)?
override var delegate: UITextViewDelegate? {
get { self }
set { }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.perform(#selector(onTap), with: nil, afterDelay: 0.5)
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
Self.cancelPreviousPerformRequests(withTarget: self, selector: #selector(onTap), object: nil)
return true
}
#objc func onTap() {
self.tapHandler?()
}
}
It works, but delay is noticeable and annoying. It is not possible to reduce this delay because shouldInteractWith happens after 350ms after touchesEnded.
And we still have to call didSelectRowAtIndexPath.
I came to another solution, which seems to work perfectly if you need clickable links, but no other interactions (not scrollable, selectable etc). Essentially, we need to make the text view ignore all touches which are not in the links area:
class TapPassingTextView: UITextView, UITextViewDelegate {
var clickableRects = [CGRect]()
override func layoutSubviews() {
super.layoutSubviews()
self.updateClickableRects()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
clickableRects.contains { $0.contains(point) } ? super.hitTest(point, with: event) : nil
}
private func updateClickableRects() {
self.clickableRects = []
let range = NSRange(location: 0, length: self.attributedText.string.count)
self.attributedText.enumerateAttribute(.link, in: range) { link, range, _ in
guard link != nil else { return }
self.layoutManager.enumerateLineFragments(forGlyphRange: range) { rect, _, _, _, _ in
self.clickableRects.append(rect)
}
}
}
}
That's it! Taps on links are working and taps in other areas go below the text view, cells are selected natively.
Related
I've looked at a ton of posts on similar things, but none of them quite match or fix this issue. Since iOS 7, whenever I add a UIButton to a UITableViewCell or even to the footerview it works "fine", meaning it receives the target action, but it doesn't show the little highlight that normally happens as you tap a UIButton. It makes the UI look funky not showing the button react to touch.
I'm pretty sure this counts as a bug in iOS7, but has anyone found a solution or could help me find one :)
Edit:
I forgot to mention that it will highlight if I long hold on the button, but not a quick tap like it does if just added to a standard view.
Code:
Creating the button:
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.titleLabel.font = [UIFont systemFontOfSize:14];
button.titleLabel.textColor = [UIColor blueColor];
[button setTitle:#"Testing" forState:UIControlStateNormal];
[button addTarget:self action:#selector(buttonPressed:) forControlEvents: UIControlEventTouchDown];
button.frame = CGRectMake(0, 0, self.view.frame.size.width/2, 40);
Things I've Tested:
//Removing gesture recognizers on UITableView in case they were getting in the way.
for (UIGestureRecognizer *recognizer in self.tableView.gestureRecognizers) {
recognizer.enabled = NO;
}
//Removing gestures from the Cell
for (UIGestureRecognizer *recognizer in self.contentView.gestureRecognizers) {
recognizer.enabled = NO;
}
//This shows the little light touch, but this isn't the desired look
button.showsTouchWhenHighlighted = YES;
In that tableview you just add this property.
tableview.delaysContentTouches = NO;
And add in cellForRowAtIndexPath after you initiate the cell you just add below code. The structure of the cell is apparently different in iOS 6 and iOS 7.
iOS 7 we have one control UITableViewCellScrollView In between UITableViewCell and content View.
for (id obj in cell.subviews)
{
if ([NSStringFromClass([obj class]) isEqualToString:#"UITableViewCellScrollView"])
{
UIScrollView *scroll = (UIScrollView *) obj;
scroll.delaysContentTouches = NO;
break;
}
}
Since iOS 8 we need to apply the same technique to UITableView subviews (table contains a hidden UITableViewWrapperView scroll view). There is no need iterate UITableViewCell subviews anymore.
for (UIView *currentView in tableView.subviews) {
if ([currentView isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)currentView).delaysContentTouches = NO;
break;
}
}
This answer should be linked with this question.
I tried to add this to the accepted answer but it never went through. This is a much safer way of turning off the cells delaysContentTouches property as it does not look for a specific class, but rather anything that responds to the selector.
In Cell:
for (id obj in self.subviews) {
if ([obj respondsToSelector:#selector(setDelaysContentTouches:)]) {
[obj setDelaysContentTouches:NO];
}
}
In TableView:
self.tableView.delaysContentTouches = NO;
For a solution that works in both iOS7 and iOS8, create a custom UITableView subclass and custom UITableViewCell subclass.
Use this sample UITableView's initWithFrame:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
// iterate over all the UITableView's subviews
for (id view in self.subviews)
{
// looking for a UITableViewWrapperView
if ([NSStringFromClass([view class]) isEqualToString:#"UITableViewWrapperView"])
{
// this test is necessary for safety and because a "UITableViewWrapperView" is NOT a UIScrollView in iOS7
if([view isKindOfClass:[UIScrollView class]])
{
// turn OFF delaysContentTouches in the hidden subview
UIScrollView *scroll = (UIScrollView *) view;
scroll.delaysContentTouches = NO;
}
break;
}
}
}
return self;
}
Use this sample UITableViewCell's initWithStyle:reuseIdentifier:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
// iterate over all the UITableViewCell's subviews
for (id view in self.subviews)
{
// looking for a UITableViewCellScrollView
if ([NSStringFromClass([view class]) isEqualToString:#"UITableViewCellScrollView"])
{
// this test is here for safety only, also there is no UITableViewCellScrollView in iOS8
if([view isKindOfClass:[UIScrollView class]])
{
// turn OFF delaysContentTouches in the hidden subview
UIScrollView *scroll = (UIScrollView *) view;
scroll.delaysContentTouches = NO;
}
break;
}
}
}
return self;
}
What I did to solve the problem was a category of UIButton using the following code :
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[NSOperationQueue.mainQueue addOperationWithBlock:^{ self.highlighted = YES; }];
}
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
[self performSelector:#selector(setDefault) withObject:nil afterDelay:0.1];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self performSelector:#selector(setDefault) withObject:nil afterDelay:0.1];
}
- (void)setDefault
{
[NSOperationQueue.mainQueue addOperationWithBlock:^{ self.highlighted = NO; }];
}
the button reacts correctly when I press on it in a UITableViewCell, and my UITableView behaves normally as the delaysContentTouches isn't forced.
Here's Roman B's answer in Swift 2:
for view in tableView.subviews {
if view is UIScrollView {
(view as? UIScrollView)!.delaysContentTouches = false
break
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
for (id view in self.tableView.subviews)
{
// looking for a UITableViewWrapperView
if ([NSStringFromClass([view class]) isEqualToString:#"UITableViewWrapperView"])
{
// this test is necessary for safety and because a "UITableViewWrapperView" is NOT a UIScrollView in iOS7
if([view isKindOfClass:[UIScrollView class]])
{
// turn OFF delaysContentTouches in the hidden subview
UIScrollView *scroll = (UIScrollView *) view;
scroll.delaysContentTouches = NO;
}
break;
}
}
}
I was having similar issues with a text-only UIButton in a UITableViewCell not highlighting upon touch. What fixed it for me was changing the buttonType from Custom back to System.
Setting delaysContentTouches to NO did the trick for the image-only UIButton in the same UITableViewCell.
self.tableView.delaysContentTouches = NO;
This is a Swift version of Raphaël Pinto's answer above. Don't forget to upvote him too :)
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in self.highlighted = true }
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
super.touchesCancelled(touches, withEvent: event)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue()) {
self.setDefault()
}
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent: event)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue()) {
self.setDefault()
}
}
func setDefault() {
NSOperationQueue.mainQueue().addOperationWithBlock { () -> Void in self.highlighted = false }
}
Solution in Swift, iOS8 only (needs the extra work on each of the cells for iOS7):
//
// NoDelayTableView.swift
// DivineBiblePhone
//
// Created by Chris Hulbert on 30/03/2015.
// Copyright (c) 2015 Chris Hulbert. All rights reserved.
//
// This solves the delayed-tap issue on buttons on cells.
import UIKit
class NoDelayTableView: UITableView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delaysContentTouches = false
// This solves the iOS8 delayed-tap issue.
// http://stackoverflow.com/questions/19256996/uibutton-not-showing-highlight-on-tap-in-ios7
for view in subviews {
if let scroll = view as? UIScrollView {
scroll.delaysContentTouches = false
}
}
}
override func touchesShouldCancelInContentView(view: UIView!) -> Bool {
// So that if you tap and drag, it cancels the tap.
return true
}
}
To use, all you have to do is change the class to NoDelayTableView in your storyboard.
I can confirm that in iOS8, buttons placed inside a contentView in a cell now highlight instantly.
Slightly modified version of Chris Harrison's answer. Swift 2.3:
class HighlightButton: UIButton {
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
NSOperationQueue.mainQueue().addOperationWithBlock { _ in self.highlighted = true }
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
super.touchesCancelled(touches, withEvent: event)
setDefault()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
setDefault()
}
private func setDefault() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
NSOperationQueue.mainQueue().addOperationWithBlock { _ in self.highlighted = false }
}
}
}
The accepted answer did not work at some "taps" for me .
Finally I add the bellow code in a uibutton category(/subclass),and it works a hundred percent.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
self.backgroundColor = [UIColor greenColor];
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
self.backgroundColor = [UIColor clearColor];
} completion:^(BOOL finished)
{
}];
[super touchesBegan:touches withEvent:event];
}
I wrote a category extension on UITableViewCell to make this issue simple to address. It does basically the same thing as the accepted answer except I walk up the view hierarchy (as opposed to down) from the UITableViewCell contentView.
I considered a fully "automagic" solution that would make all cells added to a UITableView set their delaysContentTouches state to match the owning UITableView's delaysContentTouches state. To make this work I'd have to either swizzle UITableView, or require the developer to use a UITableView subclass. Not wanting to require either I settled on this solution which I feel is simpler and more flexible.
Category extension and sample harness here:
https://github.com/TomSwift/UITableViewCell-TS_delaysContentTouches
It's dead-simple to use:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// using static cells from storyboard...
UITableViewCell* cell = [super tableView: tableView cellForRowAtIndexPath: indexPath];
cell.ts_delaysContentTouches = NO;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
Here's the code for the category:
#interface UITableViewCell (TS_delaysContentTouches)
#property (nonatomic, assign) BOOL ts_delaysContentTouches;
#end
#implementation UITableViewCell (TS_delaysContentTouches)
- (UIScrollView*) ts_scrollView
{
id sv = self.contentView.superview;
while ( ![sv isKindOfClass: [UIScrollView class]] && sv != self )
{
sv = [sv superview];
}
return sv == self ? nil : sv;
}
- (void) setTs_delaysContentTouches:(BOOL)delaysContentTouches
{
[self willChangeValueForKey: #"ts_delaysContentTouches"];
[[self ts_scrollView] setDelaysContentTouches: delaysContentTouches];
[self didChangeValueForKey: #"ts_delaysContentTouches"];
}
- (BOOL) ts_delaysContentTouches
{
return [[self ts_scrollView] delaysContentTouches];
}
#end
Since objc is dynamic, and scrollView is the only class that responds to delaysContentTouches, this should work for both ios 7 and 8 (put it somewhere early in your tableViewController, like awakeFromNib):
for (id view in self.tableView.subviews)
{
if ([view respondsToSelector:#selector(delaysContentTouches)]) {
UIScrollView *scrollView = (UIScrollView *)view;
scrollView.delaysContentTouches = NO;
break;
}
}
You may also have to turn off "delaysContentTouches" in your storyboard or nib by selecting the table inside your viewController. BTW, this might not work on ios 7 if you're using a tableView inside a viewController, at least I couldn't get it to work.
That solution for me doesn't work, I fixed subclassing TableView and implementing these two methods
- (instancetype)initWithCoder:(NSCoder *)coder{
self = [super initWithCoder:coder];
if (self) {
for (id obj in self.subviews) {
if ([obj respondsToSelector:#selector(setDelaysContentTouches:)]){
[obj performSelector:#selector(setDelaysContentTouches:) withObject:NO];
}
}
}
return self;
}
- (BOOL)delaysContentTouches{
return NO;
}
Solution in Swift for iOS 7 and 8:
First I wrote a utility function:
class func classNameAsString(obj: AnyObject) -> String {
return _stdlib_getDemangledTypeName(obj).componentsSeparatedByString(".").last!
}
then I subclass UITableView and implement this:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
for view in self.subviews {
if (Utility.classNameAsString(view) == "UITableViewWrapperView") {
if view.isKindOfClass(UIScrollView) {
var scroll = (view as UIScrollView)
scroll.delaysContentTouches = false
}
break
}
}
}
I also subclass UITableViewCell and implement this:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
for view in self.subviews {
if (Utility.classNameAsString(view) == "UITableViewCellScrollView") {
if view.isKindOfClass(UIScrollView) {
var scroll = (view as UIScrollView)
scroll.delaysContentTouches = false
}
}
}
}
In my case the init(coder:) will run. Please put debug point in your init functions to know which init function will run, then using the code above to make it work.
Hope to help someone.
In Swift 3 this UIView extension can be used on the UITableViewCell. Preferably in the cellForRowAt method.
func removeTouchDelayForSubviews() {
for subview in subviews {
if let scrollView = subview as? UIScrollView {
scrollView.delaysContentTouches = false
} else {
subview.removeTouchDelayForSubviews()
}
}
}
Is it possible to deselect a segment of the UISegmentedControl by tapping it the second time? I am currently using a UILongPressGestureRecognizer object to setSelectedSegmentIndex to UISegmentedControlNoSegment. However, I'd rather have the selected segment deselect on the second tap.
Another method is to subclass the UISegmentedControl. For example:
#interface ToggleSegmentedControl : UISegmentedControl
{
NSUInteger selectedSegment;
}
#end
#implementation ToggleSegmentedControl
-(id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
selectedSegment = self.selectedSegmentIndex;
}
return self;
}
-(void)awakeFromNib
{
selectedSegment = self.selectedSegmentIndex;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if( selectedSegment == self.selectedSegmentIndex && selectedSegment!=-1)
{
self.selectedSegmentIndex= UISegmentedControlNoSegment;
selectedSegment=-1;
}
else
{
selectedSegment=self.selectedSegmentIndex;
}
}
#end
The above code may require further tweaking to suit individual specifications.
I have a tableview with 8 custom cells. in the 8th cell I added a scrollView with paging enabled so I can show page 1 and page 2 (or 3, 4... 10) without have a very high cell.
The problem is with the scrollView I can't use didSelectRowAtIndexPath because the cell is behind the scrollView so I'm trying to detect scrollView tap (not swipe).
I played with touchesBegan and touchesEnded but they are never called (I know touches work with UIView only, but maybe.....)
Any help is very appreciated.
Thanks,
Max
There is a trick Apple recommends to use in this case, in theirs WWDC 2014 session "Advanced scrollviews" (See Demo starting from 8:10):
[cell.contentView addSubview:_scrollView];
[_scrollView setUserInteractionEnabled:NO];
[cell.contentView addGestureRecognizer:_scrollView.panGestureRecognizer];
That's all what needs to be done, no need to override touchesBegan:, touchesMoved: and others.
I used solution based on overriding of touchesBegan:, touchesMoved:, touchesEnded: and touchesCancelled: previously, but sometimes it caused a weird behaviour: when select a certain cell, method -tableView:didSelectRowAtIndexPath: was called for cell with different indexPath.
Solution from Apple has no side effects so far and looks more elegant.
There is also an elegant resolution:
Create a SubClass from UIScrollView and override the following methods
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self superview]touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self superview]touchesMoved:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self superview]touchesCancelled:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[[self superview]touchesEnded:touches withEvent:event];
}
Passing every touch to the superview of the scroll view and then the didSelectRowAtIndexPath will be called.
Solved subclassing both uitableviewcell and uiscrollview.
It worked for my needs. Hope it can help.
Max
myScrollView.h
#import <UIKit/UIKit.h>
#interface myScrollView : UIScrollView {
}
#end
myScrollView.m
#import "myScrollView.h"
#implementation myScrollView
- (id)initWithFrame:(CGRect)frame {
return [super initWithFrame:frame];
}
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
NSLog(#"touch scroll");
// If not dragging, send event to next responder
if (!self.dragging)
[self.nextResponder touchesEnded: touches withEvent:event];
else
[super touchesEnded: touches withEvent: event];
}
myCell.h
#import <UIKit/UIKit.h>
#interface myCell : UITableViewCell {
}
#end
myCell.m
#import "myCell.h"
#implementation myCell
- (id)initWithFrame:(CGRect)frame {
return [super initWithFrame:frame];
}
- (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
NSLog(#"touch cell");
// If not dragging, send event to next responder
[super touchesEnded: touches withEvent: event];
}
RootViewController.h
#import <UIKit/UIKit.h>
#class myCell;
#class myScrollView;
#interface RootViewController : UITableViewController {
myCell *cell;
myScrollView *scrollView;
}
#end
RootViewController.m
#pragma mark -
#pragma mark Table view data source
// Customize the number of sections in the table view.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 3;
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
// my custom cell
cell = [[myCell alloc] init];
if (cell == nil) {
cell = [[[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// the custom scroll view
scrollView = [[myScrollView alloc] initWithFrame:cell.frame];
scrollView.contentSize = CGSizeMake(640, 40);
[cell.contentView addSubview:scrollView];
//something to add in scrollView
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 150, 20)];
label.text = #"some text";
[scrollView addSubview:label];
// Configure the cell.
return cell;
}
The selected answer is correct, but I updated the code based on a bug I was getting.
In the subclassed scroll view add the following code.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.dragging) {
[super touchesMoved:touches withEvent:event];
} else {
if ([self.delegate isKindOfClass:[UITableViewCell class]]) {
[(UITableViewCell *)self.delegate touchesCancelled:touches withEvent:event];
}
[self.superview touchesMoved:touches withEvent:event];
}
}
If your self.delegate is not the UITableViewCell, than replace that property with a property to your cell.
The cell needs to retrieve the cancel touch event during movement to prevent the undesired results. It can be easily reproducible as follows.
Highlight the cell (assuming the scroll view is over the whole cell, if not highlight the scroll view)
While the cell is highlighted, drag the table view
Select any other cell and now the previously highlighted cell will retrieve the didSelectCell state
Another point to mention is that order matters! If the self.delegate is not called before the self.superview then the highlighted state wont happen.
I found the simplest solution for my needs:
subclass UIScrollView touchesEnded method and post a notification.
In the UITableview add an observer in viewdidAppear (remove it in viewdiddisappear) to call a function that call tableview didSelectRowForIndexPath.
Something like this (swift version)
// myScrollView.swift
import UIKit
class myScrollView: UIScrollView {
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
NSNotificationCenter.defaultCenter().postNotificationName("selectTVRow", object: nil)
}
}
In your tableView:
// ItemsList.swift
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "selectFourthRow", name: "selectTVRow", object: nil)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "selectfourthrow", object: nil)
}
func selectFourthRow() {
let rowToSelect:NSIndexPath = NSIndexPath(forRow: 4, inSection: 0);
self.tableView(self.tableView, didSelectRowAtIndexPath: rowToSelect);
}
/*
.... rest of your tableview Datasource and Delegate methods...
numberOfSectionsInTableView, numberOfRowsInSection, cellForRowAtIndexPath
*/
How do I capture touch events such as - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event without subclassing a UIView nor using UIViewControllers.
What happens is that I have a simple UIView created programmatically and I need to detect basic tap events.
If you are writing your app for iOS 4, use UIGestureRecognizer. You can then do what you want. Recognize gestures without subclassing.
Otherwise, subclassing is the way to go.
There's just no reason not to. If you subclass and add nothing it's just a UIView called by another name. All you are doing is intercepting those functions that you are interested in. Don't forget you can do [super touchesBegan:touches] inside your subclass' touchesBegan if you don't want to stop responders up the chain from getting those events too.
I don't why you don't want to use the normal method of subclassing a UIView to capture touch events, but if you really need to do something weird or sneaky, you can capture all events (including touch events) before they get sent down the view hierarchy by trapping/handling the sendEvent: method at the UIWindow level.
CustomGestureRecognizer.h
#import <UIKit/UIKit.h>
#interface CustomGestureRecognizer : UIGestureRecognizer
{
}
- (id)initWithTarget:(id)target;
#end
CustomGestureRecognizer.mm
#import "CustomGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface CustomGestureRecognizer()
{
}
#property (nonatomic, assign) id target;
#end
#implementation CustomGestureRecognizer
- (id)initWithTarget:(id)target
{
if (self = [super initWithTarget:target action:Nil]) {
self.target = target;
}
return self;
}
- (void)reset
{
[super reset];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self.target touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
[self.target touchesMoved:touches withEvent:event];
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent: event];
[self.target touchesEnded:touches withEvent:event];
}
#end
Usage:
CustomGestureRecognizer *customGestureRecognizer = [[CustomGestureRecognizer alloc] initWithTarget:self];
[glView addGestureRecognizer:customGestureRecognizer];
Is there a bug in the 3.0 SDK that disables real-time zooming and intercepting the zoom-in gesture for the MKMapView? I have some real simple code so I can detect tap events, but there are two problems:
zoom-in gesture is always interpreted as a zoom-out
none of the zoom gestures update the Map's view in realtime.
In hitTest, if I return the "map" view, the MKMapView functionality works great, but I don't get the opportunity to intercept the events.
Any ideas?
MyMapView.h:
#interface MyMapView : MKMapView
{
UIView *map;
}
MyMapView.m:
- (id)initWithFrame:(CGRect)frame
{
if (![super initWithFrame:frame])
return nil;
self.multipleTouchEnabled = true;
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(#"Hit Test");
map = [super hitTest:point withEvent:event];
return self;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"%s", __FUNCTION__);
[map touchesCancelled:touches withEvent:event];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event
{
NSLog(#"%s", __FUNCTION__);
[map touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
NSLog(#"%s, %x", __FUNCTION__, mViewTouched);
[map touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
NSLog(#"%s, %x", __FUNCTION__, mViewTouched);
[map touchesEnded:touches withEvent:event];
}
The best way I have found to achieve this is with a Gesture Recognizer. It's unclear if you want to recognize zoom events yourself or just detect when the user is zooming/panning.
I don't need to detect a map pan or zoom--i just care if the user has put a finger down anywhere in the MKMapView. If you need to detect zooming or panning with 100% recall and precision, it might be more complicated than this. What we really need is an open source implementation of MKMapView so we can add this to the delegate, among many other features.
Here's what I do: Implement a gesture recognizer that cannot be prevented and that cannot prevent other gesture recognizers. Add it to the map view and deal with the relevant touch events as you desire.
How to detect any tap inside an MKMapView (sans tricks)
WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
self.lockedOnUserLocation = NO;
};
[mapView addGestureRecognizer:tapInterceptor];
WildcardGestureRecognizer.h
//
// WildcardGestureRecognizer.h
// Copyright 2010 Floatopian LLC. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void (^TouchesEventBlock)(NSSet * touches, UIEvent * event);
#interface WildcardGestureRecognizer : UIGestureRecognizer {
TouchesEventBlock touchesBeganCallback;
}
#property(copy) TouchesEventBlock touchesBeganCallback;
#end
WildcardGestureRecognizer.m
//
// WildcardGestureRecognizer.m
// Created by Raymond Daly on 10/31/10.
// Copyright 2010 Floatopian LLC. All rights reserved.
//
#import "WildcardGestureRecognizer.h"
#implementation WildcardGestureRecognizer
#synthesize touchesBeganCallback;
-(id) init{
if (self = [super init])
{
self.cancelsTouchesInView = NO;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (touchesBeganCallback)
touchesBeganCallback(touches, event);
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void)reset
{
}
- (void)ignoreTouch:(UITouch *)touch forEvent:(UIEvent *)event
{
}
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
return NO;
}
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
return NO;
}
#end
I had the same problem - I wanted to draw map scales on top of Map View. In order to do it I had to intercept the touch events, and then send them back to the Map View. Unfortunately, when the MKMapView isn't the original receiver of the events, some smooth panning and zooming animations are not working any more.
However I have found a solution to this problem - a bit hacky but works:
1. I have put my MapScales UIView on top of MKMapView, and turned off receiving events in it, so that underlying MKMapView received the events by default.
2. I have subclassed UIWindow with MyMainWindow class and in it I have overriden the method:
- (void) sendEvent:(UIEvent*)event {
[super sendEvent:event];
[self send_the_event_also_to_my_MapScales_component_with_use_of_listener_design_pattern];
}
I have made the main window of my application an instasnce of MyMainWindow.
In this way my MapScales component receives and can react to all the touch events, and at the same time it is not spoiling the underlying MKMapView :)
#import <UIKit/UIKit.h>
#import "UIViewTouch.h"
#import <MapKit/MapKit.h>
#interface MapTouchAppDelegate : NSObject <UIApplicationDelegate>
{
UIViewTouch *viewTouch;
MKMapView *mapView;
UIWindow *window;
}
#property (nonatomic, retain) IBOutlet UIWindow *window;
#property (nonatomic, retain) UIViewTouch *viewTouch;
#property (nonatomic, retain) MKMapView *mapView;
#end
#import "MapTouchAppDelegate.h"
#implementation MapTouchAppDelegate
#synthesize window;
#synthesize viewTouch;
#synthesize mapView;
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
//We create a view wich will catch Events as they occured and Log them in the Console
viewTouch = [[UIViewTouch alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
//Next we create the MKMapView object, which will be added as a subview of viewTouch
mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
[viewTouch addSubview:mapView];
//And we display everything!
[window addSubview:viewTouch];
// Override point for customization after application launch
[window makeKeyAndVisible];
}
- (void)dealloc {
[window release];
[super dealloc];
}
#import <UIKit/UIKit.h>
#interface UIViewTouch : UIView
{
UIView *viewTouched;
}
#property (nonatomic, retain) UIView * viewTouched;
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
#end
#import "UIViewTouch.h"
#implementation UIViewTouch
#synthesize viewTouched;
//The basic idea here is to intercept the view which is sent back as the firstresponder in hitTest.
//We keep it preciously in the property viewTouched and we return our view as the firstresponder.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(#"Hit Test");
self.multipleTouchEnabled = true;
viewTouched = [super hitTest:point withEvent:event];
return self;
}
//Then, when an event is fired, we log this one and then send it back to the viewTouched we kept, and voilà!!! :)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touch Began");
[viewTouched touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touch Moved");
[viewTouched touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touch Ended");
[viewTouched touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touch Cancelled");
}
#end
This code will detect touches as well as zooming.
Try
[super touchesEnded:touches withEvent:event];
instead of
[map touchesEnded:touches withEvent:event];
And apply this idea to all of the touch event methods. That way, the touches should travel down the responder chain, and peace will be restored.
To extend up on #chomasek's answer, in order to only process those touches for the map view, I do the following:
Give the map view a tag, like 99
When I get touch, traverse up the view hierarchy of the receiving view, looking for a view with the above tag.
Here is the code:
// this is in the view controller containing the map view
// kICTagForMapView is just a constant
_mapView.tag = kICTagForMapView;
Then in sendEvent:
// this does into the UIWindow subclass
BOOL isMapView = NO;
UIView* superView = touch.view.superview;
while(superView)
{
//Debug(#"superView = %#", superView);
if (superView.tag == kICTagForMapView)
{
isMapView = YES;
break;
}
superView = superView.superview;
}
if (isMapView == NO) return;
// do stuff here
MKMapView does not respond to the touch methods listed above...