Assignment #3 Task #6

Please note, this blog entry is from a previous course. You might want to check out the current one.

Your graphing UIView must be generic and reusable (i.e. it should be a generic x-y graphing class and know nothing about a CalculatorBrain). Use a protocol to get the graphing view’s data because Views should not own their data.

Basically we need a way to calculate an y value for a given x value, and to know where we want to place the graph and the size of the graph, which we define as protocol:

@protocol GraphViewDataSource
- (id)calculateYValueFromXValue:(double)xValue; // NSNumber or string with error
@property (nonatomic) CGFloat scale; // 1 = 100%
@property (nonatomic) CGPoint origin; // point to place in the middle of the screen
@end
...
@property (nonatomic, weak) IBOutlet id <GraphViewDataSource> dataSource;
....


and make our controller the data source:

@interface GraphViewController () <GraphViewDataSource>

and

- (void)setGraphView:(GraphView *)graphView
{
    if (graphView == _graphView) return;
    _graphView = graphView;
    graphView.dataSource = self;
}

As origin and scale are already implemented, we have only to provide the calculation by creating a new dictionary with the given x value and provide it to the model:

- (id)calculateYValueFromXValue:(double)xValue
{
    return [CalculatorBrain runProgram:self.program 
                   usingVariableValues:[NSDictionary 
                       dictionaryWithObjectsAndKeys:[NSNumber numberWithDouble:xValue], 
                           @"x", nil]];
}

Finally we use our data source in the drawing function of the view:

    CGFloat scale = self.dataSource.scale;
    CGPoint origin = self.dataSource.origin;
    [AxesDrawer drawAxesInRect:area originAtPoint:origin scale:scale];
    [[UIColor blackColor] setStroke];
    NSInteger widthInPixel = area.size.width * self.contentScaleFactor;    
    CGContextSetLineWidth(context, 2);
    CGContextBeginPath(context);
    for (NSInteger xPixel = 0; xPixel <= widthInPixel; xPixel++) {
        id result = [self.dataSource calculateYValueFromXValue:
                     [self xValueFromPixel:xPixel inRect:area 
                             originAtPoint:origin scale:scale]];
        if (![result isKindOfClass:[NSNumber class]]) {
            start = YES;
            continue;
        }
        double y = [result doubleValue];
        if (start) {
            CGContextMoveToPoint(context, 
                [self xPointFromPixel:xPixel inRect:area], 
                [self yPointFromValue:y inRect:area originAtPoint:origin scale:scale]
            );
            start = NO;
        } else CGContextAddLineToPoint(context, 
            [self xPointFromPixel:xPixel inRect:area], 
            [self yPointFromValue:y inRect:area originAtPoint:origin scale:scale]
        );                
    CGContextStrokePath(context);    

where we just connect the dots with lines and use a couple of helper functions for the adjustment from/to pixel/points/values:

- (CGFloat)xPointFromPixel:(NSInteger)xPixel inRect:(CGRect)bounds
{
    return xPixel / self.contentScaleFactor + bounds.origin.x;
}

- (double)xValueFromPixel:(NSInteger)xPixel inRect:(CGRect)bounds 
            originAtPoint:(CGPoint)origin    scale:(CGFloat)pointsPerUnit 
{
    return ([self xPointFromPixel:xPixel inRect:bounds] - origin.x) / pointsPerUnit;
}

- (CGFloat)yPointFromValue:(double)y      inRect:(CGRect)bounds 
             originAtPoint:(CGPoint)origin scale:(CGFloat)pointsPerUnit
{
    return origin.y - y * pointsPerUnit;
}
FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

Leave a Reply

Your email address will not be published.