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;
}