I want to detect if mytable view has been scrolled, I tried all touch events like this one:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
//my code
}
but it seems that all touch events don't response to scroll but they response only when cells are touched, moved,...etc
Is there a way to detect scroll event of UITableView ?
If you implement the UITableViewDelegate protocol, you can also implement one of the UIScrollViewDelegate methods:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
or
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
For example, if you have a property called tableView:
// ... setting up the table view here ...
self.tableView.delegate = self;
// ...
// Somewhere in your implementation file:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"Will begin dragging");
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"Did Scroll");
}
This is because UITableViewDelegate conforms to UIScrollViewDelegate, as can be seen in the documentation or in the header file.
If you have more than one table views as asked by Solidus, you can cast the scrollview from the callback to tableview as UITableView is derived from UIScrollView and then compare with the tableviews to find the source tableview.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
UITableView* fromTableView = (UITableView*) scrollView;
UITableView* targetTableView = nil;
if (fromTableView == self.leftTable) {
targetTableView = self.leftTable;
} else {
targetTableView = self.rightTable;
}
...
}
These are the methods from UITableViewDelegate using Swift to detect when an UITableView will scroll or did scroll:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
Related
the problem is I make a new UIScrollView at Xib and then link the delegate to owner..
some how the delegate doesnt called.. any clue? or simple example for UIScrollView
sorry I am a new ios developer and thank in advance..
the code :
at header
#interface stageViewController : UIViewController<UIScrollViewDelegate>
at .m
#pragma mark drag delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"scrollViewDidScroll");
} // any offset changes
- (void)scrollViewDidZoom:(UIScrollView *)scrollView{
NSLog(#"scrollViewDidZoom");
}
// called on start of dragging (may require some time and or distance to move)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
NSLog(#"scrollViewWillBeginDragging");
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSLog(#"scrollViewWillEndDragging");
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
NSLog(#"scrollViewWillBeginDecelerating");
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSLog(#"scrollViewDidEndDecelerating");
}
Write scrollView.delegate = self; If you have made it pragmatically.
And if you have your ScrollView in XIB then ctrl+drag from your scrollView to your File's Owner and then set its delegate.
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
*/
I am using the following two UIScrollView delegate methods to call another method in my UIViewController:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// do something
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
// do something
}
I've tried various different ways of calling //do something but either end up with both not being called, or both delegate methods being called, calling //do something twice in certain situations. For example:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if(![scrollView isDecelerating] && ![scrollView isDragging]){
//do something
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
if(!decelerate){
//do something
}
}
So with the above if I scroll and let it slow to a stop, it calls scrollViewDidEndDecelerating:, but if I scroll and stop it with a tap it calls both scrollViewDidEndDragging: and scrollViewDidEndDecelerating:
I want it to call one or the other... is there something I can do with scrollViewDidEndDecelerating: and the scrollView object to stop this double method call?
The scroll view delegate does its job. You can not prevent it from happening. But with a simple logic you can achieve what you are trying to do.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
dragged = YES;
// do something
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (!dragged) {
// do something
}
dragged = NO;
}
// very simple
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if(!decelerate){
// Do something
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// Do something
}
This seems to work. Close to EmptyStack's answer
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (!dragged) {
//do something
}
dragged = NO;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
if(!decelerate){
dragged = YES;
//do something
} else {
dragged = NO;
}
}
how to detect touch event for table cells
i tried this
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//<my stuff>
[super touchesBegan:touches withEvent:event];
}
but its not working actuallly i have aUIimage view in table cell and i want to chnage imgae based on tap so my touch event is not working for that cell
If you want to detect a touch on the UITableViewCell you don't really need to detect touch events. In your UITableViewController subclass, you need to implement the following delegate method:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
Then you modify the image of the table cell for the selected index path.
You probably need to set myImageView.userInteractionEnabled = YES;.
In one of my projects I needed any tap on the tableView to dismiss the keyboard so the underlying tableView would show. Since a UITableView is really a UIScrollView, it will respond to the scrollView delegate methods. Using these 2 methods will dismiss if either the user taps on a cell or scrolls the tableView at all:
IMPORTANT: Make sure you implement the UIScrollViewDelegate in your .h file as well as the UITableViewDelegate and UITableViewDataSourceDelegate!!!
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//remove keyboard if table row is clicked
if ([self.firstName isFirstResponder] || [self.lastName isFirstResponder]) {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[self.firstName resignFirstResponder];
[self.lastName resignFirstResponder];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//remove keyboard if table scrolls
if ([self.firstName isFirstResponder] || [self.lastName isFirstResponder]) {
[self.firstName resignFirstResponder];
[self.lastName resignFirstResponder];
}
}
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.