How to make table scrolling work: UIControl within UITableView within UIControl

744 views Asked by At

Here's the setup: I have UIControls in table cells that allow sliding from right to left for a delete function (like Clear)

The table cells live inside a UITableView.

The TableView lives inside another UIControl that allows swiping from left to right or right to left in order to change days. When this happens a new TableView gets created to the right or left of the main one, and the new one is pulled in from left or right until a threshold is met and an animation takes over and then replaces the old with the new.

In some conditions all of these interactions actually work correctly. The issue is that after a couple of interactions (table slides seem to be problematic) it becomes difficult / impossible to scroll the table.

Here is the code for the TableViewSlider (the top level UIControl that contains the TableViews). Any ideas would be greatly appreciated!

 @implementation OSETableViewSlider


- (void) initialize {
    self.backgroundColor = [UIColor backgroundFlatColor];
    self.mainTableView = [self createUITableView];
    self.mainTableView.frame = CGRectMake(0,0, self.frame.size.width, self.frame.size.height);
    self.mainTableView.hidden = NO;
    [self addSubview:self.mainTableView];
    self.transitionInProgress = NO;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initialize];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if(self) {
        [self initialize];
    }
    return self;
}

- (UITableView *)createUITableView {
    UITableView *newTable = [[UITableView alloc] init];
    newTable.hidden = YES;
    newTable.separatorStyle = UITableViewCellSeparatorStyleNone;
    newTable.scrollEnabled = YES;
    newTable.bounces = YES;
    newTable.dataSource = self;
    newTable.delegate = self;
    newTable.backgroundColor = [UIColor backgroundFlatColor];

    return newTable;
}

- (void)setFrame:(CGRect)frame {
    super.frame = frame;
    self.mainTableView.frame = CGRectMake(0,0, frame.size.width, frame.size.height);
}

- (void)transitionToDate:(NSDate *)date fromRight:(BOOL)rightToLeft {
    [self.viewController beginTransitionToDate:date];

    self.transitionTableView = [self createUITableView];
    self.transitionTableView.frame = CGRectMake( rightToLeft ? self.frame.size.width : (-1 * self.frame.size.width), 0,
                                                 self.frame.size.width, self.frame.size.height );
    self.transitionTableView.hidden = NO;
    [self addSubview:self.transitionTableView];

    self.transitionInProgress = YES;

    [UIView animateWithDuration:0.2f animations:^{
        self.transitionTableView.frame = CGRectMake(0,0, self.frame.size.width, self.frame.size.height);

        self.mainTableView.frame = CGRectMake( rightToLeft ? (-1 * self.frame.size.width) : self.frame.size.width, 0,
                                              self.frame.size.width, self.frame.size.height);
    } completion:^(BOOL completed){
        [self.viewController endTransitionDidChange:YES];
        [self.mainTableView removeFromSuperview];
        self.mainTableView = self.transitionTableView;

        self.transitionInProgress = NO;

    }];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.viewController activityCountForTransition:(tableView!=self.mainTableView)];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self.viewController cellNumber:indexPath.item forTransition:(tableView!=self.mainTableView)];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self.viewController cellNumber:indexPath.item forTransition:(tableView!=self.mainTableView)].frame.size.height;
}

- (void)layoutSubviews {
    if(!self.transitionInProgress) {
        [self.mainTableView setFrame:CGRectMake(0,0, self.frame.size.width, self.frame.size.height)];
    }
}

- (BOOL) beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"begin track");

    self.locationInView = [touch locationInView:self.superview];
    self.fingerTracking = YES;
    self.fingerMoved = NO;
    self.transitionTableView = nil;

    return YES;
}

- (CGFloat) calcOffsetForTouch:(UITouch *)touch {
    CGFloat fingerOffset = [touch locationInView:self.superview].x - self.locationInView.x;
    return fingerOffset + self.startingOffset;
}

- (BOOL) continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    self.fingerMoved = YES;
    CGFloat offset = [self calcOffsetForTouch:touch];

    //NSLog(@"offset is: %f    flarb: %f", offset, fabs(offset));

    if(offset < 0 && (!self.transitioningLeft || [OSEUtils isNull:self.transitionTableView])) {
        [self.viewController beginTransitionToDate:[OSEUtils daysOffset:1 fromDate:self.viewController.selectedDate withTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]];
        [self.transitionTableView removeFromSuperview];
        self.transitionTableView = [self createUITableView];
        self.transitioningLeft = YES;
        NSLog(@"going left");
        [self addSubview:self.transitionTableView];
        [self.transitionTableView reloadData];
    }
    if(offset > 0 && (self.transitioningLeft || [OSEUtils isNull:self.transitionTableView])) {
        [self.viewController beginTransitionToDate:[OSEUtils daysOffset:-1 fromDate:self.viewController.selectedDate withTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]];
        [self.transitionTableView removeFromSuperview];
        self.transitionTableView = [self createUITableView];
        self.transitioningLeft = NO;
        NSLog(@"going right");
        [self addSubview:self.transitionTableView];
        [self.transitionTableView reloadData];
    }

    CGFloat mult = self.transitioningLeft ? 1 : -1;

    if(fabs(offset) > (self.frame.size.width / 3.0f)) {
        if(self.transitioningLeft) {
            offset = -1 * self.frame.size.width;
        } else {
            offset = self.frame.size.width;
        }

        [UIView animateWithDuration:0.2f animations:^{
            self.mainTableView.frame = CGRectMake(offset, 0, self.frame.size.width, self.frame.size.height);
            self.transitionTableView.frame = CGRectMake(offset + (mult * self.frame.size.width), 0,
                                                        self.frame.size.width, self.frame.size.height);
        } completion:^(BOOL finished) {
            [self.mainTableView removeFromSuperview];

            if(self.transitioningLeft) {
                [self.viewController.daySelector jumpToNext];
            } else {
                [self.viewController.daySelector jumpToPrevious];
            }

            self.mainTableView = self.transitionTableView;
            self.transitionTableView = nil;

            [self.viewController endTransitionDidChange:YES];
        }];

        return NO;
    }

    self.mainTableView.frame = CGRectMake(offset, 0, self.frame.size.width, self.frame.size.height);
    self.transitionTableView.frame = CGRectMake(offset + (mult * self.frame.size.width), 0, self.frame.size.width, self.frame.size.height);
    self.transitionTableView.hidden = NO;

    return YES;
}



- (void) endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"end tableslide track");
    [UIView animateWithDuration:0.2f animations:^{
        self.mainTableView.frame = CGRectMake(0,0,self.frame.size.width, self.frame.size.height);
        self.transitionTableView.frame = CGRectMake(((self.transitioningLeft ? 1 : -1 ) * self.frame.size.width), 0, self.frame.size.width, self.frame.size.height);
    } completion:^(BOOL completion){
        [self.transitionTableView removeFromSuperview];
        self.transitionTableView = nil;
        [self.viewController endTransitionDidChange:NO];
    }];



    self.fingerTracking = NO;
}


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *tableHitTest = [self.mainTableView hitTest:point withEvent:event];
    if([tableHitTest isKindOfClass:[OSECellPanelControl class]]) {
        return tableHitTest;
    } else {
        return self;
    }
}

@end
4

There are 4 answers

2
Johnathon Sullinger On

Why are you creating a new UITableView when the user swipes left or right? It would be much more efficient to use the built-in deleteRowsAtIndexPath: and insertRowsAtIndexPath: methods to animate the rows sliding in and out without having to slide in and out entire UITableView's.

You can take a look at this for a reference on animating the tableview content changes as you swipe across dates.

I don't know about your current set up, but when I tried this exact same thing with swipeable UITableViewCell's and it's superview being swipe able, I had issues with getting the interaction that I wanted. If this is contributing to your problem, you can look at detecting where the user swipes. If the user swipes within a specific offset from the left and right edge of the UITableViewCell, then it swipes the cell, anything outside of that offset (center of the cell) would have the UITableView itself swiped. You would then animate out the deleting and insertion of the new rows.

Also, by re-instancing a new UITableView each time, you are forcing your UITableViewCell's to be re-instanced each time as well. By using the delete and insert methods, you can continue to use your existing cells via the dequeueReusableCellWithIdentifier method. Each time you re-instance the UITableView, your cells are set to nil and discarded requiring the new tableview to create complete new instances of your cells. This is extremely inefficient.

0
Rufel On

Your idea share a few similarities with how the iOS7 unlock screen and calendar is built. In the unlock screen, you can swipe on notifications, move the notification list up and down, and swipe to unlock. In the calendar app, you can swipe to change the current day, or swipe the header to change the week. Thus I recommend watching the WWDC 2013 Session video #217 "Exploring Scroll Views on iOS 7" that explain and demonstrate how they have done it, and how to replicate the functionality.

Basically, what you could do (and what they did for the unlock screen and calendar screen) is: Either (A) nest UIScrollViews in a UITableView OR (B) use a swipe/pan gesture delegate on every cells, and (C) nest that UITableView along with every other UITableViews representing days in a UIScrollView with paging control enabled.

(A) will allow you to expose a delete button in cells. You could either put the delete button under the UIScrollView that sits in the cell in a way that sliding the content uncover it.

(B) will allow you to track a gesture in cells, cancel it if required, and play with the cell's contentView to display a delete button.

(C) will allow you to swipe from one UITableView to another quite easily, and with paging enable you will get a free "snap into place or bounce back" animation PLUS you can easily reuse two UITableViews instead of allocating one per day.

Personally, I would go with (B) and (C).

0
Wain On

You're getting in deep with touch tracking and hit detection. Use a higher level API instead - gesture recognisers. With gestures you can set precedence (this one has to fail before this one can begin).

So, you want a couple of custom gestures on the container view (which no longer needs to be a control). These fail if the initial touch point isn't at the edge of the view (within some bound). These trigger the overall table view change.

And the table cells have swipe gestures that each require the custom ones to fail. These trigger your cell deletion logic.

0
Fiid On

Turns out there was a problem with my hitTest method. Replacing [self.mainTableView hitTest...] with [super hitTest...] solved my problem. I think I was messing up some coordinates that was causing this method to return nil when it should have returned a view. I'm not 100% sure exactly what is wrong with it, but it does work better now.