iOS Chart control bug - swallowing all gestures


#1

Hi there,

Imagine the scenario where you have a vertically scrollable tableview that contains several cells. One or more of the cells contains a chart. Of course the user wants to be able to scroll up and down anywhere in the table.  Unfortunately, if the user tries to do a scrolling gesture and happens to tap on the chart control, nothing will happen.  (the gesture is swallowed up)

If the chart has all panning and zooming disabled, the above is still true for me.  Would love to hear any suggestions on how to work around that.  Ultimately, I want to enable horizonal panning in the chart, but have vertical panning gestures passed up to the parent so that the control behaves as users would expect.

Is this a known issue or am  I missing something? Any suggested workarounds?

Thanks!


#2
I found a very much related thread:

Propagate gesture events to containing controller (ShinobiCharts)

I see that Jan Akerman posted this work-around if your chart has no scrolling at all:
>>

chart.gesturePanType = SChartGesturePanTypeNone;

>>
 
However, this does not help me with my scenario of I want horizontal pan gestures to be handled by the chart, and vertical pan gestures to be handled by the parent (table/scrollview).
 
It’s not clear to me if a fix for this is being considered, given one of Jan’s responses.
 
>>

Your correct about the chart swallowing the gestures. It isn’t possible to have the chart responding to panning gestures and to also have your scroll view to receive those gestures,  it doesn’t really make sense from a usability point of view. This is similar to how a scroll view nested within a scrollview would work.

>>

Jay’s response does seem like it’d be the trick to solving this issue in Shinobi’s codebase. That is, have it pass on horizontal and/or vertical gestures based on which axis (ss) have enablePanningGesture set.
 
>>

Actually it is possible in iOS to have a pan gesture recognizer in an inner view (the chart) and still work for the outer-view (the scroll view) - only if the inner one is implemented correctly.

So shinobi guys - here’s how to do it…

  1. Set a UIGestureRecognizerDelegate for the pan GR and implement this:
  • (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer*)panGR

{

    CGPoint        trans = [panGR translationInView:self.view];

    CGPoint        startPos = CGPointSubtract([panGR locationInView:self.view], trans);

    // The GR is ready to start, but it is confirming with you first - 

    // There are situations that you should return NO:

    // - If the start-point not inside an interactive area (e.g. the donut part on a donut chart)

    // - If the chart is already at its limit in the direction of panning (see trans)

    // etc…

 }

>>
 
Is a fix for this being considered? 
 

Thanks!

#3

Just thinking more about this. Less than ideal, but perhaps I could subclass chart, explicitly disable gestures on the chart, implement my own gesture recognizers code in that subclass, and explicitly call 

- (BOOL)panByValue:(double)value withAnimation:(BOOL)animation withBounceLimits:(BOOL)panWithBouncing andRedraw:(BOOL)redraw;
  
 as needed.  Any other suggetions?

#4

Would it be possible to get feedback wether or not you guys agree that the chart SDK shouldn’t be swallowing up vertical pan gestures if only horizontal panning is enabled in the chart? 

I’m currently down the rabbit hole of iterating through until i find the SChartCanvasOverlay view and direclty messing with your gesture recognizers. I’m making progress, but not something I’m confortable with.   If I can’t get around this, this might be a showstopper for me.   I’m happy to discuss on phone or skype if need-be.

Cheers


#5

Okay, I have a much less frightening work-around for the issue of the chart accepting/swallowing all pan gestures, even if only one axis has panning enabled. One issues remains though.

Here’s what I did.

  1. subclass the shinboi chart

  2. set the chart’s panType to SChartGesturePanTypeNone

  3. add my own gesture recognizer to refuse pan gestures if mostly vertical (since I only have x- axis panning enabled).  if mostly horizontal accept the gesture and have my custom chart class call 

    • (BOOL)panByValue:(double)value withAnimation:(BOOL)animation withBounceLimits:(BOOL)panWithBouncing;

    Per header file:
    @param panWithBouncing - whether or not the axisRange should ‘bounce’ if it strays outside of the permissable range.

I am not getting any bounce back at all. It just scrolls beyond the valid range and stops a couple centimeters later. Has anyone else been able to use this panByValue with bouncing set successfully?  If not, is a fix for this in the backlog?

Cheers


#6

Hi TomShinobi,

Thanks so much for sharing your progress with this issue. It really is great to see someone sharing their progress to help others.

I’m not quite sure why you are having issues with the bouncing whilst using that method. I’ll raise a task to look into that, but for now, I think I can help you another way.

I’ve actually had success following Jay’s advice in that forum thread. However, to do this, you need to muck about with the internals of the chart, like so:

  1. Loop around the charts internal gesture recognizers and when you find a pan gesture recognizer, change its delegate.

    for (UIGestureRecognizer *recognizer in _chart.canvas.overlay.gestureRecognizers)
    {
    if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]])
    {
    recognizer.delegate = self;
    }
    }

Then, following Jay’s advice and probably exactly as you have done, implement the gesture recognizer’s gestureRecognizerShouldBegin: method like so:

-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer{
    CGPoint trans = [gestureRecognizer translationInView:gestureRecognizer.view];
    
   // If we have moved more Y than we have X, ignore the gesture recognizers and let it propagate up to anyone else who cares about it!
   if (trans.x < trans.y) return NO;
 
    return YES;
}

Clearly, modifying the internals of the chart is never a great solution as you are essentially coupled with our implementation, which has the potential to change. Investigate into the progress with this issue, and if no progress has been made I will raise this issue again and ensure that it is chased up. We’ll then have a think and make a decision about how we can best support this behaviour on our API.

Hopefully for now, the above advice will help.

Thanks,
Jan Akerman


#7

Hi Jan,

Thanks so much for your reply. You guys must be very busy right now trying to get the latest release out the door so appreciate your time here.

One quick note about your proposed gestureRecognizerShouldBegin, one would need to use abs 

if (abs(trans.x) < abs(trans.y)) return NO;

However, if I hook it up this way instead of my prevous workaround (setting myself as handler, and calling panByValue as needed), I’m essentially trading problems.  

  1. That is if I use my work-around, there is no bounce back when reaching bounds - goes past bounds and just gradually slows down a couple centimeters later.   [seems like a bug in the SDK]

  2. And as I discoverd and posted about in another thread, the out of the box behavior for the graphs (which also occurs with your proposed workaround) is that if one hits the bounds with momentum (finger off device at the time it hits bounds) - it hits bounds like a brick wall and instantly stops.  No bouncing. [seems like a different, albeit related bug in the SDK]  However, the rubberbanding works just fine if you have your finger on the screen at the time bounds are hit.

Would you mind trying to get time to look at these two issues added to the schedule? 

Cheers


#8

Hi Tomshinobi,

Good point about the use of abs()! 

I’ve already responded to your other forum thread regarding (2) - just wanted to mention that here so you definitely saw it! And I’ll ensure someone looks into your issue with your first implementation (1).

However, I’m a little confused. Do you not get the rubber banding issue you see in method (2) when you use method (1)? I would expect you would see that issue in both implementations. As I understood your issue, when you were using method (1) you weren’t seeing any bounce back at all? And infact, the panning simply stopped a little bit past the charts default range?

If you get the rubber banding issue you mention here with both issues, I would definitely suggest using the method I proposed above instead.

Thanks,
Jan Akerman


#9

Hello again! I just had a play with the method -panByValue:withAnimation:withBounceLimits: and I can see the same problem that you are. The axis should be bouncing back, but the range just stays outside of the default range. I’ve raised this as a bug and we’ll look into it!

Thanks!
Jan


#10

Jan, thanks so much for your responses. That’s great that those two issues are-now/soon-will-be in the backlog.

Do you think the root issue/feature-request has any chance of making it into backlog? That is, have your code infer which pan directions the user wants the SDK to process/swallow based on the value of Y and X axis  ‘enableGesturePanning’ values  (my vote), or create new property to set on the chart to allow us set?  I’d feel much more comfortable with you guys owning that logic.  I’ve spent many more days wrestling with this and multiple chart panning, than I’d care to admit.

Given the rather limited screen realestate on the iPhone, I’m surprised I don’t see more people asking about  it. Seems like a very likely scenario.  

Thanks again!


#11

Hey - we have a task to look into ways we can expose our gesture recognizers to our users for customisation. I couldn’t comment on the details of any implementation, but customisation of our gesture recognizers is a feature we’ve had requested before, and it sounds like a good idea to me!

If you have any feedback about our charts or API, please email us, we love to hear our users opinions. It helps us plan our development & improve the product, so it is a win-win for everyone!

Thanks,
Jan


#12

Hi Jan,I tried to implement your solution for handling gesture recognizer. But I got error saying “Property overlay couldn’t be found in class object SChartCanvas”._chart.canvas.overlay.gestureRecognizers


#13

Hi.

Same Error for me: “Property ‘overlay’ cannot be found in forward class object ‘SChartCanvas’”


#14

Oh, stupid me.

Including the following headers solved the issue.

#import <ShinobiCharts/SChartCanvas.h>
#import <ShinobiCharts/SChartCanvasOverlay.h>

Cheers
Frank


#15

Is this “gesture swallowing” solved in version 2.6" ?

I’m facing the same issue too :frowning:


#16

 

Hi all

I like to ask where do I actually set the following?

which file should I edit? is it SChartCanvas.m because files under the Shinobi framwork are lock, what are the ways to unlock it?

Thank you 

for (UIGestureRecognizer *recognizer in _chart.canvas.overlay.gestureRecognizers)
    {
        if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]])
        {
            recognizer.delegate = self;
        }
    }

#17

I’m sorry for the repeated thread. I could not find the delete button.

There was only edit. Sorry guys.


#18

Hi Bosslee,

You can add that code after your chart initialisation code (say in viewDidLoad, or viewWillAppear), and then also implement the chart delegate as discussed in this thread on the view controller.

The behaviour is still the same in our latest version (2.7.0). This is actually the same way that two scroll views within one another would work. The inner scroll view would swallow the gestures. The gesture recognizer delegate gives you the ability to decide what gestures you want to handle, and which you want to throw up the view heirachy (by returning NO from gestureRecognizerShouldBegin).

Best of luck,
Jan


#19

Jan, are there any news regarding the bouncing of the xAxis?

 

I’m facing the exact same taks as the OP (http://www.shinobicontrols.com/forum/shinobicontrols/2014/11/multiple-shinobicharts-in-uiscrollview-with-vertical-scrolling) and having an effortless horizontal panning of multiple shinobi charts within a vertically scrolling UIScrollView would be such a nice feature.

 

To me it feels very hacky to mess with transformations and and scaling of the charts myself, which is what is currently the only option it seems. I’d much rather have you guys in control over pan and zooming gestures entirely.

 

In case I do have to implement the pan and zoom gestures myself, do you guys have an example regarding the pan and zoom gesture recognizers?

I can’t figure out what values to set for panByValue and zoomToPoint


#20

After some more hours of trial and error I am to the point there zooming works OK (just ok as there’s something wrong with the scaling that would case the chart to zoom in the opposite direction of your fingers). I took some parts of this post (very helpful and well written): https://blog.serverdensity.com/how-to-build-stunning-custom-ios-graphs-on-iphone-and-ipad/

Regardless of the current “setZoom” implementation my code shows, this needs to be replaced with “zoomToPoint” eventually as you’d want the chart to zoom relative to the DataPoint in between your two fingers.

The ScrollView works as expected as it is disabled during pinch/pan and moving the crosshair. It is just the panning that isn’t working yet as I still don’t know what values to set.

Here’s the code thus far; 

#import "ViewController.h"
#import <ShinobiCharts/SChartCanvas.h>
#import <ShinobiCharts/SChartCanvasOverlay.h>

@interface ViewController () <SChartDatasource, SChartDelegate, UIGestureRecognizerDelegate, UIScrollViewDelegate>

@end

@implementation ViewController {
    BOOL cancelPan;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    cancelPan = NO;
    
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
    self.scrollView.scrollEnabled = YES;
    self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width, 900);
    self.scrollView.delegate = self;
    
    [self.view addSubview:self.scrollView];
    
    UIPanGestureRecognizer* panSwipeRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
    panSwipeRecognizer.delegate = self;
    [self.view addGestureRecognizer:panSwipeRecognizer];
    
    UIPinchGestureRecognizer* pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];
    
    self.chart1 = [[ShinobiChart alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 600)];
    self.chart1 = [self configureChart:self.chart1];
    self.chart1.delegate = self;
    
    self.chart2 = [[ShinobiChart alloc] initWithFrame:CGRectMake(0, 600, self.view.frame.size.width, 300)];
    self.chart2 = [self configureChart:self.chart2];
    
    [self.scrollView addSubview:self.chart1];
    [self.scrollView addSubview:self.chart2];
}

- (void)sChart:(ShinobiChart *)chart crosshairMovedToXValue:(id)x andYValue:(id)y {
    cancelPan = YES;
    
    // TODO set crosshair to secondary chart
}

- (ShinobiChart *)configureChart:(ShinobiChart *)chart {
    
    chart.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    chart.licenseKey = @""; // TODO: add your trial licence key here!
    
    // add a pair of axes
    SChartNumberAxis *xAxis = [SChartNumberAxis new];
    chart.xAxis = xAxis;
    
    SChartNumberAxis *yAxis = [SChartNumberAxis new];
    yAxis.rangePaddingLow = @(0.1);
    yAxis.rangePaddingHigh = @(0.1);
    chart.yAxis = yAxis;
    
    chart.gesturePanType = SChartGesturePanTypeNone;
    
    // enable gestures
    yAxis.enableGesturePanning = NO;
    yAxis.enableGestureZooming = NO;
    xAxis.enableGesturePanning = YES;
    xAxis.enableGestureZooming = YES;
    
    chart.datasource = self;
    
    return chart;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}



-(void) scrollViewWillBeginDragging:(UIScrollView *)scrollView {
     cancelPan = YES;
}
 
-(void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     cancelPan = NO;
}

-(void) panGesture:(UIPanGestureRecognizer *) recognizer {
    
    CGPoint velocity = [recognizer velocityInView:recognizer.view];
    if(fabs(velocity.x) > fabs(velocity.y)) {
        if(cancelPan == NO) {
            
            // TODO set chart panning
            
            self.scrollView.scrollEnabled = NO;
        }
    }
    
    if(recognizer.state == UIGestureRecognizerStateEnded) {
        cancelPan = NO;
        self.scrollView.scrollEnabled = YES;
    }
}

- (void)handlePinch:(UIPinchGestureRecognizer*)sender {
    if(sender.state == UIGestureRecognizerStateChanged) {
        [self.chart1.xAxis setZoom:(self.chart1.xAxis.zoom * sender.scale) fromPosition:nil withAnimation:YES];
        [self.chart2.xAxis setZoom:(self.chart2.xAxis.zoom * sender.scale) fromPosition:nil withAnimation:YES];
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (NSInteger)numberOfSeriesInSChart:(ShinobiChart *)chart {
    return 1;
}

-(SChartSeries *)sChart:(ShinobiChart *)chart seriesAtIndex:(NSInteger)index {
    
    SChartPointSeries *lineSeries = [[SChartPointSeries alloc] init];
    lineSeries.crosshairEnabled = YES;
    return lineSeries;
}

- (NSInteger)sChart:(ShinobiChart *)chart numberOfDataPointsForSeriesAtIndex:(NSInteger)seriesIndex {
    return 1000;
}

- (id<SChartData>)sChart:(ShinobiChart *)chart dataPointAtIndex:(NSInteger)dataIndex forSeriesAtIndex:(NSInteger)seriesIndex {

    SChartDataPoint *datapoint = [[SChartDataPoint alloc] init];
    
    // both functions share the same x-values
    double xValue = dataIndex / 10.0;
    datapoint.xValue = [NSNumber numberWithDouble:xValue];
    datapoint.yValue = [NSNumber numberWithDouble:cosf(xValue)];
    
    return datapoint;
}

@end

Also, given that this task (having multiple synchronised charts in a vertical scrollview) seems to be very common, there should be something like a sticky post, or better yet, tutorial somewhere around here. That would help tremendously.