Mar 30

Nov 4th 2010 note: This is still a valid and working method, but since iOS4 there’s an easier method to detect swipes using Apple’s UISwipeGestureRecognizer method

I needed proper swipe detection in a UITableViewCell, I’ve seen some online examples but in practice they responded fairly poorly.
I found a proper example in the iPhoneIncubator example which is freely available, and does a proper detection of a swipe through a NSNotificationCenter.

Personally I didn’t need the NSNotificationCenter, so I modified the code to fit into a UITableViewCell subclass.
First we define the horizontal swipe distance at which it gets registered as being an actual swipe, the allowed vertical scrolling margin for people with shaky fingers and some generic CGPoint math operations (these are a 1on1 copied from the original source, I take no credit for this)

#define SWIPE_DRAG_HORIZ_MIN 40
#define SWIPE_DRAG_VERT_MAX 40

#pragma mark -
#pragma mark Helper functions for generic math operations on CGPoints

CGFloat CGPointDot(CGPoint a,CGPoint b) {
        return a.x*b.x+a.y*b.y;
}
CGFloat CGPointLen(CGPoint a) {
        return sqrtf(a.x*a.x+a.y*a.y);
}
CGPoint CGPointSub(CGPoint a,CGPoint b) {
        CGPoint c = {a.x-b.x,a.y-b.y};
        return c;
}
CGFloat CGPointDist(CGPoint a,CGPoint b) {
        CGPoint c = CGPointSub(a,b);
        return CGPointLen(c);
}
CGPoint CGPointNorm(CGPoint a) {
        CGFloat m = sqrtf(a.x*a.x+a.y*a.y);
        CGPoint c;
        c.x = a.x/m;
        c.y = a.y/m;
        return c;
}

Then we begin the touchesBegan function, to detect and possibly override a touch in the tablecell

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
        NSArray *allTouches = [[event allTouches] allObjects];
        UITouch *touch = [[event allTouches] anyObject];
       
        if (touch.phase==UITouchPhaseBegan) {
                startTouchPosition1 = [touch locationInView:self];
                startTouchTime = touch.timestamp;

                if ([[event allTouches] count] > 1) {
                        startTouchPosition2 = [[allTouches objectAtIndex:1] locationInView:self];
                        previousTouchPosition1 = startTouchPosition1;
                        previousTouchPosition2 = startTouchPosition2;
                }
        }       
        [super touchesBegan:touches withEvent:event];
}

Next we do the actual detection of the swipe, and define which action to take, note that there’s a set of if statements in there which sets and checks various variables. These are simply integers I defined in the header file (booleans would be fine too). The localSwipeActive variable defines if the swipe is being performed, this will prevent a single swipe being registered as multiple swipes.

The cellSwiped is a variable I use to see which action to take (either invoke the setSwipeView or the resetSwipeView function) based on the cell’s swipe status.

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
        UITouch *touch = [[event allTouches] anyObject];
        CGPoint currentTouchPosition = [touch locationInView:self];
        NSLog(@"%d %f %d %f time: %g",fabsf(startTouchPosition1.x – currentTouchPosition.x) >= SWIPE_DRAG_HORIZ_MIN ? 1 : 0,
                 fabsf(startTouchPosition1.y – currentTouchPosition.y),
                 fabsf(startTouchPosition1.x – currentTouchPosition.x) > fabsf(startTouchPosition1.y – currentTouchPosition.y)  ? 1 : 0, touch.timestamp – startTouchTime, touch.timestamp – startTouchTime);
        if (fabsf(startTouchPosition1.x – currentTouchPosition.x) >= SWIPE_DRAG_HORIZ_MIN &&
                fabsf(startTouchPosition1.y – currentTouchPosition.y) <= SWIPE_DRAG_VERT_MAX &&
                fabsf(startTouchPosition1.x – currentTouchPosition.x) > fabsf(startTouchPosition1.y – currentTouchPosition.y) &&
                touch.timestamp – startTouchTime < .7 && self.localSwipeActive == 0
                ) {
                // It appears to be a swipe.
                if (startTouchPosition1.x < currentTouchPosition.x) {
                        NSLog(@"swipe right");
                        self.localSwipeActive = 1; // set the swipe status to active so this function doesn’t get called again within the same swipe
                        if (self.cellSwiped == 0) {
                                self.setSwipeView;
                                self.cellSwiped = 1;
                                } else {
                                        if (self.cellSwiped == 1) {
                                                self.resetSwipeView;
                                                self.cellSwiped = 0;
                                        }       
                                }
                        } else {
                                NSLog(@"swipe left");
                                self.localSwipeActive = 1;
                                if (self.cellSwiped == 0) {
                                        self.setSwipeView;
                                        self.cellSwiped = 1;
                                }  else {
                                        if (self.cellSwiped == 1) {
                                                self.resetSwipeView;
                                                self.cellSwiped = 0;
                                        }       
                                }
                        }
                }
        startTouchPosition1 = CGPointMake(-1, -1);
}

And at the end we define that the swipe is over:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
        self.localSwipeActive = 0; // Reset the swipeActive status now that we finished the swipe
        [super touchesEnded:touches withEvent:event];
}

There you have it, a proper swipe detection in a UITableViewCell subclass

Mar 30

Yesterday I ran into an issue where a UITableView would not display until some remote data was loaded into a local array, even though I was loading this data in a seperate thread.

- (void)viewDidAppear:(BOOL)animated {
        [self doInitialLoad];
}
- (void)doInitialLoad {
        [self showActivityViewer]; // shows a screen activity indicator overlay
        [self laadJSONTabel]; // loads the actual data into the tableview
        [self.recentTable reloadData]; // reloads the tableview so data shows up
        [self hideActivityViewer]; // hides the activity indicator overlay
}

It turns out that visual stuff gets queued to the end run of the loop, this resulted in a very sloppy interface which would react very slow to a change to another UIViewController due to remote data being downloaded first.

An easy fix for this, is using a performSelector on the function with a 0.0 delay, that will give it a zero delay but move loading the data to the next run-loop so all other stuff gets properly executed first. This results in an interface which feels a lot snappier and a proper UIActivityIndicatorView being displayed on screen while the data is loading:

- (void)viewWillAppear:(BOOL)animated {
        [self showActivityViewer];
}
- (void)viewDidAppear:(BOOL)animated {
        [self performSelector:@selector(doInitialLoad) withObject:NULL afterDelay:0.0];

}
- (void)doInitialLoad {
        [self laadJSONTabel];
        [self.recentTable reloadData];
        [self hideActivityViewer];
}

Tagged with:
preload preload preload