diff --git a/.travis.yml b/.travis.yml index ad4bd74..b2feb30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,3 +3,5 @@ osx_image: xcode8.2 xcode_project: Sample Project/SimpleLineChart.xcodeproj xcode_scheme: SimpleLineChartTests xcode_sdk: iphonesimulator +script: + - xcodebuild clean build test -project "Sample Project/SimpleLineChart.xcodeproj" -scheme SimpleLineChartTests -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 7" ONLY_ACTIVE_ARCH=NO diff --git a/Classes/BEMAverageLine.h b/Classes/BEMAverageLine.h index 3d2b07d..528add8 100644 --- a/Classes/BEMAverageLine.h +++ b/Classes/BEMAverageLine.h @@ -11,7 +11,7 @@ /// A line displayed horizontally across the graph at the average y-value -@interface BEMAverageLine : NSObject +@interface BEMAverageLine : NSObject /// When set to YES, an average line will be displayed on the line graph @@ -35,7 +35,7 @@ /// Dash pattern for the average line -@property (strong, nonatomic, nullable) NSArray *dashPattern; +@property (strong, nonatomic, nullable) NSArray *dashPattern; //Label for average line in y axis. Default is blank. diff --git a/Classes/BEMAverageLine.m b/Classes/BEMAverageLine.m index d59d91c..bb2369a 100644 --- a/Classes/BEMAverageLine.m +++ b/Classes/BEMAverageLine.m @@ -14,7 +14,6 @@ - (instancetype)init { self = [super init]; if (self) { _enableAverageLine = NO; - _color = [UIColor whiteColor]; _alpha = 1.0; _width = 3.0; _yValue = NAN; @@ -22,14 +21,53 @@ - (instancetype)init { return self; } --(void) setLabel:(UILabel *)label { + +- (instancetype)initWithCoder:(NSCoder *)coder { + +#define RestoreProperty(property, type) {\ +if ([coder containsValueForKey:@#property]) { \ +self.property = [coder decode ## type ##ForKey:@#property ]; \ +}\ +} + self = [self init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + + RestoreProperty (enableAverageLine, Bool); + RestoreProperty (color, Object); + RestoreProperty (yValue, Float); + RestoreProperty (alpha, Float); + RestoreProperty (width, Float); + RestoreProperty (dashPattern, Object); + RestoreProperty (title, Object); +#pragma clang diagnostic pop + + //AverageLine + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + +#define EncodeProperty(property, type) [coder encode ## type: self.property forKey:@#property] + EncodeProperty (enableAverageLine, Bool); + EncodeProperty (color, Object); + EncodeProperty (yValue, Double); + EncodeProperty (alpha, Double); + EncodeProperty (width, Double); + EncodeProperty (dashPattern, Object); + EncodeProperty (title, Object); +} + + + +- (void)setLabel:(UILabel *)label { if (_label != label) { [_label removeFromSuperview]; _label = label; } } --(void) dealloc { +- (void)dealloc { self.label= nil; } @end diff --git a/Classes/BEMCircle.h b/Classes/BEMCircle.h index 6f56e3d..4b2a825 100644 --- a/Classes/BEMCircle.h +++ b/Classes/BEMCircle.h @@ -28,4 +28,4 @@ /// The value of the point @property (nonatomic) CGFloat absoluteValue; -@end \ No newline at end of file +@end diff --git a/Classes/BEMCircle.m b/Classes/BEMCircle.m index 9381dd1..4087921 100644 --- a/Classes/BEMCircle.m +++ b/Classes/BEMCircle.m @@ -27,4 +27,4 @@ - (void)drawRect:(CGRect)rect { CGContextFillPath(ctx); } -@end \ No newline at end of file +@end diff --git a/Classes/BEMGraphCalculator.m b/Classes/BEMGraphCalculator.m index 0cae401..3f0253c 100644 --- a/Classes/BEMGraphCalculator.m +++ b/Classes/BEMGraphCalculator.m @@ -41,13 +41,13 @@ - (instancetype)init { // MARK: - // MARK: Essential Calculations -- (nonnull NSArray *)calculationDataPointsOnGraph:(nonnull BEMSimpleLineGraphView *)graph { +- (nonnull NSArray *)calculationDataPointsOnGraph:(nonnull BEMSimpleLineGraphView *)graph { NSPredicate *filter = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { NSNumber *value = (NSNumber *)evaluatedObject; BOOL retVal = ![value isEqualToNumber:@(BEMNullGraphValue)]; return retVal; }]; - NSArray *filteredArray = [[graph graphValuesForDataPoints] filteredArrayUsingPredicate:filter]; + NSArray *filteredArray = [[graph graphValuesForDataPoints] filteredArrayUsingPredicate:filter]; return filteredArray; } @@ -55,7 +55,7 @@ - (nonnull NSArray *)calculationDataPointsOnGraph:(nonnull BEMSimpleLineGraphVie // MARK: Basic Statistics - (nonnull NSNumber *)calculatePointValueAverageOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"average:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -65,7 +65,7 @@ - (nonnull NSNumber *)calculatePointValueAverageOnGraph:(nonnull BEMSimpleLineGr } - (nonnull NSNumber *)calculatePointValueSumOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"sum:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -75,7 +75,7 @@ - (nonnull NSNumber *)calculatePointValueSumOnGraph:(nonnull BEMSimpleLineGraphV } - (nonnull NSNumber *)calculatePointValueMedianOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"median:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -85,19 +85,19 @@ - (nonnull NSNumber *)calculatePointValueMedianOnGraph:(nonnull BEMSimpleLineGra } - (nonnull NSNumber *)calculatePointValueModeOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"mode:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; - NSMutableArray *value = [expression expressionValueWithObject:nil context:nil]; - NSNumber *numberValue = [value firstObject]; + NSMutableArray *values = [expression expressionValueWithObject:nil context:nil]; + NSNumber *numberValue = [values firstObject]; if (numberValue) return numberValue; else return [NSNumber numberWithInt:0]; } - (nonnull NSNumber *)calculateStandardDeviationOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"stddev:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -110,7 +110,7 @@ - (nonnull NSNumber *)calculateStandardDeviationOnGraph:(nonnull BEMSimpleLineGr // MARK: Minimum / Maximum - (nonnull NSNumber *)calculateMinimumPointValueOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"min:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -119,7 +119,7 @@ - (nonnull NSNumber *)calculateMinimumPointValueOnGraph:(nonnull BEMSimpleLineGr } - (nonnull NSNumber *)calculateMaximumPointValueOnGraph:(nonnull BEMSimpleLineGraphView *)graph { - NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; + NSArray *filteredArray = [self calculationDataPointsOnGraph:graph]; if (filteredArray.count == 0) return [NSNumber numberWithInt:0]; NSExpression *expression = [NSExpression expressionForFunction:@"max:" arguments:@[[NSExpression expressionForConstantValue:filteredArray]]]; @@ -132,7 +132,7 @@ - (nonnull NSNumber *)calculateMaximumPointValueOnGraph:(nonnull BEMSimpleLineGr // MARK: Integration - (nonnull NSNumber *)calculateAreaUsingIntegrationMethod:(BEMIntegrationMethod)integrationMethod onGraph:(nonnull BEMSimpleLineGraphView *)graph xAxisScale:(nonnull NSNumber *)scale { - NSArray *fixedDataPoints = [self calculationDataPointsOnGraph:graph]; + NSArray *fixedDataPoints = [self calculationDataPointsOnGraph:graph]; if (integrationMethod == BEMIntegrationMethodLeftReimannSum) return [self integrateUsingLeftReimannSum:fixedDataPoints xAxisScale:scale]; else if (integrationMethod == BEMIntegrationMethodRightReimannSum) return [self integrateUsingRightReimannSum:fixedDataPoints xAxisScale:scale]; else if (integrationMethod == BEMIntegrationMethodTrapezoidalSum) return [self integrateUsingTrapezoidalSum:fixedDataPoints xAxisScale:scale]; @@ -140,10 +140,10 @@ - (nonnull NSNumber *)calculateAreaUsingIntegrationMethod:(BEMIntegrationMethod) else return [NSNumber numberWithInt:0]; } -- (NSNumber *)integrateUsingLeftReimannSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { +- (NSNumber *)integrateUsingLeftReimannSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { NSNumber *totalArea = [NSNumber numberWithInt:0]; - NSMutableArray *leftSumPoints = graphPoints.mutableCopy; + NSMutableArray *leftSumPoints = graphPoints.mutableCopy; [leftSumPoints removeLastObject]; for (NSNumber *yValue in leftSumPoints) { @@ -154,10 +154,10 @@ - (NSNumber *)integrateUsingLeftReimannSum:(nonnull NSArray *)graphPoints xAxisS return totalArea; } -- (NSNumber *)integrateUsingRightReimannSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { +- (NSNumber *)integrateUsingRightReimannSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { NSNumber *totalArea = [NSNumber numberWithInt:0]; - NSMutableArray *rightSumPoints = graphPoints.mutableCopy; + NSMutableArray *rightSumPoints = graphPoints.mutableCopy; [rightSumPoints removeObjectAtIndex:0]; for (NSNumber *yValue in rightSumPoints) { @@ -168,16 +168,16 @@ - (NSNumber *)integrateUsingRightReimannSum:(nonnull NSArray *)graphPoints xAxis return totalArea; } -- (NSNumber *)integrateUsingTrapezoidalSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { +- (NSNumber *)integrateUsingTrapezoidalSum:(nonnull NSArray *)graphPoints xAxisScale:(nonnull NSNumber *)scale { NSNumber *left = [self integrateUsingLeftReimannSum:graphPoints xAxisScale:scale]; NSNumber *right = [self integrateUsingRightReimannSum:graphPoints xAxisScale:scale]; NSNumber *trapezoidal = [NSNumber numberWithFloat:(left.floatValue+right.floatValue)/2]; return trapezoidal; } -- (NSNumber *)integrateUsingParabolicSimpsonSum:(nonnull NSArray *)points xAxisScale:(nonnull NSNumber *)scale { +- (NSNumber *)integrateUsingParabolicSimpsonSum:(nonnull NSArray *)points xAxisScale:(nonnull NSNumber *)scale { // Get all the points from the graph into a mutable array - NSMutableArray *graphPoints = points.mutableCopy; + NSMutableArray *graphPoints = points.mutableCopy; // If there are two or fewer points on the graph, no parabolic curve can be created. Thus, the next most accurate method will be employed: a trapezoidal summation if (graphPoints.count <= 2) return [self integrateUsingTrapezoidalSum:points xAxisScale:scale]; @@ -233,27 +233,27 @@ - (NSNumber *)integrateUsingParabolicSimpsonSum:(nonnull NSArray *)points xAxisS - (NSNumber *)calculateCorrelationCoefficientUsingCorrelationMethod:(BEMCorrelationMethod)correlationMethod onGraph:(BEMSimpleLineGraphView *)graph xAxisScale:(nonnull NSNumber *)scale { // Grab the x and y points // Because a BEMSimpleLineGraph object simply increments X-Values, we must calculate the values here - NSArray *yPoints = [self calculationDataPointsOnGraph:graph]; - NSMutableArray *xPoints = [NSMutableArray arrayWithCapacity:yPoints.count]; + NSArray *yPoints = [self calculationDataPointsOnGraph:graph]; + NSMutableArray *xPoints = [NSMutableArray arrayWithCapacity:yPoints.count]; if (scale == nil || scale.floatValue == 0.0) { for (NSUInteger i = 1; i <= yPoints.count; i++) { - [xPoints addObject:[NSNumber numberWithInteger:i]]; + [xPoints addObject:@(i)]; } } else { for (NSUInteger i = 1; i <= yPoints.count; i++) { - [xPoints addObject:[NSNumber numberWithFloat:(i*scale.floatValue)]]; + [xPoints addObject:@(i*scale.floatValue)]; } } // Set the initial values of our sum counts - NSInteger pointsCount = yPoints.count; + NSUInteger pointsCount = yPoints.count; CGFloat sumY = 0.0; CGFloat sumX = 0.0; CGFloat sumXY = 0.0; CGFloat sumX2 = 0.0; CGFloat sumY2 = 0.0; - NSInteger iterationCount = 0; + NSUInteger iterationCount = 0; for (NSNumber *yPoint in yPoints) { NSNumber *xPoint = xPoints[iterationCount]; iterationCount++; @@ -272,13 +272,13 @@ - (NSNumber *)calculateCorrelationCoefficientUsingCorrelationMethod:(BEMCorrelat // Calculate the correlational value CGFloat numeratorFirstChunk = (pointsCount * sumXY); // Calculate the mean of the points CGFloat numeratorSecondChunk = (sumX * sumY); // Calculate total graph values - CGFloat denomenatorFirstChunk = sqrt(pointsCount * sumX2 - (sumX * sumX)); // Square root of the sum of all X-Values squared - CGFloat denomenatorSecondChunk = sqrt(pointsCount * sumY2 - (sumY * sumY)); // Square root of the sum of all Y-Values squared - CGFloat correlation = (numeratorFirstChunk - numeratorSecondChunk) / (denomenatorFirstChunk * denomenatorSecondChunk); + double denomenatorFirstChunk = sqrt(pointsCount * sumX2 - (sumX * sumX)); // Square root of the sum of all X-Values squared + double denomenatorSecondChunk = sqrt(pointsCount * sumY2 - (sumY * sumY)); // Square root of the sum of all Y-Values squared + double correlation = (numeratorFirstChunk - numeratorSecondChunk) / (denomenatorFirstChunk * denomenatorSecondChunk); // NSLog(@"CORRELATION:\nSlope: %f\nIntercept:%f\nCorrelation:%f", slope, intercept, correlation); - return [NSNumber numberWithFloat:correlation]; + return @(correlation); } - (BEMPearsonCorrelationStrength)calculatePearsonCorrelationStrengthOnGraph:(BEMSimpleLineGraphView *)graph xAxisScale:(nonnull NSNumber *)scale { diff --git a/Classes/BEMLine.h b/Classes/BEMLine.h index 3572cfc..4337390 100644 --- a/Classes/BEMLine.h +++ b/Classes/BEMLine.h @@ -42,20 +42,14 @@ typedef NS_ENUM(NSUInteger, BEMLineGradientDirection) { //----- POINTS -----// -/// All of the Y-axis values for the points -@property (strong, nonatomic, nonnull) NSArray *arrayOfPoints; +/// All of the values for the points +@property (strong, nonatomic, nonnull) NSArray *points; /// All of the X-Axis coordinates used to draw vertical lines through -@property (strong, nonatomic, nonnull) NSArray *arrayOfVerticalReferenceLinePoints; - -/// The value used to offset the fringe vertical reference lines when the x-axis labels are on the edge -@property (assign, nonatomic) CGFloat verticalReferenceHorizontalFringeNegation; +@property (strong, nonatomic, nonnull) NSArray *arrayOfVerticalReferenceLinePoints; /// All of the Y-Axis coordinates used to draw horizontal lines through -@property (strong, nonatomic, nullable) NSArray *arrayOfHorizontalReferenceLinePoints; - -/// All of the point values -@property (strong, nonatomic, nullable) NSArray *arrayOfValues; +@property (strong, nonatomic, nullable) NSArray *arrayOfHorizontalReferenceLinePoints; /** Draw thin, translucent, reference lines using the provided X-Axis and Y-Axis coordinates. @see Use \p arrayOfVerticalReferenceLinePoints to specify vertical reference lines' positions. Use \p arrayOfHorizontalReferenceLinePoints to specify horizontal reference lines' positions. */ @@ -77,10 +71,10 @@ typedef NS_ENUM(NSUInteger, BEMLineGradientDirection) { @property (assign, nonatomic) BOOL enableTopReferenceFrameLine; /** Dash pattern for the references line on the X axis */ -@property (nonatomic, strong, nullable) NSArray *lineDashPatternForReferenceXAxisLines; +@property (nonatomic, strong, nullable) NSArray *lineDashPatternForReferenceXAxisLines; /** Dash pattern for the references line on the Y axis */ -@property (nonatomic, strong, nullable) NSArray *lineDashPatternForReferenceYAxisLines; +@property (nonatomic, strong, nullable) NSArray *lineDashPatternForReferenceYAxisLines; /** If a null value is present, interpolation would draw a best fit line through the null point bound by its surrounding points. Default: YES */ @property (assign, nonatomic) BOOL interpolateNullValues; @@ -99,16 +93,16 @@ typedef NS_ENUM(NSUInteger, BEMLineGradientDirection) { @property (strong, nonatomic, nullable) UIColor *topColor; /// A color gradient applied to the area above the line, inside of its superview. If set, it will be drawn on top of the fill from the \p topColor property. -@property (assign, nonatomic, nullable) CGGradientRef topGradient; +@property (strong, nonatomic, nullable) __attribute__((NSObject)) CGGradientRef topGradient; /// The color of the area below the line, inside of its superview @property (strong, nonatomic, nullable) UIColor *bottomColor; /// A color gradient applied to the area below the line, inside of its superview. If set, it will be drawn on top of the fill from the \p bottomColor property. -@property (assign, nonatomic, nullable) CGGradientRef bottomGradient; +@property (strong, nonatomic, nullable) __attribute__((NSObject)) CGGradientRef bottomGradient; /// A color gradient to be applied to the line. If this property is set, it will mask (override) the \p color property. -@property (assign, nonatomic, nullable) CGGradientRef lineGradient; +@property (strong, nonatomic, nullable) __attribute__((NSObject)) CGGradientRef lineGradient; /// The drawing direction of the line gradient color @property (nonatomic) BEMLineGradientDirection lineGradientDirection; diff --git a/Classes/BEMLine.m b/Classes/BEMLine.m index fd8800e..decd00c 100644 --- a/Classes/BEMLine.m +++ b/Classes/BEMLine.m @@ -10,19 +10,6 @@ #import "BEMLine.h" #import "BEMSimpleLineGraphView.h" -#if CGFLOAT_IS_DOUBLE -#define CGFloatValue doubleValue -#else -#define CGFloatValue floatValue -#endif - - -@interface BEMLine() - -@property (nonatomic, strong) NSMutableArray *points; - -@end - @implementation BEMLine - (instancetype)initWithFrame:(CGRect)frame { @@ -33,6 +20,7 @@ - (instancetype)initWithFrame:(CGRect)frame { _enableLeftReferenceFrameLine = YES; _enableBottomReferenceFrameLine = YES; _interpolateNullValues = YES; + self.clipsToBounds = YES; } return self; } @@ -42,7 +30,11 @@ - (void)drawRect:(CGRect)rect { //---- Draw Reference Lines ---// //----------------------------// self.layer.sublayers = nil; - + if (self.points.count == 0) { + self.backgroundColor = self.bottomColor ?: [UIColor clearColor]; + return; + }; + UIBezierPath *verticalReferenceLinesPath = [UIBezierPath bezierPath]; UIBezierPath *horizontalReferenceLinesPath = [UIBezierPath bezierPath]; UIBezierPath *referenceFramePath = [UIBezierPath bezierPath]; @@ -56,44 +48,36 @@ - (void)drawRect:(CGRect)rect { referenceFramePath.lineCapStyle = kCGLineCapButt; referenceFramePath.lineWidth = 0.7f; - if (self.enableReferenceFrame == YES) { + if (self.enableReferenceFrame== YES) { + CGFloat offset = self.referenceLineWidth/4; //moves framing ref line slightly into view if (self.enableBottomReferenceFrameLine) { // Bottom Line - [referenceFramePath moveToPoint:CGPointMake(0, self.frame.size.height)]; - [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width, self.frame.size.height)]; + [referenceFramePath moveToPoint: CGPointMake(0, self.frame.size.height-offset)]; + [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width, self.frame.size.height-offset)]; } if (self.enableLeftReferenceFrameLine) { // Left Line - [referenceFramePath moveToPoint:CGPointMake(0+self.referenceLineWidth/4, self.frame.size.height)]; - [referenceFramePath addLineToPoint:CGPointMake(0+self.referenceLineWidth/4, 0)]; + [referenceFramePath moveToPoint: CGPointMake(0+offset, self.frame.size.height)]; + [referenceFramePath addLineToPoint:CGPointMake(0+offset, 0)]; } if (self.enableTopReferenceFrameLine) { // Top Line - [referenceFramePath moveToPoint:CGPointMake(0+self.referenceLineWidth/4, 0)]; - [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width, 0)]; + [referenceFramePath moveToPoint: CGPointMake(0, offset)]; + [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width, offset)]; } if (self.enableRightReferenceFrameLine) { // Right Line - [referenceFramePath moveToPoint:CGPointMake(self.frame.size.width - self.referenceLineWidth/4, self.frame.size.height)]; - [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width - self.referenceLineWidth/4, 0)]; + [referenceFramePath moveToPoint: CGPointMake(self.frame.size.width - offset, self.frame.size.height)]; + [referenceFramePath addLineToPoint:CGPointMake(self.frame.size.width - offset, 0)]; } } - if (self.enableReferenceLines == YES) { if (self.arrayOfVerticalReferenceLinePoints.count > 0) { for (NSNumber *xNumber in self.arrayOfVerticalReferenceLinePoints) { - CGFloat xValue; - if (self.verticalReferenceHorizontalFringeNegation != 0.0) { - if ([self.arrayOfVerticalReferenceLinePoints indexOfObject:xNumber] == 0) { // far left reference line - xValue = [xNumber floatValue] + self.verticalReferenceHorizontalFringeNegation; - } else if ([self.arrayOfVerticalReferenceLinePoints indexOfObject:xNumber] == [self.arrayOfVerticalReferenceLinePoints count]-1) { // far right reference line - xValue = [xNumber floatValue] - self.verticalReferenceHorizontalFringeNegation; - } else xValue = [xNumber floatValue]; - } else xValue = [xNumber floatValue]; - + CGFloat xValue =[xNumber floatValue]; CGPoint initialPoint = CGPointMake(xValue, self.frame.size.height); CGPoint finalPoint = CGPointMake(xValue, 0); @@ -113,7 +97,6 @@ - (void)drawRect:(CGRect)rect { } } - //----------------------------// //----- Draw Average Line ----// //----------------------------// @@ -129,41 +112,98 @@ - (void)drawRect:(CGRect)rect { [averageLinePath addLineToPoint:finalPoint]; } - //----------------------------// //------ Draw Graph Line -----// //----------------------------// // LINE - UIBezierPath *line = [UIBezierPath bezierPath]; - UIBezierPath *fillTop; - UIBezierPath *fillBottom; - - CGFloat xIndexScale = self.frame.size.width/([self.arrayOfPoints count] - 1); + NSUInteger numPoints = self.points.count; + CGFloat rightEdge = CGRectGetMaxX(self.bounds); + NSInteger numOverRightEdge = 0; + NSMutableArray *drawPoints = [NSMutableArray arrayWithCapacity:numPoints]; + for (NSUInteger index = 0; index < self.points.count; index++) { + NSValue *value = self.points[index]; + CGPoint point = value.CGPointValue; + CGFloat xValue = point.x; + CGFloat yValue = point.y; + if (xValue < 0) { + //only need two points to left of view (negative x) to get curve correct + NSUInteger nextIndex = index+1; + NSUInteger numNegsRight = 0; + while (nextIndex < self.points.count && numNegsRight < 2) { + CGFloat xValue = self.points[nextIndex].CGPointValue.x; + if (xValue < 0) { + numNegsRight ++; + } else if (xValue < BEMNullGraphValue) { + //hit positive x's so + break; + } + nextIndex++; + } + if (numNegsRight >= 2) continue; //don't need this point + } + if (xValue > rightEdge) { + //only include twp points to right of view to get curve correct + if (numOverRightEdge >= 2) continue; + numOverRightEdge++; + } - self.points = [NSMutableArray arrayWithCapacity:self.arrayOfPoints.count]; - for (NSUInteger i = 0; i < self.arrayOfPoints.count; i++) { - CGPoint value = CGPointMake(xIndexScale * i, [self.arrayOfPoints[i] CGFloatValue]); - if (value.y < BEMNullGraphValue || !self.interpolateNullValues) { - [self.points addObject:[NSValue valueWithCGPoint:value]]; + if (yValue >= BEMNullGraphValue && self.interpolateNullValues) { + //need to linear interpolate. For midpoints, just don't add a point + if (drawPoints.count <= 1) { + //extrapolate a left edge point from two actual values, either next two or last one and next one + NSUInteger nextIndex = index+1; + NSValue * firstPoint = nil; + if (drawPoints.count == 0) { + while (nextIndex < numPoints && self.points[nextIndex].CGPointValue.y >= BEMNullGraphValue) nextIndex++; + if (nextIndex >= numPoints) break; // all NaNs?? =>don't create any line + firstPoint = self.points[nextIndex]; + nextIndex++; //look for second real value + } else { + firstPoint = drawPoints[0]; + } + CGFloat firstXValue = firstPoint.CGPointValue.x; + CGFloat firstYValue = firstPoint.CGPointValue.y; + while (nextIndex < self.points.count && self.points[nextIndex].CGPointValue.y >= BEMNullGraphValue) nextIndex++; + if (nextIndex >= numPoints) { + // only one real number + yValue = firstYValue; + } else { + CGFloat deltaY = firstYValue - self.points[nextIndex].CGPointValue.y; + CGFloat deltaX = self.points[nextIndex].CGPointValue.x - firstXValue; + yValue = firstYValue + (firstXValue-xValue)*deltaY/deltaX; + } + } else if (value == self.points[numPoints-1]) { + //extrapolate a right edge point from previous two actual values + NSInteger firstPos = ((NSInteger)numPoints)-2; //look for first real value + while (firstPos >= 0 && self.points[(NSUInteger)firstPos].CGPointValue.y >= BEMNullGraphValue) firstPos--; + if (firstPos < 0 ) continue; // all NaNs?? =>don't create any line; should already be gone + + CGFloat firstValue = self.points[(NSUInteger)firstPos].CGPointValue.y; + NSInteger secondPos = firstPos-1; //look for second real value + while (secondPos >= 0 && self.points[(NSUInteger)secondPos].CGPointValue.y >= BEMNullGraphValue) secondPos--; + if (secondPos < 0) { + // only one real number + yValue = firstValue; + } else { + CGFloat delta = firstValue - self.points[(NSUInteger)secondPos].CGPointValue.y; + yValue = firstValue + ((NSInteger)numPoints - firstPos-1)*delta/(firstPos - secondPos); + } + + } else { + continue; //skip this (middle Null) point, let graphics handle interpolation + } } + CGPoint newPoint = CGPointMake(xValue, yValue); + [drawPoints addObject:[NSValue valueWithCGPoint:newPoint]]; } - - BOOL bezierStatus = self.bezierCurveIsEnabled; - if (self.arrayOfPoints.count <= 2 && self.bezierCurveIsEnabled == YES) bezierStatus = NO; - - if (!self.disableMainLine && bezierStatus) { - line = [BEMLine quadCurvedPathWithPoints:self.points]; - fillBottom = [BEMLine quadCurvedPathWithPoints:self.bottomPointsArray]; - fillTop = [BEMLine quadCurvedPathWithPoints:self.topPointsArray]; - } else if (!self.disableMainLine && !bezierStatus) { - line = [BEMLine linesToPoints:self.points]; - fillBottom = [BEMLine linesToPoints:self.bottomPointsArray]; - fillTop = [BEMLine linesToPoints:self.topPointsArray]; - } else { - fillBottom = [BEMLine linesToPoints:self.bottomPointsArray]; - fillTop = [BEMLine linesToPoints:self.topPointsArray]; + UIBezierPath *line = [UIBezierPath bezierPath]; + if (!self.disableMainLine && drawPoints.count > 0 ) { + line = [BEMLine pathWithPoints:drawPoints curved:self.bezierCurveIsEnabled open:YES]; } + UIBezierPath *fillBottom = [BEMLine pathWithPoints: [self bottomPointsFromArray:drawPoints] curved:self.bezierCurveIsEnabled open:NO]; + UIBezierPath *fillTop = [BEMLine pathWithPoints: [self topPointsFromArray: drawPoints] curved:self.bezierCurveIsEnabled open:NO]; + //----------------------------// //----- Draw Fill Colors -----// //----------------------------// @@ -178,7 +218,7 @@ - (void)drawRect:(CGRect)rect { CGContextSaveGState(ctx); CGContextAddPath(ctx, [fillTop CGPath]); CGContextClip(ctx); - CGContextDrawLinearGradient(ctx, self.topGradient, CGPointZero, CGPointMake(0, CGRectGetMaxY(fillTop.bounds)), 0); + CGContextDrawLinearGradient(ctx, self.topGradient, CGPointZero, CGPointMake(0, CGRectGetMaxY(fillTop.bounds)), (CGGradientDrawingOptions) 0); CGContextRestoreGState(ctx); } @@ -186,7 +226,7 @@ - (void)drawRect:(CGRect)rect { CGContextSaveGState(ctx); CGContextAddPath(ctx, [fillBottom CGPath]); CGContextClip(ctx); - CGContextDrawLinearGradient(ctx, self.bottomGradient, CGPointZero, CGPointMake(0, CGRectGetMaxY(fillBottom.bounds)), 0); + CGContextDrawLinearGradient(ctx, self.bottomGradient, CGPointZero, CGPointMake(0, CGRectGetMaxY(fillBottom.bounds)), (CGGradientDrawingOptions) 0); CGContextRestoreGState(ctx); } @@ -286,78 +326,132 @@ - (void)drawRect:(CGRect)rect { } } -- (NSArray *)topPointsArray { - CGPoint topPointZero = CGPointMake(0,0); - CGPoint topPointFull = CGPointMake(self.frame.size.width, 0); - NSMutableArray *topPoints = [NSMutableArray arrayWithArray:self.points]; - [topPoints insertObject:[NSValue valueWithCGPoint:topPointZero] atIndex:0]; - [topPoints addObject:[NSValue valueWithCGPoint:topPointFull]]; - return topPoints; +- (NSArray *)areaArrayFromArray:(NSArray *)array withEdgeAt:(CGFloat)edgeHeight { + CGFloat halfHeight = self.frame.size.height/2; + CGPoint midLeftPoint = CGPointMake(0, halfHeight); + CGPoint midRightPoint = CGPointMake(self.frame.size.width,halfHeight); + if (array.count > 0) { + midLeftPoint.y = array[0].CGPointValue.y; + midRightPoint.y = [array lastObject].CGPointValue.y; + } + + CGPoint topPointZero = CGPointMake(0,edgeHeight); + CGPoint topPointFull = CGPointMake(self.frame.size.width, edgeHeight); + NSMutableArray *areaPoints = [NSMutableArray arrayWithArray:array]; + [areaPoints insertObject:[NSValue valueWithCGPoint:topPointZero] atIndex:0]; + [areaPoints insertObject:[NSValue valueWithCGPoint:midLeftPoint] atIndex:1]; + [areaPoints addObject:[NSValue valueWithCGPoint:midRightPoint]]; + [areaPoints addObject:[NSValue valueWithCGPoint:topPointFull]]; + return areaPoints; } -- (NSArray *)bottomPointsArray { - CGPoint bottomPointZero = CGPointMake(0, self.frame.size.height); - CGPoint bottomPointFull = CGPointMake(self.frame.size.width, self.frame.size.height); - NSMutableArray *bottomPoints = [NSMutableArray arrayWithArray:self.points]; - [bottomPoints insertObject:[NSValue valueWithCGPoint:bottomPointZero] atIndex:0]; - [bottomPoints addObject:[NSValue valueWithCGPoint:bottomPointFull]]; - return bottomPoints; +- (NSArray *)topPointsFromArray:(NSArray *)array { + return [self areaArrayFromArray: array withEdgeAt:0]; } -+ (UIBezierPath *)linesToPoints:(NSArray *)points { - UIBezierPath *path = [UIBezierPath bezierPath]; - NSValue *value = points[0]; - CGPoint p1 = [value CGPointValue]; - [path moveToPoint:p1]; +- (NSArray *)bottomPointsFromArray:(NSArray *)array { + return [self areaArrayFromArray: array withEdgeAt:self.frame.size.height]; +} - for (NSUInteger i = 1; i < points.count; i++) { - value = points[i]; - CGPoint p2 = [value CGPointValue]; - [path addLineToPoint:p2]; - } - return path; +static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) { + CGFloat avgY = (p1.y + p2.y) / 2.0f; + if (isinf(avgY)) avgY = BEMNullGraphValue; + return CGPointMake((p1.x + p2.x) / 2, avgY); } -+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points { ++ (UIBezierPath *)pathWithPoints:(NSArray *)points curved:(BOOL) curved open:(BOOL) open { + //Cubic fit based on Roman Filippov code: http://stackoverflow.com/a/40203583/580850 + //open means allow gaps in path. + //Also, if not open, then first/last points are for frame, and should not affect curve. UIBezierPath *path = [UIBezierPath bezierPath]; - - NSValue *value = points[0]; - CGPoint p1 = [value CGPointValue]; + CGPoint p1 = [points[0] CGPointValue]; + NSUInteger dataStart = 1; + NSUInteger dataEnd = points.count-1; [path moveToPoint:p1]; - - if (points.count == 2) { - value = points[1]; - CGPoint p2 = [value CGPointValue]; - [path addLineToPoint:p2]; - return path; + if (!open) { + //skip first/last points (frame, not data) to ensure line/frame use same bezier path + [path addLineToPoint:[points[1] CGPointValue]]; + p1 = [points[2] CGPointValue]; + [path addLineToPoint:p1]; + dataStart += 2; + dataEnd -= 2; } - - for (NSUInteger i = 1; i < points.count; i++) { - value = points[i]; - CGPoint p2 = [value CGPointValue]; - - CGPoint midPoint = midPointForPoints(p1, p2); - [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)]; - [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)]; - + CGPoint oldControlPoint = p1; + for (NSUInteger pointIndex = dataStart; pointIndex< points.count; pointIndex++) { + CGPoint p2 = [points[pointIndex] CGPointValue]; + + if (p1.y >= BEMNullGraphValue || p2.y >= BEMNullGraphValue) { + if (open) { + [path moveToPoint:p2]; + } else { + [path addLineToPoint:p2]; + } + oldControlPoint = p2; + } else if (curved ) { + CGPoint p3 = CGPointZero; + //Don't let frame points beyond actual data affect curve. + if (pointIndex < dataEnd) p3 = [points[pointIndex+1] CGPointValue] ; + if (p3.y >= BEMNullGraphValue) p3 = CGPointZero; + CGPoint newControlPoint = controlPointForPoints(p1, p2, p3); + if (!CGPointEqualToPoint( newControlPoint, CGPointZero)) { + [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: newControlPoint]; + oldControlPoint = imaginForPoints( newControlPoint, p2); + //this "if" not in original algorithm, but seems to smooth so that curves don't "backup" when points are too close in x dimension + if (! CGPointEqualToPoint(p3,CGPointZero)) { + if (oldControlPoint.x > p3.x ) { + oldControlPoint.x = p3.x; + } + } + } else { + [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: p2]; + oldControlPoint = p2; + } + } else { + [path addLineToPoint:p2]; + oldControlPoint = p2; + } p1 = p2; } return path; } -static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) { - return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); +static CGPoint imaginForPoints(CGPoint point, CGPoint center) { + //returns "mirror image" of point: the point that is symmetrical through center. + if (CGPointEqualToPoint(point, CGPointZero) || CGPointEqualToPoint(center, CGPointZero)) { + return CGPointZero; + } + CGFloat newX = center.x + (center.x-point.x); + CGFloat newY = center.y + (center.y-point.y); + if (isinf(newY)) { + newY = BEMNullGraphValue; + } + return CGPointMake(newX,newY); } -static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) { - CGPoint controlPoint = midPointForPoints(p1, p2); - CGFloat diffY = (CGFloat)fabs(p2.y - controlPoint.y); +static CGFloat clamp(CGFloat num, CGFloat bounds1, CGFloat bounds2) { + //ensure num is between bounds. + if (bounds1 < bounds2) { + return MIN(MAX(bounds1,num),bounds2); + } else { + return MIN(MAX(bounds2,num),bounds1); + } +} + +static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2, CGPoint p3) { + if (CGPointEqualToPoint(p3, CGPointZero)) return CGPointZero; + CGPoint leftMidPoint = midPointForPoints(p1, p2); + CGPoint rightMidPoint = midPointForPoints(p2, p3); + CGPoint imaginPoint = imaginForPoints(rightMidPoint, p2); + CGPoint controlPoint = midPointForPoints(leftMidPoint, imaginPoint); + + controlPoint.y = clamp(controlPoint.y, p1.y, p2.y); - if (p1.y < p2.y) - controlPoint.y += diffY; - else if (p1.y > p2.y) - controlPoint.y -= diffY; + CGFloat flippedP3 = p2.y + (p2.y-p3.y); + controlPoint.y = clamp(controlPoint.y, p2.y, flippedP3); + //x clamp not in original algorithm, but seems to smooth + //so that curves don't "backup" when points are too close in x dimension + controlPoint.x = clamp(controlPoint.x, p1.x, p2.x); return controlPoint; } @@ -403,7 +497,7 @@ - (CALayer *)backgroundGradientLayerForLayer:(CAShapeLayer *)shapeLayer { end = CGPointMake(CGRectGetMidX(shapeLayer.bounds), CGRectGetMaxY(shapeLayer.bounds)); } - CGContextDrawLinearGradient(imageCtx, self.lineGradient, start, end, 0); + CGContextDrawLinearGradient(imageCtx, self.lineGradient, start, end, (CGGradientDrawingOptions)0); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); CALayer *gradientLayer = [CALayer layer]; diff --git a/Classes/BEMSimpleLineGraphView.h b/Classes/BEMSimpleLineGraphView.h index 3eec526..b986fd0 100644 --- a/Classes/BEMSimpleLineGraphView.h +++ b/Classes/BEMSimpleLineGraphView.h @@ -69,6 +69,11 @@ IB_DESIGNABLE @interface BEMSimpleLineGraphView : UIView Left) @property (nonatomic) IBInspectable BOOL positionYAxisRight; +/// Position of the x-Axis in relation to the chart (Default: NO ==> Top) +@property (nonatomic) IBInspectable BOOL positionXAxisTop; + /// A line dash patter to be applied to X axis reference lines. This allows you to draw a dotted or hashed line -@property (nonatomic, strong) NSArray *lineDashPatternForReferenceXAxisLines; +@property (nonatomic, strong) NSArray *lineDashPatternForReferenceXAxisLines; /// A line dash patter to be applied to Y axis reference lines. This allows you to draw a dotted or hashed line -@property (nonatomic, strong) NSArray *lineDashPatternForReferenceYAxisLines; +@property (nonatomic, strong) NSArray *lineDashPatternForReferenceYAxisLines; /// Color to be used for the no data label on the chart @@ -387,26 +412,49 @@ IB_DESIGNABLE @interface BEMSimpleLineGraphView : UIView *)incrementPositionsForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph; +/** The total number of X-axis labels on the line graph. + @discussion Calculates the total wdith of the graph and evenly spaces the labels based on the graph width. If this and lineGraph:locationForPointAtIndex: are implemented, labels may diverge from data points + @param graph The graph object which is requesting the number of labels. + @return The number of labels displayed on the Y-axis. */ +- (NSInteger)numberOfXAxisLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph; +/** Informs delegate of user request to zoom the chart and requests permission to proceed + @param oldScale The current scale level. 1.0 = autosized to fit all points. 2.0 would show half the chart + @param newScale New scale level requested by pinchZoom. + @param displayMinXValue The smallest datapoint that will be included in the chart (either index or value, depending on whether locationForPointAtIndex is implemented). + @param displayMaxXValue The largest datapoint that will be included in the chart. + @return YES if pan/zoom is ok; No if pan/zoom is prevented. */ +- (BOOL)lineGraph:(BEMSimpleLineGraphView *)graph shouldScaleFrom:(CGFloat)oldScale to:(CGFloat)newScale showingFromXMinValue:(CGFloat)displayMinXValue toXMaxValue:(CGFloat)displayMaxXValue; //----- Y AXIS -----// @@ -575,7 +650,7 @@ IB_DESIGNABLE @interface BEMSimpleLineGraphView : UIView *xAxisValues; +#pragma mark Properties to store data and computed locations +/// The number of Points in the Graph +/// Set by layoutself.numberOfPoints +@property (assign, nonatomic ) NSInteger numberOfPoints; - /// All of the X-Axis Label Points - NSMutableArray *xAxisLabelPoints; +/// All of the Data Points from datasource (Y values) +/// Set by getData and used throughout. +@property (strong, nonatomic ) NSArray *dataPoints; - /// How much to ?? - CGFloat xAxisHorizontalFringeNegationValue; +/// All of the Y-Axis Values as scaled to current view +// Set by getData; used by circleDotAtIndex, and handled to BEM to draw mainLine +@property (strong, nonatomic ) NSArray *locations; - /// All of the Y-Axis Label Points - NSMutableArray *yAxisLabelPoints; - - /// All of the Y-Axis Values - NSMutableArray *yAxisValues; - - /// All of the Data Points - NSMutableArray *dataPoints; - -} - -#pragma mark Properties to store all subviews +#pragma mark Properties to store main views (yAxis, xAxis, actual line, dots layer and labels layers // Stores the background X Axis view @property (strong, nonatomic ) UIView *backgroundXAxis; // Stores the background Y Axis view @property (strong, nonatomic) UIView *backgroundYAxis; +/// The line itself and decorators; these three views share same size and overlay each other. +@property (strong, nonatomic) BEMLine * masterLine; +///Container for all BEMCircle dots +@property (strong, nonatomic) UIView *dotsView; +///Container for all labels on dots and gestures +@property (strong, nonatomic) UIView *labelsView; + +#pragma mark Properties to store all subviews; used to avoid recreating each time. /// All of the Y-Axis Labels -@property (strong, nonatomic) NSMutableArray *yAxisLabels; +@property (strong, nonatomic) NSArray *yAxisLabels; /// All of the X-Axis Labels -@property (strong, nonatomic) NSMutableArray *xAxisLabels; +@property (strong, nonatomic) NSArray *xAxisLabels; + +/// All of the X-Axis label texts (for testing) +@property (strong, nonatomic) NSArray *xAxisLabelTexts; /// All of the dataPoint Labels -@property (strong, nonatomic) NSMutableArray *permanentPopups; +@property (strong, nonatomic) NSArray *permanentPopups; /// All of the dataPoint dots -@property (strong, nonatomic) NSMutableArray *circleDots; - -/// The line itself -@property (strong, nonatomic) BEMLine *masterLine; +@property (strong, nonatomic) NSArray *circleDots; /// The vertical line which appears when the user drags across the graph @property (strong, nonatomic) UIView *touchInputLine; -/// View for picking up pan gesture -@property (strong, nonatomic, readwrite) UIView *panView; - /// Label to display when there is no data @property (strong, nonatomic) UILabel *noDataLabel; -/// Cirle to display when there's only one datapoint -@property (strong, nonatomic) BEMCircle *oneDot; +/// The label displayed when enablePopUpReport is set to YES +@property (strong, nonatomic) UILabel *popUpLabel; + +// Possible custom View displayed instead of popUpLabel +@property (strong, nonatomic) UIView *customPopUpView; + +#pragma mark Gesture Recognizers and supporting globals /// The gesture recognizer picking up the pan in the graph view -@property (strong, nonatomic) UIPanGestureRecognizer *panGesture; +@property (strong, nonatomic) UIPanGestureRecognizer *touchReportPanGesture; /// This gesture recognizer picks up the initial touch on the graph view @property (strong, nonatomic) UILongPressGestureRecognizer *longPressGesture; -/// The label displayed when enablePopUpReport is set to YES -@property (strong, nonatomic) UILabel *popUpLabel; +@property (strong, nonatomic) UIPinchGestureRecognizer *zoomGesture; +@property (strong, nonatomic) UIPanGestureRecognizer * zoomPanGesture; + +// set by zoomPanGesture to pan along X axis +@property (nonatomic) CGFloat panMovementBase; +@property (nonatomic) CGFloat panMovement; -/// The label displayed when a custom popUpView is returned by the delegate -@property (strong, nonatomic) UIView *popUpView; +//used during zoom to remember original anchor point and corresponding value +@property (nonatomic) CGFloat zoomCenterLocation; +@property (nonatomic) CGFloat zoomCenterValue; +//maximum amount user is allowed to zoom in +@property (nonatomic, assign) CGFloat maxZoom; -#pragma mark calculated properties -/// The Y offset necessary to compensate the labels on the X-Axis -@property (nonatomic) CGFloat XAxisLabelYOffset; +//used to restore zoom +@property (strong, nonatomic) UITapGestureRecognizer *doubleTapGesture; +// set by doubleTap to remember previous scale +@property (nonatomic) CGFloat doubleTapScale; -/// The X offset necessary to compensate the labels on the Y-Axis. Will take the value of the bigger label on the Y-Axis -@property (nonatomic) CGFloat YAxisLabelXOffset; +#pragma mark Calculated min/max properties; set by getData and Zoom /// The biggest value out of all of the data points -@property (nonatomic) CGFloat maxValue; +@property (nonatomic) CGFloat maxYValue; /// The smallest value out of all of the data points -@property (nonatomic) CGFloat minValue; +@property (nonatomic) CGFloat minYValue; -// Tracks whether the popUpView is custom or default -@property (nonatomic) BOOL usingCustomPopupView; +/// The biggest value on the X axis +@property (nonatomic) CGFloat maxXValue; + +/// The smallest value on the X axis +@property (nonatomic) CGFloat minXValue; // Stores the current view size to detect whether a redraw is needed in layoutSubviews @property (nonatomic) CGSize currentViewSize; -/// Find which point is currently the closest to the vertical line -- (BEMCircle *)closestDotFromTouchInputLine:(UIView *)touchInputLine; - -/// Determines the biggest Y-axis value from all the points -- (CGFloat)maxValue; - -/// Determines the smallest Y-axis value from all the points -- (CGFloat)minValue; - - @end + @implementation BEMSimpleLineGraphView #pragma mark - Initialization @@ -139,9 +140,138 @@ - (instancetype)initWithFrame:(CGRect)frame { - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) [self commonInit]; + [self restorePropertyWithCoder:coder]; return self; } +- (void)decodeRestorableStateWithCoder:(NSCoder *)coder { + [super decodeRestorableStateWithCoder:coder]; + [self restorePropertyWithCoder:coder]; +} + +- (void)restorePropertyWithCoder:(NSCoder *)coder { + +#define RestoreProperty(property, type) \ +if ([coder containsValueForKey:@#property]) { \ +self.property = [coder decode ## type ##ForKey:@#property]; \ +}\ + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + + RestoreProperty (animationGraphEntranceTime, Float); + RestoreProperty (animationGraphStyle, Integer); + + RestoreProperty (colorXaxisLabel, Object); + RestoreProperty (colorYaxisLabel, Object); + RestoreProperty (colorTop, Object); + RestoreProperty (colorLine, Object); + RestoreProperty (colorBottom, Object); + RestoreProperty (colorPoint, Object); + RestoreProperty (colorTouchInputLine, Object); + RestoreProperty (colorBackgroundPopUplabel, Object); + RestoreProperty (colorBackgroundYaxis, Object); + RestoreProperty (colorBackgroundXaxis, Object); + RestoreProperty (averageLine.color, Object); + + RestoreProperty (alphaTop, Float); //float is sufficent, and works on 32-bit + RestoreProperty (alphaLine, Float); + RestoreProperty (alphaTouchInputLine, Float); + RestoreProperty (alphaBackgroundXaxis, Float); + RestoreProperty (alphaBackgroundYaxis, Float); + + RestoreProperty (widthLine, Float); + RestoreProperty (widthReferenceLines, Float); + RestoreProperty (sizePoint, Float); + RestoreProperty (widthTouchInputLine, Float); + + RestoreProperty (enableTouchReport, Bool); + RestoreProperty (enablePopUpReport, Bool); + RestoreProperty (enableUserScaling, Bool); + RestoreProperty (enableBezierCurve, Bool); + RestoreProperty (enableXAxisLabel, Bool); + RestoreProperty (enableYAxisLabel, Bool); + RestoreProperty (autoScaleYAxis, Bool); + RestoreProperty (alwaysDisplayDots, Bool); + RestoreProperty (alwaysDisplayPopUpLabels, Bool); + RestoreProperty (enableLeftReferenceAxisFrameLine, Bool); + RestoreProperty (enableBottomReferenceAxisFrameLine, Bool); + RestoreProperty (interpolateNullValues, Bool); + RestoreProperty (adaptiveDataPoints, Bool); + RestoreProperty (displayDotsOnly, Bool); + RestoreProperty (displayDotsWhileAnimating, Bool); + + RestoreProperty (touchReportFingersRequired, Int); + RestoreProperty (formatStringForValues, Object); + + RestoreProperty (averageLine, Object); +#pragma clang diagnostic pop +} + +- (void)encodeRestorableStateWithCoder:(NSCoder *)coder { + [super encodeRestorableStateWithCoder:coder]; + [self encodePropertiesWithCoder:coder]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [self encodePropertiesWithCoder:coder]; +} + +- (void)encodePropertiesWithCoder:(NSCoder *)coder { + +#define EncodeProperty(property, type) [coder encode ## type: self.property forKey:@#property] + + EncodeProperty (labelFont, Object); + EncodeProperty (animationGraphEntranceTime, Double); + EncodeProperty (animationGraphStyle, Integer); + + EncodeProperty (colorXaxisLabel, Object); + EncodeProperty (colorYaxisLabel, Object); + EncodeProperty (colorTop, Object); + EncodeProperty (colorLine, Object); + EncodeProperty (colorBottom, Object); + EncodeProperty (colorPoint, Object); + EncodeProperty (colorTouchInputLine, Object); + EncodeProperty (colorBackgroundPopUplabel, Object); + EncodeProperty (colorBackgroundYaxis, Object); + EncodeProperty (colorBackgroundXaxis, Object); + EncodeProperty (averageLine.color, Object); + + EncodeProperty (alphaTop, Double); + EncodeProperty (alphaLine, Double); + EncodeProperty (alphaTouchInputLine, Double); + EncodeProperty (alphaBackgroundXaxis, Double); + EncodeProperty (alphaBackgroundYaxis, Double); + + EncodeProperty (widthLine, Double); + EncodeProperty (widthReferenceLines, Double); + EncodeProperty (sizePoint, Double); + EncodeProperty (widthTouchInputLine, Double); + + EncodeProperty (enableTouchReport, Bool); + EncodeProperty (enablePopUpReport, Bool); + EncodeProperty (enableUserScaling, Bool); + EncodeProperty (enableBezierCurve, Bool); + EncodeProperty (enableXAxisLabel, Bool); + EncodeProperty (enableYAxisLabel, Bool); + EncodeProperty (autoScaleYAxis, Bool); + EncodeProperty (alwaysDisplayDots, Bool); + EncodeProperty (alwaysDisplayPopUpLabels, Bool); + EncodeProperty (enableLeftReferenceAxisFrameLine, Bool); + EncodeProperty (enableBottomReferenceAxisFrameLine, Bool); + EncodeProperty (enableTopReferenceAxisFrameLine, Bool); + EncodeProperty (enableRightReferenceAxisFrameLine, Bool); + EncodeProperty (interpolateNullValues, Bool); + EncodeProperty (adaptiveDataPoints, Bool); + EncodeProperty (displayDotsOnly, Bool); + EncodeProperty (displayDotsWhileAnimating, Bool); + + [coder encodeInt: (int)(self.touchReportFingersRequired) forKey:@"touchReportFingersRequired"]; + EncodeProperty (formatStringForValues, Object); + EncodeProperty (averageLine, Object); +} + - (void)commonInit { // Do any initialization that's common to both -initWithFrame: and -initWithCoder: in this method @@ -185,7 +315,6 @@ - (void)commonInit { _enableBezierCurve = NO; _enableXAxisLabel = YES; _enableYAxisLabel = NO; - _YAxisLabelXOffset = 0; _autoScaleYAxis = YES; _alwaysDisplayDots = NO; _alwaysDisplayPopUpLabels = NO; @@ -194,33 +323,31 @@ - (void)commonInit { _formatStringForValues = @"%.0f"; _interpolateNullValues = YES; _displayDotsOnly = NO; - - // Initialize the various arrays - xAxisValues = [NSMutableArray array]; - xAxisLabelPoints = [NSMutableArray array]; - yAxisValues = [NSMutableArray array]; - yAxisLabelPoints = [NSMutableArray array]; - dataPoints = [NSMutableArray array]; - _xAxisLabels = [NSMutableArray array]; - _yAxisLabels = [NSMutableArray array]; - _permanentPopups = [NSMutableArray array]; - _circleDots = [NSMutableArray array]; - xAxisHorizontalFringeNegationValue = 0.0; + _adaptiveDataPoints = NO; + _enableUserScaling = NO; + _zoomScale = 1.0; + _panMovement = 0; + _panMovementBase = 0; + _doubleTapScale = 1.0; + _zoomCenterLocation = 0; + _zoomCenterValue = 0; // Initialize BEM Objects _averageLine = [[BEMAverageLine alloc] init]; -} -- (void)prepareForInterfaceBuilder { - // Set points and remove all dots that were previously on the graph - numberOfPoints = 10; - for (UILabel *subview in [self subviews]) { - if ([subview isEqual:self.noDataLabel]) - [subview removeFromSuperview]; - } + if (!self.backgroundYAxis) self.backgroundYAxis = [[UIView alloc] initWithFrame:CGRectZero]; + if (!self.backgroundXAxis) self.backgroundXAxis = [[UIView alloc] initWithFrame:CGRectZero]; + if (!self.masterLine) self.masterLine = [[BEMLine alloc] initWithFrame:CGRectZero]; + if (!self.dotsView) self.dotsView = [[UIView alloc] initWithFrame:CGRectZero]; + if (!self.labelsView) self.labelsView = [[UIView alloc] initWithFrame:CGRectZero]; + + [self addSubview: self.backgroundYAxis]; + [self addSubview: self.backgroundXAxis]; + [self addSubview: self.masterLine]; + [self addSubview: self.dotsView]; + [self addSubview: self.labelsView]; - [self drawEntireGraph]; } - (void)drawGraph { @@ -229,21 +356,23 @@ - (void)drawGraph { [self.delegate lineGraphDidBeginLoading:self]; // Get the number of points in the graph - [self layoutNumberOfPoints]; + self.numberOfPoints = [self getNumberOfPoints]; - if (numberOfPoints <= 1) { - return; + if (self.numberOfPoints <= 0) { + [self showNoData]; } else { + [self clearNoData]; // Draw the graph [self drawEntireGraph]; // Setup the touch report [self layoutTouchReport]; - - // Let the delegate know that the graph finished updates - if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) - [self.delegate lineGraphDidFinishLoading:self]; + [self startUserScaling]; } + // Let the delegate know that the graph finished updates + if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) + [self.delegate lineGraphDidFinishLoading:self]; + [self didFinishDrawingWithAnimation: self.numberOfPoints > 0]; } @@ -256,61 +385,96 @@ - (void)layoutSubviews { [self drawGraph]; } -- (void)layoutNumberOfPoints { - // Get the total number of data points from the delegate - if ([self.dataSource respondsToSelector:@selector(numberOfPointsInLineGraph:)]) { - numberOfPoints = [self.dataSource numberOfPointsInLineGraph:self]; - - } else numberOfPoints = 0; - - // There are no points to load - if (numberOfPoints == 0) { - if (self.delegate && - [self.delegate respondsToSelector:@selector(noDataLabelEnableForLineGraph:)] && - ![self.delegate noDataLabelEnableForLineGraph:self]) return; - - NSLog(@"[BEMSimpleLineGraph] Data source contains no data. A no data label will be displayed and drawing will stop. Add data to the data source and then reload the graph."); +- (void)clearGraph { + for (UIView * subView in self.subviews) { + for (UIView * subSubView in subView.subviews) { + [subSubView removeFromSuperview]; + } + } + self.masterLine.points = @[]; + [self.masterLine setNeedsDisplay]; +} +- (NSInteger)getNumberOfPoints { + // Get the total number of data points from the delegate #ifndef TARGET_INTERFACE_BUILDER - self.noDataLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.viewForFirstBaselineLayout.frame.size.width, self.viewForFirstBaselineLayout.frame.size.height)]; + if ([self.dataSource respondsToSelector:@selector(numberOfPointsInLineGraph:)]) { + return [self.dataSource numberOfPointsInLineGraph:self]; + } else { + return 0; + } #else - self.noDataLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.viewForFirstBaselineLayout.frame.size.width, self.viewForFirstBaselineLayout.frame.size.height-(self.viewForFirstBaselineLayout.frame.size.height/4))]; + return 10; #endif +} - self.noDataLabel.backgroundColor = [UIColor clearColor]; - self.noDataLabel.textAlignment = NSTextAlignmentCenter; +- (void)clearNoData { + [self.noDataLabel removeFromSuperview]; +} -#ifndef TARGET_INTERFACE_BUILDER - NSString *noDataText; - if ([self.delegate respondsToSelector:@selector(noDataLabelTextForLineGraph:)]) { - noDataText = [self.delegate noDataLabelTextForLineGraph:self]; - } - self.noDataLabel.text = noDataText ?: NSLocalizedString(@"No Data", nil); -#else - self.noDataLabel.text = @"Data is not loaded in Interface Builder"; -#endif - self.noDataLabel.font = self.noDataLabelFont ?: [UIFont systemFontOfSize:15 weight:UIFontWeightRegular]; - self.noDataLabel.textColor = self.noDataLabelColor ?: self.colorLine; +- (void)showNoData { + // There are no points to load + [self clearGraph]; + if (self.delegate && + [self.delegate respondsToSelector:@selector(noDataLabelEnableForLineGraph:)] && + ![self.delegate noDataLabelEnableForLineGraph:self]) { + return ; + } + + NSLog(@"[BEMSimpleLineGraph] Data source contains no data. A no data label will be displayed and drawing will stop. Add data to the data source and then reload the graph."); + if (!self.noDataLabel) { + self.noDataLabel = [[UILabel alloc] initWithFrame:self.bounds]; + self.noDataLabel.backgroundColor = [UIColor clearColor]; + self.noDataLabel.textAlignment = NSTextAlignmentCenter; + } + NSString *noDataText = NSLocalizedString(@"No Data", nil); + if ([self.delegate respondsToSelector:@selector(noDataLabelTextForLineGraph:)]) { + noDataText = [self.delegate noDataLabelTextForLineGraph:self]; + } + self.noDataLabel.text = noDataText; + self.noDataLabel.font = self.noDataLabelFont ?: [UIFont preferredFontForTextStyle:UIFontTextStyleTitle1]; + self.noDataLabel.textColor = self.noDataLabelColor ?: (self.colorXaxisLabel ?: [UIColor blackColor]); + + [self addSubview:self.noDataLabel]; +} - [self.viewForFirstBaselineLayout addSubview:self.noDataLabel]; +- (void)startUserScaling { + if (self.enableUserScaling) { + if (!self.zoomGesture) { + _zoomScale = 1.0; + self.zoomGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleZoomGestureAction:)]; + self.zoomGesture.delegate = self; + [self addGestureRecognizer:self.zoomGesture]; - // Let the delegate know that the graph finished layout updates - if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) - [self.delegate lineGraphDidFinishLoading:self]; + self.doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGestureAction:)]; + self.doubleTapGesture.delegate = self; + self.doubleTapGesture.numberOfTapsRequired = 2; + [self.labelsView addGestureRecognizer:self.doubleTapGesture]; - } else if (numberOfPoints == 1) { - NSLog(@"[BEMSimpleLineGraph] Data source contains only one data point. Add more data to the data source and then reload the graph."); - BEMCircle *circleDot = [[BEMCircle alloc] initWithFrame:CGRectMake(0, 0, self.sizePoint, self.sizePoint)]; - circleDot.center = CGPointMake(self.frame.size.width/2, self.frame.size.height/2); - circleDot.color = self.colorPoint; - circleDot.alpha = 1; - [self addSubview:circleDot]; - self.oneDot = circleDot; + self.zoomPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGestureAction:)]; + self.zoomPanGesture.delegate = self; + self.zoomPanGesture.minimumNumberOfTouches = 2; + [self addGestureRecognizer:self.zoomPanGesture]; + } } else { - // Remove all dots that might have previously been on the graph - [self.noDataLabel removeFromSuperview]; - [self.oneDot removeFromSuperview]; + _zoomScale = 1.0; + if (self.zoomPanGesture) { + self.zoomPanGesture.delegate = nil; + [self removeGestureRecognizer:self.zoomPanGesture]; + self.zoomPanGesture = nil; + } + if (self.doubleTapGesture) { + self.doubleTapGesture.delegate = nil; + [self.labelsView removeGestureRecognizer:self.doubleTapGesture]; + self.doubleTapGesture = nil; + } + if (self.zoomGesture) { + self.zoomGesture.delegate = nil; + [self removeGestureRecognizer:self.zoomGesture]; + self.zoomGesture = nil; + [self drawEntireGraph]; // was on, now off, so need to redraw + } } } @@ -319,215 +483,252 @@ - (void)layoutTouchReport { if (self.enableTouchReport == YES || self.enablePopUpReport == YES) { // Initialize the vertical gray line that appears where the user touches the graph. if (!self.touchInputLine) { - self.touchInputLine = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.widthTouchInputLine, self.frame.size.height)]; - self.touchInputLine.backgroundColor = self.colorTouchInputLine; - self.touchInputLine.alpha = 0; - [self addSubview:self.touchInputLine]; + self.touchInputLine = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.widthTouchInputLine, self.labelsView.bounds.size.height)]; } + self.touchInputLine.alpha = 0; + self.touchInputLine.backgroundColor = self.colorTouchInputLine; + [self.labelsView addSubview:self.touchInputLine]; - if (!self.panView) { - self.panView = [[UIView alloc] initWithFrame:CGRectMake(10, 10, self.viewForFirstBaselineLayout.frame.size.width, self.viewForFirstBaselineLayout.frame.size.height)]; - self.panView.backgroundColor = [UIColor clearColor]; - [self.viewForFirstBaselineLayout addSubview:self.panView]; - - self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureAction:)]; - self.panGesture.delegate = self; - [self.panGesture setMaximumNumberOfTouches:1]; - [self.panView addGestureRecognizer:self.panGesture]; + if (!self.touchReportPanGesture) { + self.touchReportPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureAction:)]; + self.touchReportPanGesture.delegate = self; + [self.touchReportPanGesture setMaximumNumberOfTouches:1]; + [self.labelsView addGestureRecognizer:self.touchReportPanGesture]; self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureAction:)]; self.longPressGesture.minimumPressDuration = 0.1f; - [self.panView addGestureRecognizer:self.longPressGesture]; + [self.labelsView addGestureRecognizer:self.longPressGesture]; + } + } else { + [self.touchInputLine removeFromSuperview]; + if (self.touchReportPanGesture) { + self.touchReportPanGesture.delegate = nil; + [self.labelsView removeGestureRecognizer:self.touchReportPanGesture]; + self.touchReportPanGesture = nil; + self.longPressGesture.delegate = nil; + [self.labelsView removeGestureRecognizer: self.longPressGesture]; + self.longPressGesture = nil; } } } #pragma mark - Drawing -- (void)didFinishDrawingIncludingYAxis:(BOOL)yAxisFinishedDrawing { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (self.animationGraphEntranceTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - if (self.enableYAxisLabel == NO) { +- (void)didFinishDrawingWithAnimation:(BOOL)animated { + NSTimeInterval animateTime = animated ? self.animationGraphEntranceTime : 0.01; + if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishDrawing:)]) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t) (animateTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // Let the delegate know that the graph finished rendering - if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishDrawing:)]) - [self.delegate lineGraphDidFinishDrawing:self]; - return; - } else { - if (yAxisFinishedDrawing == YES) { - // Let the delegate know that the graph finished rendering - if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishDrawing:)]) - [self.delegate lineGraphDidFinishDrawing:self]; - return; - } - } - }); + [self.delegate lineGraphDidFinishDrawing:self]; + + }); + } +} + +- (void)divideUpView { + //carves up main view into axes and graph areas + CGRect frameForRest = self.bounds; + + CGRect frameForBackgroundYAxis = CGRectZero; + if (self.enableYAxisLabel) { + CGFloat yAxisWidth = [self calculateWidestLabel] + 6.0f; + CGRectEdge edge = self.positionYAxisRight ? CGRectMaxXEdge : CGRectMinXEdge; + CGRectDivide(self.bounds, &frameForBackgroundYAxis, &frameForRest, yAxisWidth, edge); + } + + CGRect frameForBackgroundXAxis = CGRectZero; + if (self.enableXAxisLabel) { + CGFloat xAxisHeight = self.labelFont.pointSize + 8.0f; + CGRectEdge edge = self.positionXAxisTop ? CGRectMinYEdge : CGRectMaxYEdge; + CGRectDivide(frameForRest, &frameForBackgroundXAxis, &frameForRest, xAxisHeight, edge); + } + + self.backgroundYAxis.frame = frameForBackgroundYAxis; + self.backgroundXAxis.frame = frameForBackgroundXAxis; + self.masterLine.frame = frameForRest; + self.dotsView.frame = frameForRest; + self.labelsView.frame = frameForRest; + + [self addSubview: self.backgroundYAxis]; + [self addSubview: self.backgroundXAxis]; + [self addSubview: self.masterLine]; + [self addSubview: self.dotsView]; + [self addSubview: self.labelsView]; } - (void)drawEntireGraph { // The following method calls are in this specific order for a reason // Changing the order of the method calls below can result in drawing glitches and even crashes - self.averageLine.yValue = NAN; - self.maxValue = [self getMaximumValue]; - self.minValue = [self getMinimumValue]; + self.dataPoints = [self getData]; + [self calculateMinMax]; + + [self divideUpView]; //requires dataPoints and Min/Max + + self.locations = [self layoutPoints]; //requires main views to be sized + + // Draw the Y-Axis + [self drawYAxis]; - // Set the Y-Axis Offset if the Y-Axis is enabled. The offset is relative to the size of the longest label on the Y-Axis. - if (self.enableYAxisLabel) { - self.YAxisLabelXOffset = 2.0f + [self calculateWidestLabel]; - } else { - self.YAxisLabelXOffset = 0; - } // Draw the X-Axis [self drawXAxis]; - // Draw the data points - [self drawDots]; - // Draw line with bottom and top fill [self drawLine]; - // Draw the Y-Axis - [self drawYAxis]; + // Draw the data points and labels + [self drawDots]; + } - (CGFloat)labelWidthForValue:(CGFloat)value { - NSDictionary *attributes = @{NSFontAttributeName: self.labelFont}; + UIFont * labelFont = self.labelFont; + if (!labelFont) return 0; + NSDictionary *attributes = @{NSFontAttributeName: labelFont}; NSString *valueString = [self yAxisTextForValue:value]; NSString *labelString = [valueString stringByReplacingOccurrencesOfString:@"[0-9-]" withString:@"N" options:NSRegularExpressionSearch range:NSMakeRange(0, [valueString length])]; return [labelString sizeWithAttributes:attributes].width; } - (CGFloat)calculateWidestLabel { - NSDictionary *attributes = @{NSFontAttributeName: self.labelFont}; CGFloat widestNumber; if (self.autoScaleYAxis == YES){ - widestNumber = MAX([self labelWidthForValue:self.maxValue], - [self labelWidthForValue:self.minValue]); + widestNumber = MAX([self labelWidthForValue:self.maxYValue], + [self labelWidthForValue:self.minYValue]); + } else { + widestNumber = [self labelWidthForValue:CGRectGetMaxY(self.backgroundYAxis.bounds)] ; + } + if (self.averageLine.enableAverageLine) { + UIFont * labelFont = self.labelFont; + if (!labelFont) return 0; + NSDictionary *attributes = @{NSFontAttributeName: labelFont}; + return MAX(widestNumber, [self.averageLine.title sizeWithAttributes:attributes].width); } else { - widestNumber = [self labelWidthForValue:self.frame.size.height] ; + return widestNumber; } - return MAX(widestNumber, [self.averageLine.title sizeWithAttributes:attributes].width); } +- (BEMCircle *)circleDotAtIndex:(NSUInteger)index { -- (BEMCircle *)circleDotAtIndex:(NSUInteger)index forValue:(CGFloat)dotValue reuseNumber:(NSUInteger)reuseNumber { - CGFloat positionOnXAxis = numberOfPoints > 1 ? - (((self.frame.size.width - self.YAxisLabelXOffset) / (numberOfPoints - 1)) * index) : - self.frame.size.width/2; - if (self.positionYAxisRight == NO) { - positionOnXAxis += self.YAxisLabelXOffset; + BEMCircle * circleDot = nil; + CGRect dotFrame = CGRectMake(0, 0, self.sizePoint, self.sizePoint); + if (index < self.circleDots.count) { + circleDot = self.circleDots[index]; + circleDot.frame = dotFrame; + [circleDot setNeedsDisplay]; + } else { + circleDot = [[BEMCircle alloc] initWithFrame:dotFrame]; } + circleDot.frame = dotFrame; + circleDot.tag = (NSInteger) index + DotFirstTag100; + [self.dotsView addSubview:circleDot]; - CGFloat positionOnYAxis = [self yPositionForDotValue:dotValue]; - - [yAxisValues addObject:@(positionOnYAxis)]; - + CGFloat dotValue = self.dataPoints[index].CGPointValue.y; + circleDot.absoluteValue = dotValue; if (dotValue >= BEMNullGraphValue) { // If we're dealing with an null value, don't draw the dot (but put it in yAxis to interpolate line) - return nil; + [circleDot removeFromSuperview]; + return circleDot; } - BEMCircle *circleDot; - CGRect dotFrame = CGRectMake(0, 0, self.sizePoint, self.sizePoint); - if (reuseNumber < self.circleDots.count) { - circleDot = self.circleDots[reuseNumber]; - circleDot.frame = dotFrame; - } else { - circleDot = [[BEMCircle alloc] initWithFrame:dotFrame]; - [self.circleDots addObject:circleDot]; + CGFloat positionOnXAxis = self.locations[index].CGPointValue.x; + if (positionOnXAxis < -self.sizePoint/2 || positionOnXAxis > self.dotsView.bounds.size.width + self.sizePoint/2 ) { + //off screen so not visible + [circleDot removeFromSuperview]; + return circleDot; } + CGFloat positionOnYAxis = self.locations[index].CGPointValue.y; circleDot.center = CGPointMake(positionOnXAxis, positionOnYAxis); - circleDot.tag = (NSInteger) index + DotFirstTag100; - circleDot.absoluteValue = dotValue; circleDot.color = self.colorPoint; return circleDot; - } - (void)drawDots { - // Remove all data points before adding them to the array - [dataPoints removeAllObjects]; - - // Remove all yAxis values before adding them to the array - [yAxisValues removeAllObjects]; - // Loop through each point and add it to the graph - NSUInteger circleDotNumber = 0; + NSUInteger numPoints = (NSUInteger) self.numberOfPoints; + NSMutableArray *newPopups = [NSMutableArray arrayWithCapacity:numPoints]; + NSMutableArray *newDots = [NSMutableArray arrayWithCapacity:numPoints]; @autoreleasepool { - for (NSUInteger index = 0; index < numberOfPoints; index++) { - CGFloat dotValue = 0; + for (NSUInteger index = 0; index < numPoints; index++) { -#ifndef TARGET_INTERFACE_BUILDER - if ([self.dataSource respondsToSelector:@selector(lineGraph:valueForPointAtIndex:)]) { - dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:index]; + BEMCircle * circleDot = [self circleDotAtIndex: index]; + [newDots addObject:circleDot]; - } else [NSException raise:@"lineGraph:valueForPointAtIndex: protocol method is not implemented in the data source. Throwing exception here before the system throws a CALayerInvalidGeometry Exception." format:@"Value for point %f at index %lu is invalid. CALayer position may contain NaN: [0 nan]", dotValue, (unsigned long)index]; -#else - dotValue = (int)(arc4random() % 10000); -#endif - [dataPoints addObject:@(dotValue)]; + UILabel * label = nil; + if (index < self.permanentPopups.count) { + label = self.permanentPopups[index]; + } else { + label = [[UILabel alloc] initWithFrame:CGRectZero]; + } - BEMCircle *circleDot = [self circleDotAtIndex:index forValue:dotValue reuseNumber:circleDotNumber]; - if (circleDot) { - if (!circleDot.superview) { - [self addSubview:circleDot]; - } + if (circleDot.superview) { - if (self.alwaysDisplayPopUpLabels == YES) { - if (![self.delegate respondsToSelector:@selector(lineGraph:alwaysDisplayPopUpAtIndex:)] || - [self.delegate lineGraph:self alwaysDisplayPopUpAtIndex:index]) { - UILabel *label = (UILabel *)[self labelForPoint:circleDot reuseNumber:circleDotNumber]; - if (label && !label.superview) { - [self addSubview:label]; - } - } + if ((self.alwaysDisplayPopUpLabels == YES) && + (![self.delegate respondsToSelector:@selector(lineGraph:alwaysDisplayPopUpAtIndex:)] || + [self.delegate lineGraph:self alwaysDisplayPopUpAtIndex:(NSInteger)index])) { + label = [self configureLabel:label forPoint: circleDot avoiding: newPopups ]; + + } else { + //not showing labels this time, so remove if any + [label removeFromSuperview]; } - // Dot entrance animation - circleDot.alpha = 0; + // Dot and/or label entrance animation + circleDot.alpha = 0.0f; + label.alpha = 0.0f; + BOOL displayDots = self.alwaysDisplayDots || self.numberOfPoints == 1; if (self.animationGraphEntranceTime <= 0) { - if (self.displayDotsOnly || self.alwaysDisplayDots ) { - circleDot.alpha = 1.0; + if (displayDots) { + circleDot.alpha = 1.0f; } - } else { - if (self.displayDotsWhileAnimating) { - [UIView animateWithDuration: self.animationGraphEntranceTime/numberOfPoints delay: index*(self.animationGraphEntranceTime/numberOfPoints) options:UIViewAnimationOptionCurveLinear animations:^{ - circleDot.alpha = 1.0; - } completion:^(BOOL finished) { - if (self.alwaysDisplayDots == NO && self.displayDotsOnly == NO) { - [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - circleDot.alpha = 0; - } completion:nil]; + if (label) label.alpha = 1.0f; + } else if (!_displayDotsWhileAnimating && displayDots) { + //turn all dots/labels on after main animation. + [UIView animateWithDuration:0.3 + delay:self.animationGraphEntranceTime - 0.3 + options:UIViewAnimationOptionCurveLinear + animations:^{ + circleDot.alpha = 1.0; + label.alpha = 1.0; + } + completion:nil ]; + } else if (label || self.displayDotsWhileAnimating || displayDots) { + [UIView animateWithDuration: MAX(0.3,self.animationGraphEntranceTime/self.numberOfPoints) + delay: self.animationGraphEntranceTime*(circleDot.center.x/CGRectGetMaxX(self.dotsView.bounds)) + options:UIViewAnimationOptionCurveLinear + animations:^{ + if (self.displayDotsWhileAnimating) { + circleDot.alpha = 1.0; + if (label) label.alpha = 1.0; } - }]; - } + } completion:^(BOOL finished) { + if (displayDots != self.displayDotsWhileAnimating || + (label && !self.displayDotsWhileAnimating) ) { + [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + circleDot.alpha = displayDots ? 1 : 0; + if (label) label.alpha = 1.0; + } completion:nil]; + } + }]; } - circleDotNumber++; + + } else { + [label removeFromSuperview]; } + [newPopups addObject:label ]; } - - for (NSUInteger i = self.circleDots.count -1; i>=circleDotNumber; i--) { - [[self.permanentPopups lastObject] removeFromSuperview]; //no harm if not created - [self.permanentPopups removeLastObject]; - [[self.circleDots lastObject] removeFromSuperview]; - [self.circleDots removeLastObject]; + for (NSUInteger i = numPoints; i < self.circleDots.count; i++) { + [self.permanentPopups[i] removeFromSuperview]; //no harm if not showing + [self.circleDots [i] removeFromSuperview]; } - - // Remove popups - [self.popUpLabel removeFromSuperview]; - [self.popUpView removeFromSuperview]; + self.permanentPopups = [newPopups copy]; //save for next time + self.circleDots = [newDots copy]; } } - (void)drawLine { - if (!self.masterLine) { - self.masterLine = [[BEMLine alloc] initWithFrame:[self drawableGraphArea]]; - [self addSubview:self.masterLine]; - } else { - self.masterLine.frame = [self drawableGraphArea]; - [self.masterLine setNeedsDisplay]; - } BEMLine * line = self.masterLine; line.opaque = NO; line.alpha = 1; @@ -542,8 +743,7 @@ - (void)drawLine { line.referenceLineWidth = self.widthReferenceLines > 0.0 ? self.widthReferenceLines : (self.widthLine/2); line.lineAlpha = self.alphaLine; line.bezierCurveIsEnabled = self.enableBezierCurve; - line.arrayOfPoints = yAxisValues; - line.arrayOfValues = self.graphValuesForDataPoints; + line.points = self.locations; line.lineDashPatternForReferenceYAxisLines = self.lineDashPatternForReferenceYAxisLines; line.lineDashPatternForReferenceXAxisLines = self.lineDashPatternForReferenceXAxisLines; line.interpolateNullValues = self.interpolateNullValues; @@ -557,9 +757,9 @@ - (void)drawLine { if (self.enableReferenceXAxisLines || self.enableReferenceYAxisLines) { line.enableReferenceLines = YES; line.referenceLineColor = self.colorReferenceLines; - line.verticalReferenceHorizontalFringeNegation = xAxisHorizontalFringeNegationValue; - line.arrayOfVerticalReferenceLinePoints = self.enableReferenceXAxisLines ? xAxisLabelPoints : nil; - line.arrayOfHorizontalReferenceLinePoints = self.enableReferenceYAxisLines ? yAxisLabelPoints : nil; + //Note that arrayOfVerticalReferenceLinePoints (and horizontal) must be set already by drawYaxis and drawXAxis + } else { + line.enableReferenceLines = NO; } line.color = self.colorLine; @@ -569,290 +769,317 @@ - (void)drawLine { line.animationType = self.animationGraphStyle; if (self.averageLine.enableAverageLine == YES) { - NSNumber *average = [[BEMGraphCalculator sharedCalculator] calculatePointValueAverageOnGraph:self]; - if (isnan(self.averageLine.yValue)) self.averageLine.yValue = average.floatValue; line.averageLineYCoordinate = [self yPositionForDotValue:self.averageLine.yValue]; - line.averageLine = self.averageLine; - } else line.averageLine = self.averageLine; + } + line.averageLine = self.averageLine; - line.disableMainLine = self.displayDotsOnly; + line.disableMainLine = self.displayDotsOnly && self.numberOfPoints > 1; - [self sendSubviewToBack:line]; - [self sendSubviewToBack:self.backgroundXAxis]; + [self.masterLine setNeedsDisplay]; - [self didFinishDrawingIncludingYAxis:NO]; } - (void)drawXAxis { + + for (UILabel * label in self.xAxisLabels) { + [label removeFromSuperview]; + } + self.xAxisLabels = nil; + if (!self.enableXAxisLabel) { [self.backgroundXAxis removeFromSuperview]; - self.backgroundXAxis = nil; - for (UILabel * label in self.xAxisLabels) { - [label removeFromSuperview]; - } - self.xAxisLabels = [NSMutableArray array]; + self.masterLine.arrayOfVerticalReferenceLinePoints = [NSArray array]; return; } - if (![self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)]) return; - - [xAxisValues removeAllObjects]; - xAxisHorizontalFringeNegationValue = 0.0; + if (!([self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)] || + [self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForLocation:atLabelIndex:)])) return; // Draw X-Axis Background Area - if (!self.backgroundXAxis) { - self.backgroundXAxis = [[UIView alloc] initWithFrame:[self drawableXAxisArea]]; - [self addSubview:self.backgroundXAxis]; + + if (self.colorBackgroundXaxis) { + self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis; + self.backgroundXAxis.alpha = self.alphaBackgroundXaxis; } else { - self.backgroundXAxis.frame = [self drawableXAxisArea]; + self.backgroundXAxis.backgroundColor = self.colorBottom; + self.backgroundXAxis.alpha = self.alphaBottom; + } + + //labels can be one of three kinds. + //The default is evenly spaced, indexed, tied to data points, numbered 0, 1, 2... i + //If the datapoint's x-location is specifed with lineGraph:locationForPointAtIndex, then the labels will follow (although now numbered with the x-locations). + //If the function numberOfXAxisLabelsOnLineGraph: is also implemented, then labels move back to evenly spaced. + + NSArray * allLabelLocations = nil; + CGFloat xAxisWidth = CGRectGetWidth(self.backgroundXAxis.bounds); + + if ([self.delegate respondsToSelector:@selector(numberOfXAxisLabelsOnLineGraph:) ]) { + NSInteger numberLabels = [self.delegate numberOfXAxisLabelsOnLineGraph: self]; + if (numberLabels <= 0) numberLabels = 1; + NSMutableArray * labelLocs = [NSMutableArray arrayWithCapacity:(NSUInteger)numberLabels]; + if ([self.dataSource respondsToSelector:@selector(lineGraph:locationForPointAtIndex: )]) { + CGFloat step = xAxisWidth/(numberLabels-1); + CGFloat positionOnXAxis = 0; + for (NSInteger i = 0; i < numberLabels; i++) { + [labelLocs addObject:[NSValue valueWithCGPoint:CGPointMake(positionOnXAxis, 0)]]; + positionOnXAxis += step; + } + } else { + CGFloat indicesDisplayed = self.maxXDisplayedValue - self.minXDisplayedValue; + numberLabels = MIN(numberLabels, (NSInteger)indicesDisplayed+1); + CGFloat step = indicesDisplayed / (numberLabels-1); + CGFloat currIndex = (CGFloat)ceil(self.minXDisplayedValue); + for (NSInteger i = 0; i < numberLabels; i++) { + NSUInteger index = (NSUInteger)currIndex; //default rounding + [labelLocs addObject:self.locations[index]]; + currIndex += step; + } + } + allLabelLocations = [NSArray arrayWithArray:labelLocs]; + } else { + allLabelLocations = self.locations; } - self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis ?: self.colorBottom; - self.backgroundXAxis.alpha = self.alphaBackgroundXaxis; NSArray *axisIndices = nil; if ([self.delegate respondsToSelector:@selector(incrementPositionsForXAxisOnLineGraph:)]) { axisIndices = [self.delegate incrementPositionsForXAxisOnLineGraph:self]; } else { - NSUInteger baseIndex; - NSUInteger increment; + NSInteger baseIndex = 0; + NSInteger increment = 1; if ([self.delegate respondsToSelector:@selector(baseIndexForXAxisOnLineGraph:)] && [self.delegate respondsToSelector:@selector(incrementIndexForXAxisOnLineGraph:)]) { baseIndex = [self.delegate baseIndexForXAxisOnLineGraph:self]; increment = [self.delegate incrementIndexForXAxisOnLineGraph:self]; - } else { - if ([self.delegate respondsToSelector:@selector(numberOfGapsBetweenLabelsOnLineGraph:)]) { - increment = [self.delegate numberOfGapsBetweenLabelsOnLineGraph:self] + 1; - if (increment >= numberOfPoints -1) { - //need at least two points - baseIndex = 0; - increment = numberOfPoints - 1; - } else { - NSUInteger leftGap = increment - 1; - NSUInteger rightGap = numberOfPoints % increment; - NSUInteger offset = (leftGap-rightGap)/2; - baseIndex = increment - 1 - offset; - } - } else { - increment = 1; + } else if ([self.delegate respondsToSelector:@selector(numberOfGapsBetweenLabelsOnLineGraph:)]) { + increment = [self.delegate numberOfGapsBetweenLabelsOnLineGraph:self] + 1; + if (increment >= self.numberOfPoints -1) { + //need at least two points baseIndex = 0; + increment = self.numberOfPoints - 1; + } else { + NSInteger leftGap = increment - 1; + NSInteger rightGap = self.numberOfPoints % increment; + NSInteger offset = (leftGap-rightGap)/2; + baseIndex = increment - 1 - offset; } } + if (increment == 0) increment = 1; + NSMutableArray *values = [NSMutableArray array ]; - NSUInteger index = baseIndex; - while (index < numberOfPoints) { + NSInteger index = baseIndex; + while (index < (NSInteger)allLabelLocations.count) { [values addObject:@(index)]; index += increment; } axisIndices = [values copy]; } - NSUInteger xAxisLabelNumber = 0; + NSMutableArray *newReferenceLinePoints = [NSMutableArray arrayWithCapacity:axisIndices.count];; + NSMutableArray *newAxisLabelTexts = [NSMutableArray arrayWithCapacity:axisIndices.count]; + NSMutableArray *newXAxisLabels = [NSMutableArray arrayWithCapacity:axisIndices.count]; + @autoreleasepool { + BOOL usingLocation = [self.dataSource respondsToSelector:@selector(lineGraph:locationForPointAtIndex: )]; + BOOL locationLabels = usingLocation && [self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForLocation:atLabelIndex:)]; + BOOL indexLabels = !usingLocation && [self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)]; + CGFloat valueRangeWidth = (self.maxXValue - self.minXValue) / self.zoomScale; + for (NSNumber *indexNum in axisIndices) { NSUInteger index = indexNum.unsignedIntegerValue; - NSString *xAxisLabelText = [self xAxisTextForIndex:index]; - - UILabel *labelXAxis = [self xAxisLabelWithText:xAxisLabelText atIndex:index reuseNumber: xAxisLabelNumber]; + if (index >= allLabelLocations.count) continue; + NSString *xAxisLabelText = @""; + CGFloat positionOnXAxis = allLabelLocations[index].CGPointValue.x ; + CGFloat realValue = self.minXDisplayedValue + valueRangeWidth * positionOnXAxis/xAxisWidth; + if (locationLabels) { + if (positionOnXAxis >= -0.01 && positionOnXAxis <= xAxisWidth +.01) { + //have to convert back to value from viewLoc + // CGFloat realValue = [self valueForDisplayPoint:positionOnXAxis]; + xAxisLabelText = [self.dataSource lineGraph:self labelOnXAxisForLocation:realValue atLabelIndex:(NSInteger)index] ?: @""; + } + } else { + NSInteger realIndex = (NSInteger)(round(realValue)); + if (realIndex < 0) realIndex = 0; + if (realIndex >= self.numberOfPoints) realIndex = self.numberOfPoints - 1; + if (indexLabels) { + xAxisLabelText = [self.dataSource lineGraph:self labelOnXAxisForIndex:realIndex ] ?: @""; + } else { + xAxisLabelText = [NSString stringWithFormat:@"%lu", (long)realIndex]; + } + } + [newAxisLabelTexts addObject:xAxisLabelText]; - [xAxisLabelPoints addObject:@(labelXAxis.center.x - (self.positionYAxisRight ? self.YAxisLabelXOffset : 0.0f))]; + UILabel *labelXAxis = [self xAxisLabelWithText:xAxisLabelText atLocation:positionOnXAxis reuseNumber: index]; + [newXAxisLabels addObject:labelXAxis]; - if (!labelXAxis.superview) [self addSubview:labelXAxis]; - [xAxisValues addObject:xAxisLabelText]; - xAxisLabelNumber++; + if (positionOnXAxis >= -0.01 && positionOnXAxis <= xAxisWidth + .01) { + [self.backgroundXAxis addSubview:labelXAxis]; + if (self.enableReferenceXAxisLines && + (allLabelLocations[index].CGPointValue.y < BEMNullGraphValue || self.interpolateNullValues)) { + [newReferenceLinePoints addObject:@(positionOnXAxis)]; + } + } } } - for (NSUInteger i = self.xAxisLabels.count -1; i>=xAxisLabelNumber; i--) { - [[self.xAxisLabels lastObject] removeFromSuperview]; - [self.xAxisLabels removeLastObject]; - } + self.xAxisLabels = [newXAxisLabels copy]; + self.xAxisLabelTexts = [newAxisLabelTexts copy]; + self.masterLine.arrayOfVerticalReferenceLinePoints = [newReferenceLinePoints copy]; - __block UILabel *prevLabel; + UILabel *prevLabel = nil; - NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:self.xAxisLabels.count]; - [self.xAxisLabels enumerateObjectsUsingBlock:^(UILabel *label, NSUInteger idx, BOOL *stop) { - if (idx == 0) { + for (UILabel *label in self.xAxisLabels) { + if (label == self.xAxisLabels[0]) { prevLabel = label; //always show first label } else if (label.superview) { //only look at active labels - if (CGRectIsNull(CGRectIntersection(prevLabel.frame, label.frame)) && - CGRectContainsRect(self.backgroundXAxis.frame, label.frame)) { + //allow at least five points betwen labels + if (CGRectGetMaxX(prevLabel.frame) + 5 < CGRectGetMinX( label.frame)) { prevLabel = label; //no overlap and inside frame, so show this one } else { - // NSLog(@"Not showing %@ due to %@; label: %@, width: %@ prevLabel: %@, frame: %@", - // label.text, - // CGRectIsNull(CGRectIntersection(prevLabel.frame, label.frame)) ?@"Overlap" : @"Out of bounds", - // NSStringFromCGRect(label.frame), - // @(CGRectGetMaxX(label.frame)), - // NSStringFromCGRect(prevLabel.frame), - // NSStringFromCGRect(self.backgroundXAxis.frame)); - [overlapLabels addObject:label]; // Overlapped + [label removeFromSuperview]; // Overlapped } } - }]; - - for (UILabel *l in overlapLabels) { - [l removeFromSuperview]; - } + }; } -- (NSString *)xAxisTextForIndex:(NSUInteger)index { - NSString *xAxisLabelText = @""; - - if ([self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)]) { - xAxisLabelText = [self.dataSource lineGraph:self labelOnXAxisForIndex:index]; - } else { - xAxisLabelText = @""; - } - - return xAxisLabelText; -} - -- (UILabel *)xAxisLabelWithText:(NSString *)text atIndex:(NSUInteger)index reuseNumber:(NSUInteger) xAxisLabelNumber{ +- (UILabel *)xAxisLabelWithText:(NSString *)text atLocation:(CGFloat)positionOnXAxis reuseNumber:(NSUInteger)xAxisLabelNumber{ UILabel *labelXAxis; if (xAxisLabelNumber < self.xAxisLabels.count) { labelXAxis = self.xAxisLabels[xAxisLabelNumber]; } else { labelXAxis = [[UILabel alloc] init]; - [self.xAxisLabels addObject:labelXAxis]; } labelXAxis.text = text; labelXAxis.font = self.labelFont; - labelXAxis.textAlignment = 1; + labelXAxis.textAlignment = NSTextAlignmentCenter; labelXAxis.textColor = self.colorXaxisLabel; labelXAxis.backgroundColor = [UIColor clearColor]; // Add support multi-line, but this might overlap with the graph line if text have too many lines labelXAxis.numberOfLines = 0; - CGRect lRect = [labelXAxis.text boundingRectWithSize:self.viewForFirstBaselineLayout.frame.size options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:labelXAxis.font} context:nil]; + CGRect lRect = [labelXAxis.text boundingRectWithSize:self.backgroundXAxis.bounds.size options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:labelXAxis.font} context:nil]; + CGFloat halfWidth = lRect.size.width/2; - // Determine the horizontal translation to perform on the far left and far right labels - // This property is negated when calculating the position of reference frames - CGFloat horizontalTranslation; - if (index == 0) { - horizontalTranslation = lRect.size.width/2; - } else if (index+1 == numberOfPoints) { - horizontalTranslation = -lRect.size.width/2; - } else horizontalTranslation = 0; - xAxisHorizontalFringeNegationValue = horizontalTranslation; - - // Determine the final x-axis position - CGFloat positionOnXAxis = (((self.frame.size.width - self.YAxisLabelXOffset) / (numberOfPoints - 1)) * index) + horizontalTranslation; - if (!self.positionYAxisRight) { - positionOnXAxis += self.YAxisLabelXOffset; + //if labels are partially on screen, nudge onto screen + if (positionOnXAxis + halfWidth >= 0 && positionOnXAxis <= halfWidth) { + positionOnXAxis = halfWidth; + } else { + CGFloat rightEdge = CGRectGetMaxX(self.backgroundXAxis.bounds) ; + if (positionOnXAxis - halfWidth <= rightEdge && positionOnXAxis > rightEdge - halfWidth-.01f) { + positionOnXAxis = rightEdge - halfWidth-.01f; + } } - labelXAxis.frame = lRect; - labelXAxis.center = CGPointMake(positionOnXAxis, self.frame.size.height - lRect.size.height/2.0f-1.0f); + labelXAxis.center = CGPointMake(positionOnXAxis, CGRectGetMidY(self.backgroundXAxis.bounds)); return labelXAxis; } - (NSString *)yAxisTextForValue:(CGFloat)value { + if ([self.dataSource respondsToSelector:@selector(lineGraph:labelOnYAxisForValue:)]) { + return [self.dataSource lineGraph:self labelOnYAxisForValue:value]; + } + NSString *yAxisSuffix = @""; NSString *yAxisPrefix = @""; - if ([self.delegate respondsToSelector:@selector(yAxisPrefixOnLineGraph:)]) yAxisPrefix = [self.delegate yAxisPrefixOnLineGraph:self]; if ([self.delegate respondsToSelector:@selector(yAxisSuffixOnLineGraph:)]) yAxisSuffix = [self.delegate yAxisSuffixOnLineGraph:self]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" - NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value]; + NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value]; #pragma clang diagnostic pop return [NSString stringWithFormat:@"%@%@%@", yAxisPrefix, formattedValue, yAxisSuffix]; } -- (UILabel *)yAxisLabelWithText:(NSString *)text atValue:(CGFloat)value reuseNumber:(NSUInteger) reuseNumber { +- (UILabel *)yAxisLabelWithText:(NSString *)text atValue:(CGFloat)value reuseNumber:(NSInteger)reuseNumber { //provide a Y-Axis Label with text at Value, reusing reuseNumber'd label if it exists //special case: use self.Averageline.label if reuseNumber = NSIntegerMax - CGFloat labelHeight = self.labelFont.pointSize + 7.0f; - CGRect frameForLabelYAxis = CGRectMake(1.0f, 0.0f, self.YAxisLabelXOffset - 1.0f, labelHeight); + CGFloat labelHeight = self.labelFont.lineHeight + 2.0f; + CGFloat backgroundWidth = self.backgroundYAxis.bounds.size.width - 1.0f; + CGRect frameForLabelYAxis = CGRectMake(1.0f, 0.0f, backgroundWidth, labelHeight); - CGFloat xValueForCenterLabelYAxis = (self.YAxisLabelXOffset-1.0f) /2.0f; + CGFloat xValueForCenterLabelYAxis = backgroundWidth /2.0f; NSTextAlignment textAlignmentForLabelYAxis = NSTextAlignmentRight; - if (self.positionYAxisRight) { - frameForLabelYAxis.origin = CGPointMake(self.frame.size.width - self.YAxisLabelXOffset - 1.0f, 0.0f); - xValueForCenterLabelYAxis = self.frame.size.width - xValueForCenterLabelYAxis; - } UILabel *labelYAxis; - if (reuseNumber == NSIntegerMax) { + if ( reuseNumber == NSIntegerMax) { if (!self.averageLine.label) { self.averageLine.label = [[UILabel alloc] initWithFrame:frameForLabelYAxis]; } labelYAxis = self.averageLine.label; - } else if (reuseNumber < self.yAxisLabels.count) { - labelYAxis = self.yAxisLabels[reuseNumber]; + } else if (reuseNumber < (NSInteger)self.yAxisLabels.count && reuseNumber >= 0) { + labelYAxis = self.yAxisLabels[(NSUInteger)reuseNumber]; } else { labelYAxis = [[UILabel alloc] initWithFrame:frameForLabelYAxis]; - [self.yAxisLabels addObject:labelYAxis]; } + + labelYAxis.frame = frameForLabelYAxis; labelYAxis.text = text; labelYAxis.textAlignment = textAlignmentForLabelYAxis; labelYAxis.font = self.labelFont; labelYAxis.textColor = self.colorYaxisLabel; labelYAxis.backgroundColor = [UIColor clearColor]; CGFloat yAxisPosition = [self yPositionForDotValue:value]; + if (self.enableXAxisLabel && self.positionXAxisTop) { + //because y axis frame is independent of xaxis presence (as opposed to dot view), we have to adjust here + yAxisPosition += CGRectGetHeight(self.backgroundXAxis.frame); + } + CGFloat halfLabel = labelHeight/2; + CGFloat topEdge = CGRectGetMaxY(self.backgroundYAxis.bounds); + //nudge partially visible top/bottom labels onto screen + if (yAxisPosition > -halfLabel && yAxisPosition < halfLabel) { + yAxisPosition = halfLabel; + } else if (yAxisPosition > topEdge - halfLabel && yAxisPosition < topEdge +halfLabel) { + yAxisPosition = topEdge - halfLabel; + } labelYAxis.center = CGPointMake(xValueForCenterLabelYAxis, yAxisPosition); - NSNumber *yAxisLabelCoordinate = @(labelYAxis.center.y); - [yAxisLabelPoints addObject:yAxisLabelCoordinate]; + return labelYAxis; } - (void)drawYAxis { + if (!self.enableYAxisLabel) { [self.backgroundYAxis removeFromSuperview]; - self.backgroundYAxis = nil; + [self.averageLine.label removeFromSuperview]; + self.averageLine.label = nil; for (UILabel * label in self.yAxisLabels) { [label removeFromSuperview]; } - self.yAxisLabels = [NSMutableArray array]; + self.yAxisLabels = nil; return; } //Make Background for Y Axis - CGRect frameForBackgroundYAxis = CGRectMake( - (self.positionYAxisRight ? - self.frame.size.width - self.YAxisLabelXOffset - 1: - 1), - 0, - self.YAxisLabelXOffset -1, - self.frame.size.height); - - if (!self.backgroundYAxis) { - self.backgroundYAxis= [[UIView alloc] initWithFrame:frameForBackgroundYAxis]; - [self addSubview:self.backgroundYAxis]; + if (self.colorBackgroundYaxis) { + self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis; + self.backgroundYAxis.alpha = self.alphaBackgroundYaxis; } else { - self.backgroundYAxis.frame = frameForBackgroundYAxis; + self.backgroundYAxis.backgroundColor = self.colorTop; + self.backgroundYAxis.alpha = self.alphaTop; } - self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis ?: self.colorTop; - self.backgroundYAxis.alpha = self.alphaBackgroundYaxis; - - [yAxisLabelPoints removeAllObjects]; - NSUInteger numberOfLabels = 3; + NSInteger numberOfLabels = 3; if ([self.delegate respondsToSelector:@selector(numberOfYAxisLabelsOnLineGraph:)]) { numberOfLabels = [self.delegate numberOfYAxisLabelsOnLineGraph:self]; if (numberOfLabels <= 0) return; } //Now calculate baseValue and increment for all scenarios - CGFloat value; + CGFloat dotValue; CGFloat increment; if (self.autoScaleYAxis) { // Plot according to min-max range - NSNumber *minimumNumber = [[BEMGraphCalculator sharedCalculator] calculateMinimumPointValueOnGraph:self]; - NSNumber *maximumNumber = [[BEMGraphCalculator sharedCalculator] calculateMaximumPointValueOnGraph:self]; - CGFloat minValue = minimumNumber.floatValue; - CGFloat maxValue = maximumNumber.floatValue; if (numberOfLabels == 1) { - value = (minValue + maxValue)/2.0f; + dotValue = (self.minYValue + self.maxYValue)/2.0f; increment = 0; //NA } else { - value = minValue; - increment = (maxValue - minValue)/(numberOfLabels-1); + dotValue = self.minYValue; + increment = (self.maxYValue - self.minYValue)/(numberOfLabels-1); if ([self.delegate respondsToSelector:@selector(baseValueForYAxisOnLineGraph:)] && [self.delegate respondsToSelector:@selector(incrementValueForYAxisOnLineGraph:)]) { - value = [self.delegate baseValueForYAxisOnLineGraph:self]; + dotValue = [self.delegate baseValueForYAxisOnLineGraph:self]; increment = [self.delegate incrementValueForYAxisOnLineGraph:self]; - numberOfLabels = (NSUInteger) ((maxValue - value)/increment)+1; + if (increment <= 0) increment = 1; + numberOfLabels = (NSInteger)((self.maxYValue - dotValue)/increment)+1; if (numberOfLabels > 100) { NSLog(@"[BEMSimpleLineGraph] Increment does not properly lay out Y axis, bailing early"); return; @@ -861,333 +1088,210 @@ - (void)drawYAxis { } } else { //not AutoScale - CGFloat graphHeight = self.frame.size.height - self.XAxisLabelYOffset; + CGFloat graphHeight = self.backgroundYAxis.bounds.size.height; if (numberOfLabels == 1) { - value = graphHeight/2.0f; + dotValue = graphHeight/2.0f; increment = 0; //NA } else { increment = graphHeight / numberOfLabels; - value = increment/2; + dotValue = increment/2; } } - NSMutableArray *dotValues = [[NSMutableArray alloc] initWithCapacity:numberOfLabels]; - for (NSUInteger i = 0; i < numberOfLabels; i++) { - [dotValues addObject:@(value)]; - value += increment; - } - NSUInteger yAxisLabelNumber = 0; + NSMutableArray *newLabels = [NSMutableArray arrayWithCapacity:(NSUInteger)numberOfLabels]; + NSMutableArray *newPoints = [NSMutableArray arrayWithCapacity:(NSUInteger)numberOfLabels]; + @autoreleasepool { - for (NSNumber *dotValueNum in dotValues) { - CGFloat dotValue = dotValueNum.floatValue; + for (NSInteger index = 0; index < numberOfLabels; index++) { NSString *labelText = [self yAxisTextForValue:dotValue]; UILabel *labelYAxis = [self yAxisLabelWithText:labelText atValue:dotValue - reuseNumber:yAxisLabelNumber]; + reuseNumber:index]; - if (!labelYAxis.superview) [self addSubview:labelYAxis]; - yAxisLabelNumber++; + [self.backgroundYAxis addSubview:labelYAxis]; + [newLabels addObject:labelYAxis]; + if (self.enableReferenceYAxisLines) { + [newPoints addObject:@(labelYAxis.center.y)]; + } + dotValue += increment; } } - - for (NSUInteger i = self.yAxisLabels.count -1; i>=yAxisLabelNumber; i--) { - [[self.yAxisLabels lastObject] removeFromSuperview]; - [self.yAxisLabels removeLastObject]; + for (NSUInteger index = (NSUInteger)numberOfLabels; index < self.yAxisLabels.count ; index++) { + [self.yAxisLabels[index] removeFromSuperview]; } - // Detect overlapped labels - __block UILabel * prevLabel = nil;; - NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:0]; + UILabel * averageLabel = nil; + if (self.averageLine.enableAverageLine && self.averageLine.title.length > 0 && self.numberOfPoints > 1) { + self.averageLine.yValue = self.getAverageValue; + averageLabel = [self yAxisLabelWithText:self.averageLine.title + atValue:self.averageLine.yValue + reuseNumber:NSIntegerMax]; - [self.yAxisLabels enumerateObjectsUsingBlock:^(UILabel *label, NSUInteger idx, BOOL *stop) { + [self.backgroundYAxis addSubview:averageLabel]; + } else { + [self.averageLine.label removeFromSuperview]; + } + self.averageLine.label = averageLabel; - if (idx == 0) { - prevLabel = label; //always show first label - } else if (label.superview) { //only look at active labels - if (CGRectIsNull(CGRectIntersection(prevLabel.frame, label.frame)) && - CGRectContainsRect(self.backgroundYAxis.bounds, label.frame)) { - prevLabel = label; //no overlap and inside frame, so show this one - } else { - [overlapLabels addObject:label]; // Overlapped -// NSLog(@"Not showing %@ due to %@; label: %@, width: %@ prevLabel: %@, frame: %@", -// label.text, -// CGRectIsNull(CGRectIntersection(prevLabel.frame, label.frame)) ?@"Overlap" : @"Out of bounds", -// NSStringFromCGRect(label.frame), -// @(CGRectGetMaxX(label.frame)), -// NSStringFromCGRect(prevLabel.frame), -// NSStringFromCGRect(self.backgroundXAxis.frame)); - } + // Detect overlapped labels + UILabel * prevLabel = nil; + for (UILabel * label in newLabels) { + if (label == newLabels[0] || //always show first label + (CGRectIsNull(CGRectIntersection(prevLabel.frame, label.frame)) && + (!averageLabel || + CGRectIsNull(CGRectIntersection(averageLabel.frame, label.frame))) && + CGRectContainsRect(self.backgroundYAxis.bounds, label.frame))) { + prevLabel = label; //no overlap and inside frame, so show this one + } else { + [label removeFromSuperview]; // Overlapped } - }]; + }; + self.yAxisLabels = [newLabels copy]; + self.masterLine.arrayOfHorizontalReferenceLinePoints = [newPoints copy]; - if (self.averageLine.enableAverageLine && self.averageLine.title.length > 0) { - - UILabel *averageLabel = [self yAxisLabelWithText:self.averageLine.title - atValue:self.averageLine.yValue - reuseNumber:NSIntegerMax]; - - if (!averageLabel.superview) [self addSubview:averageLabel]; - - //check for overlap; Average wins - for (UILabel * label in self.yAxisLabels) { - if (! CGRectIsNull(CGRectIntersection(averageLabel.frame, label.frame))) { - [overlapLabels addObject:label]; - } - } - } +} - for (UILabel *label in overlapLabels) { - [label removeFromSuperview]; - } - [self didFinishDrawingIncludingYAxis:YES]; -} - -/// Area on the graph that doesn't include the axes -- (CGRect)drawableGraphArea { - // CGRectMake(xAxisXPositionFirstOffset, self.frame.size.height-20, viewWidth/2, 20); - CGFloat xAxisHeight = self.labelFont.pointSize + 8.0f; - CGFloat xOrigin = self.positionYAxisRight ? 0 : self.YAxisLabelXOffset; - CGFloat viewWidth = self.frame.size.width - self.YAxisLabelXOffset; - CGFloat adjustedHeight = self.bounds.size.height - xAxisHeight; - - CGRect rect = CGRectMake(xOrigin, 0, viewWidth, adjustedHeight); - return rect; -} - -- (CGRect)drawableXAxisArea { - CGFloat xAxisHeight = self.labelFont.pointSize + 8.0f; - CGFloat xAxisWidth = [self drawableGraphArea].size.width + 1; - CGFloat xAxisXOrigin = self.positionYAxisRight ? 0 : self.YAxisLabelXOffset; - CGFloat xAxisYOrigin = self.bounds.size.height - xAxisHeight; - return CGRectMake(xAxisXOrigin, xAxisYOrigin, xAxisWidth, xAxisHeight); -} - -- (UIView *)labelForPoint:(BEMCircle *)circleDot reuseNumber:(NSUInteger)reuseNumber { - // If the reuse number is NSIntegerMax, then we've got a popup from a touch gesture. - BOOL touchPopUp = reuseNumber == NSIntegerMax; - NSUInteger index = (NSUInteger) circleDot.tag - DotFirstTag100; - - if (touchPopUp) { - // If the popup is from a touch gesture, simply add it to our view hierarchy. - if (!self.popUpLabel) self.popUpLabel = [[UILabel alloc] init]; - } else return [self permanentLabel:circleDot reuseNumber:reuseNumber]; - - // Check if the delegate has provided a custom popup view - if ([self.delegate respondsToSelector:@selector(popUpViewForLineGraph:)]) { - // The delegate has provided a custom popup. Lets retrieve it and - if (!self.popUpView) self.popUpView = [self.delegate popUpViewForLineGraph:self]; - self.usingCustomPopupView = YES; - - // Fix any left / right layout issues - CGFloat xCenter = circleDot.center.x; - CGFloat halfLabelWidth = self.popUpView.frame.size.width/2 ; - if (self.enableYAxisLabel && !self.positionYAxisRight && ((xCenter - halfLabelWidth) <= self.YAxisLabelXOffset) ) { - // When bumping into left Y axis - xCenter = halfLabelWidth + self.YAxisLabelXOffset + 4.0f; - } else if (self.enableYAxisLabel && self.positionYAxisRight && (xCenter + halfLabelWidth >= self.frame.size.width - self.YAxisLabelXOffset)) { - // When bumping into right Y axis - xCenter = self.frame.size.width - halfLabelWidth - self.YAxisLabelXOffset - 4.0f; - } else if (xCenter - halfLabelWidth <= 0) { - // When over left edge - xCenter = halfLabelWidth + 4.0f; - } else if (xCenter + halfLabelWidth >= self.frame.size.width) { - // When over right edge - xCenter = self.frame.size.width - halfLabelWidth; - } - - // Now set the Y directions. The default is over point. - CGFloat halfLabelHeight = self.popUpView.frame.size.height/2.0f; - CGFloat yCenter = circleDot.frame.origin.y - 2.0f - halfLabelHeight; - self.popUpView.center = CGPointMake(xCenter, yCenter); - - if (!self.popUpView.superview) { - self.popUpView.alpha = 0.0f; - - [UIView animateWithDuration:0.2f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - [self addSubview:self.popUpView]; - self.popUpView.alpha = 1.0f; - } completion:nil]; - } +- (UILabel *)configureLabel:(UILabel *)oldLabel forPoint:(BEMCircle *)circleDot avoiding:(NSArray *)previousLabels { - return self.popUpView; - } else { - self.usingCustomPopupView = NO; - - // Set the basic parameters for the popup label - self.popUpLabel.textAlignment = NSTextAlignmentCenter; - self.popUpLabel.numberOfLines = 0; - self.popUpLabel.font = self.labelFont; - self.popUpLabel.backgroundColor = [UIColor clearColor]; - self.popUpLabel.layer.backgroundColor = [self.colorBackgroundPopUplabel colorWithAlphaComponent:0.7f].CGColor; - self.popUpLabel.layer.cornerRadius = 6; - - // Populate the popup label text with values - self.popUpLabel.text = nil; - if ([self.delegate respondsToSelector:@selector(popUpTextForlineGraph:atIndex:)]) self.popUpLabel.text = [self.delegate popUpTextForlineGraph:self atIndex:index]; - - // If the supplied popup label text is nil we can proceed to fill out the text using suffixes, prefixes, and the graph's data source. - if (self.popUpLabel.text == nil) { - NSString *prefix = @""; - NSString *suffix = @""; - - if ([self.delegate respondsToSelector:@selector(popUpSuffixForlineGraph:)]) - suffix = [self.delegate popUpSuffixForlineGraph:self]; - - if ([self.delegate respondsToSelector:@selector(popUpPrefixForlineGraph:)]) - prefix = [self.delegate popUpPrefixForlineGraph:self]; - - NSNumber *value = (index <= dataPoints.count) ? value = dataPoints[index] : @(0); // @((NSInteger) circleDot.absoluteValue) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wformat-nonliteral" - NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value.doubleValue]; -#pragma clang diagnostic pop - self.popUpLabel.text = [NSString stringWithFormat:@"%@%@%@", prefix, formattedValue, suffix]; - } - - // Set the size and frame of the popup - [self.popUpLabel sizeToFit]; - self.popUpLabel.frame = CGRectMake(0, 0, self.popUpLabel.frame.size.width + 7, self.popUpLabel.frame.size.height + 2); - - // Fix any left / right layout issues - CGFloat xCenter = circleDot.center.x; - CGFloat halfLabelWidth = self.popUpLabel.frame.size.width/2 ; - if (self.enableYAxisLabel && !self.positionYAxisRight && ((xCenter - halfLabelWidth) <= self.YAxisLabelXOffset) ) { - // When bumping into left Y axis - xCenter = halfLabelWidth + self.YAxisLabelXOffset + 4.0f; - } else if (self.enableYAxisLabel && self.positionYAxisRight && (xCenter + halfLabelWidth >= self.frame.size.width - self.YAxisLabelXOffset)) { - // When bumping into right Y axis - xCenter = self.frame.size.width - halfLabelWidth - self.YAxisLabelXOffset - 4.0f; - } else if (xCenter - halfLabelWidth <= 0) { - // When over left edge - xCenter = halfLabelWidth + 4.0f; - } else if (xCenter + halfLabelWidth >= self.frame.size.width) { - // When over right edge - xCenter = self.frame.size.width - halfLabelWidth; - } - - // Now set the Y directions. The default is over point. - CGFloat halfLabelHeight = self.popUpLabel.frame.size.height/2.0f; - CGFloat yCenter = circleDot.frame.origin.y - 2.0f - halfLabelHeight; - self.popUpLabel.center = CGPointMake(xCenter, yCenter); - - if (!self.popUpLabel.superview) { - self.popUpLabel.alpha = 0.0f; - - [UIView animateWithDuration:0.2f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - [self addSubview:self.popUpLabel]; - self.popUpLabel.alpha = 1.0f; - } completion:nil]; - } - - return self.popUpLabel; + UILabel *newPopUpLabel = oldLabel; + if ( !newPopUpLabel) { + newPopUpLabel =[[UILabel alloc] init]; + newPopUpLabel.alpha = 0; } -} -- (UIView *)permanentLabel:(BEMCircle *)circleDot reuseNumber:(NSUInteger)reuseNumber { - // If the reuse number is NSIntegerMax, then we've got a popup from a touch gesture. - NSUInteger index = (NSUInteger) circleDot.tag - DotFirstTag100; - - UILabel *newPopUpLabel; - if (reuseNumber < self.permanentPopups.count) { - newPopUpLabel = self.permanentPopups[reuseNumber]; - } else { - newPopUpLabel = [[UILabel alloc] init]; - [self.permanentPopups addObject:newPopUpLabel]; - } - - self.usingCustomPopupView = NO; - - // Set the basic parameters for the popup label newPopUpLabel.textAlignment = NSTextAlignmentCenter; newPopUpLabel.numberOfLines = 0; newPopUpLabel.font = self.labelFont; newPopUpLabel.backgroundColor = [UIColor clearColor]; newPopUpLabel.layer.backgroundColor = [self.colorBackgroundPopUplabel colorWithAlphaComponent:0.7f].CGColor; newPopUpLabel.layer.cornerRadius = 6; - + + NSInteger index = circleDot.tag - DotFirstTag100; + // Populate the popup label text with values newPopUpLabel.text = nil; if ([self.delegate respondsToSelector:@selector(popUpTextForlineGraph:atIndex:)]) newPopUpLabel.text = [self.delegate popUpTextForlineGraph:self atIndex:index]; - + // If the supplied popup label text is nil we can proceed to fill out the text using suffixes, prefixes, and the graph's data source. if (newPopUpLabel.text == nil) { NSString *prefix = @""; NSString *suffix = @""; - + if ([self.delegate respondsToSelector:@selector(popUpSuffixForlineGraph:)]) suffix = [self.delegate popUpSuffixForlineGraph:self]; - + if ([self.delegate respondsToSelector:@selector(popUpPrefixForlineGraph:)]) prefix = [self.delegate popUpPrefixForlineGraph:self]; - - NSNumber *value = (index <= dataPoints.count) ? value = dataPoints[index] : @(0); // @((NSInteger) circleDot.absoluteValue) + + double value = (index <= (NSInteger)self.dataPoints.count && index >= 0) ? self.dataPoints[(NSUInteger)index].CGPointValue.y : 0; // @((NSInteger) circleDot.absoluteValue) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" - NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value.doubleValue]; + //note this can indeed crash if delegate provides junk for formatString (e.g. %@); try/catch doesn't work + NSString *formattedValue = [NSString stringWithFormat:self.formatStringForValues, value]; #pragma clang diagnostic pop newPopUpLabel.text = [NSString stringWithFormat:@"%@%@%@", prefix, formattedValue, suffix]; } - - // Set the size and frame of the popup - [newPopUpLabel sizeToFit]; - newPopUpLabel.frame = CGRectMake(0, 0, newPopUpLabel.frame.size.width + 7, newPopUpLabel.frame.size.height + 2); - - // Fix any left / right layout issues - CGFloat xCenter = circleDot.center.x; - CGFloat halfLabelWidth = newPopUpLabel.frame.size.width/2 ; - if (self.enableYAxisLabel && !self.positionYAxisRight && ((xCenter - halfLabelWidth) <= self.YAxisLabelXOffset) ) { - // When bumping into left Y axis - xCenter = halfLabelWidth + self.YAxisLabelXOffset + 4.0f; - } else if (self.enableYAxisLabel && self.positionYAxisRight && (xCenter + halfLabelWidth >= self.frame.size.width - self.YAxisLabelXOffset)) { - // When bumping into right Y axis - xCenter = self.frame.size.width - halfLabelWidth - self.YAxisLabelXOffset - 4.0f; - } else if (xCenter - halfLabelWidth <= 0) { - // When over left edge + CGSize requiredSize = [newPopUpLabel sizeThatFits:CGSizeMake(100.0f, CGFLOAT_MAX)]; + newPopUpLabel.frame = CGRectMake(10, 10, requiredSize.width+10.0f, requiredSize.height+10.0f); + + [self adjustXLocForLabel:newPopUpLabel avoidingDot:circleDot.frame]; + + if ( [self adjustYLocForLabel:newPopUpLabel + atIndex:index + avoidingDot:circleDot.frame + andLabels: previousLabels] ) { + [self.labelsView addSubview:newPopUpLabel]; + } else { + [newPopUpLabel removeFromSuperview]; + } + + return newPopUpLabel; +} + +- (void)adjustXLocForLabel:(UIView *)popUpLabel avoidingDot:(CGRect)circleDotFrame { + + //now fixup left/right layout issues + CGFloat xCenter = CGRectGetMidX(circleDotFrame); + CGFloat halfLabelWidth = popUpLabel.frame.size.width/2 ; + CGFloat rightEdge = CGRectGetMaxX(self.labelsView.bounds); + if ((xCenter - halfLabelWidth <= 0) && (xCenter + halfLabelWidth > 0)) { + //When bumping into left Y axis or edge, but not all the way off xCenter = halfLabelWidth + 4.0f; - } else if (xCenter + halfLabelWidth >= self.frame.size.width) { - // When over right edge - xCenter = self.frame.size.width - halfLabelWidth; + } else if ((xCenter + halfLabelWidth >= rightEdge) && (xCenter + halfLabelWidth > rightEdge)) { + //When bumping into right Y axis or edge, but not all the way off + xCenter = rightEdge - halfLabelWidth - 4.0f; } - - // Now set the Y directions. The default is over point. - CGFloat halfLabelHeight = newPopUpLabel.frame.size.height/2.0f; - CGFloat yCenter = circleDot.frame.origin.y - 2.0f - halfLabelHeight; - newPopUpLabel.center = CGPointMake(xCenter, yCenter); - - // Check for bumping into top OR overlap with left neighbors if this is not a touch-driven popup. Remember, the user can only touch one popup at time, so it doesn't matter if there's overlap with those. - CGRect leftNeighborFrame = (reuseNumber >= 1) ? self.permanentPopups[reuseNumber-1].frame : CGRectNull; - CGRect secondNeighborFrame = (reuseNumber >= 2) ? self.permanentPopups[reuseNumber-2].frame : CGRectNull; - if (CGRectGetMinY(newPopUpLabel.frame) < 2.0f || - (!CGRectIsNull(leftNeighborFrame) && !CGRectIsNull(CGRectIntersection(newPopUpLabel.frame, leftNeighborFrame))) || - (!CGRectIsNull(secondNeighborFrame) && !CGRectIsNull(CGRectIntersection(newPopUpLabel.frame, secondNeighborFrame)))) { - // If so, try below point instead - CGRect frame = newPopUpLabel.frame; - frame.origin.y = CGRectGetMaxY(circleDot.frame)+2.0f; - newPopUpLabel.frame = frame; - // Check for bottom and again for overlap with neighbor and even neighbor second to the left - if (CGRectGetMaxY(frame) > (self.frame.size.height - self.XAxisLabelYOffset) || - (!CGRectIsNull(leftNeighborFrame) && !CGRectIsNull(CGRectIntersection(newPopUpLabel.frame, leftNeighborFrame))) || - (!CGRectIsNull(secondNeighborFrame) && !CGRectIsNull(CGRectIntersection(newPopUpLabel.frame, secondNeighborFrame)))) { - return nil; + popUpLabel.center = CGPointMake(xCenter, popUpLabel.center.y); +} + +- (BOOL)adjustYLocForLabel:(UIView *)popUpLabel atIndex:(NSInteger)myIndex avoidingDot:(CGRect)dotFrame andLabels:(NSArray *)previousLabels { + //returns YES if it can avoid neighbors to left + //note: index < 0 for no checking neighbors + //check for bumping into top OR overlap with left neighbors + //default Y is above point + //check above and below dot + CGFloat halfLabelHeight = popUpLabel.frame.size.height/2.0f; + popUpLabel.center = CGPointMake(popUpLabel.center.x, CGRectGetMinY(dotFrame) - 12.0f - halfLabelHeight ); + CGFloat leftEdge = CGRectGetMinX(popUpLabel.frame); + + if (CGRectGetMinY(popUpLabel.frame) > 2.0f) { + BOOL noConflict = YES; + if (myIndex < 0) return YES; + for (UILabel * neighbor in previousLabels.reverseObjectEnumerator) { + if (!neighbor.superview) continue; + if (leftEdge > CGRectGetMaxX(neighbor.frame)) { + return YES; //no conflicts found at all + } + if (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, neighbor.frame))) { + noConflict = NO; + break; // conflict with neighbor + } } + if (noConflict) return YES; } - - if (self.animationGraphEntranceTime <= 0) { - newPopUpLabel.alpha = 1.0f; + //conflict (or too high), try below point instead + CGRect frame = popUpLabel.frame; + frame.origin.y = CGRectGetMaxY(dotFrame)+12.0f; + popUpLabel.frame = frame; + //check for bottom and again for overlap with neighbors + if (CGRectGetMaxY(frame) < CGRectGetMaxY(self.labelsView.bounds)) { + BOOL noConflict = YES; + if (myIndex < 0) return YES; + for (UILabel * neighbor in previousLabels.reverseObjectEnumerator ) { + if (!neighbor.superview) continue; + if (leftEdge > CGRectGetMaxX(neighbor.frame)) { + return YES; //no conflicts found at all + } + if (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, neighbor.frame))) { + noConflict = NO; + break; // conflict with neighbor + } + } + return noConflict; } else { - [UIView animateWithDuration:0.5f delay:self.animationGraphEntranceTime options:UIViewAnimationOptionCurveLinear animations:^{ - newPopUpLabel.alpha = 1.0f; - } completion:nil]; + return NO; } - - return newPopUpLabel; } + - (UIImage *)graphSnapshotImage { return [self graphSnapshotImageRenderedWhileInBackground:NO]; } +- (UIImage *)graphSnapshotImage:(CGSize)size { + CGRect testRect = CGRectMake(0, 0, size.height, size.width); + CGRect savedRect = self.frame; + CGFloat savedAnimationTime = self.animationGraphEntranceTime; + self.animationGraphEntranceTime = 0.0; + self.frame = testRect; + + UIImage * result = [self graphSnapshotImageRenderedWhileInBackground:NO]; + self.animationGraphEntranceTime = savedAnimationTime; + self.frame = savedRect; + return result; +} + - (UIImage *)graphSnapshotImageRenderedWhileInBackground:(BOOL)appIsInBackground { UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale); @@ -1207,16 +1311,30 @@ - (UIImage *)graphSnapshotImageRenderedWhileInBackground:(BOOL)appIsInBackground - (void)reloadGraph { [self drawGraph]; + // [self setNeedsLayout]; } #pragma mark - Values - (NSArray *)graphValuesForXAxis { - return xAxisValues; + return self.xAxisLabelTexts; } - (NSArray *)graphValuesForDataPoints { - return dataPoints; + NSMutableArray * points = [NSMutableArray arrayWithCapacity:self.dataPoints.count]; + if (self.adaptiveDataPoints) { + for (NSValue * point in self.dataPoints) { + CGPoint pointValue = point.CGPointValue; + if (pointValue.x < self.minXDisplayedValue) continue; + if (pointValue.x > self.maxXDisplayedValue) break; + [points addObject:@(pointValue.y)]; + } + } else { + for (NSValue * point in self.dataPoints) { + [points addObject:@(point.CGPointValue.y)]; + } + } + return [points copy]; } - (NSArray *)graphLabelsForXAxis { @@ -1236,14 +1354,40 @@ - (void)setAnimationGraphStyle:(BEMLineAnimation)animationGraphStyle { #pragma mark - Touch Gestures +- (CGFloat)xValueForLocation:(CGFloat)location { + + CGFloat xAxisWidth = CGRectGetMaxX(self.labelsView.bounds); + CGFloat valueRangeWidth = (self.maxXValue - self.minXValue) / self.zoomScale; + return self.minXDisplayedValue + valueRangeWidth * location/xAxisWidth; +} + +- (CGFloat)xLocationForValue:(NSNumber *) value { + CGFloat xAxisWidth = CGRectGetMaxX(self.labelsView.bounds); + CGFloat valueRangeWidth = (self.maxXValue - self.minXValue) / self.zoomScale; + return (value.doubleValue-self.minXDisplayedValue)/valueRangeWidth * xAxisWidth; +} + - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { - if ([gestureRecognizer isEqual:self.panGesture]) { - if (gestureRecognizer.numberOfTouches >= self.touchReportFingersRequired) { - CGPoint translation = [self.panGesture velocityInView:self.panView]; + CGFloat xLoc = [gestureRecognizer locationInView:self.labelsView].x; + if ([gestureRecognizer isEqual:self.touchReportPanGesture]) { + if ((NSInteger)gestureRecognizer.numberOfTouches >= self.touchReportFingersRequired) { + CGPoint translation = [self.touchReportPanGesture velocityInView:self.labelsView]; return fabs(translation.y) < fabs(translation.x); - } else return NO; + } else { + return NO; + } + } else if ([gestureRecognizer isEqual:self.zoomGesture]) { + ((UIPinchGestureRecognizer *)gestureRecognizer).scale = self.zoomScale; + self.doubleTapScale = 1.0; + self.zoomCenterLocation = xLoc; + self.zoomCenterValue = [self xValueForLocation:xLoc]; + return YES; + } else if ([gestureRecognizer isEqual:self.zoomPanGesture]) { + self.panMovementBase = xLoc; return YES; - } else return [super gestureRecognizerShouldBegin:gestureRecognizer]; + } else { + return [super gestureRecognizerShouldBegin:gestureRecognizer]; + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { @@ -1254,146 +1398,446 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive return YES; } -- (void)handleGestureAction:(UIGestureRecognizer *)recognizer { - CGPoint translation = [recognizer locationInView:self.viewForFirstBaselineLayout]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +- (void)setZoomScale:(CGFloat)zoomScale { + if (zoomScale !=_zoomScale ) { + if (self.labelsView) { + [self handleZoom:zoomScale orMovement:self.panMovement checkDelegate:NO]; + } else { + _zoomScale = zoomScale; + } + } +} + +- (void)setPanMovement:(CGFloat)panMovement { + if (panMovement != _panMovement ) { + if (self.labelsView) { + [self handleZoom:self.zoomScale orMovement:panMovement checkDelegate:NO]; + } else { + _panMovement = panMovement; + } + } +} +#pragma clang diagnostic pop + +#pragma mark Handle zoom gesture +- (void)handleZoomGestureAction:(UIPinchGestureRecognizer *)recognizer { + if (recognizer.numberOfTouches < 2) return; //avoid dragging when lifting fingers off + [self handleZoom: MAX(1.0f, recognizer.scale) orMovement:self.panMovement checkDelegate:YES]; - if (!((translation.x + self.frame.origin.x) <= self.frame.origin.x) && !((translation.x + self.frame.origin.x) >= self.frame.origin.x + self.frame.size.width)) { // To make sure the vertical line doesn't go beyond the frame of the graph. - self.touchInputLine.frame = CGRectMake(translation.x - self.widthTouchInputLine/2, 0, self.widthTouchInputLine, self.frame.size.height); + if (recognizer.state == UIGestureRecognizerStateEnded) { + self.zoomCenterLocation = 0 ; + self.zoomCenterValue = [self xValueForLocation:0]; } +} - self.touchInputLine.alpha = self.alphaTouchInputLine; +- (void)handlePanGestureAction:(UIPanGestureRecognizer *)recognizer { + if (recognizer.numberOfTouches < 2) return; //avoid dragging when lifting fingers off + CGFloat currentX = [recognizer locationInView:self.labelsView].x; - BEMCircle *closestDot = [self closestDotFromTouchInputLine:self.touchInputLine]; - closestDot.alpha = 0.8f; + CGFloat newPanMovement = self.panMovement + (currentX - self.panMovementBase); + if ([self handleZoom: self.zoomScale orMovement:newPanMovement checkDelegate:YES]) { + self.panMovementBase = currentX; + } +} + +- (BOOL)handleZoom:(CGFloat)newScale orMovement:(CGFloat)newPanMovement checkDelegate:(BOOL)checkDelegate { - if (self.enablePopUpReport == YES && closestDot.tag >= DotFirstTag100 && [closestDot isKindOfClass:[BEMCircle class]] && self.alwaysDisplayPopUpLabels == NO) { - [self labelForPoint:closestDot reuseNumber:NSIntegerMax]; + if (newScale <= 1.0) { + newScale = 1.0; + newPanMovement = 0; + } else if (newScale > self.maxZoom) { + //don't allow scaling beyond showing less than 2 points (on average) + newScale = self.maxZoom; + } + //assumes we're zooming around fixed point self.zoomCenterValue which is currently displayed at self.zoomCenterLocation + //Now with newScale and newPanMovement, + //1) adjust self.zoomCenterLocation by change in panMovement + //2) calculate lowest value that will now be displayed (newMinXDisplayed) + //3) that lets us check if min or max X values will be located in middle of screen, adjust if necessary. + //4) finally ask permission for new panZoom and update geometry globals + CGFloat xAxisWidth = CGRectGetMaxX(self.labelsView.bounds); + CGFloat totalValueRangeWidth = self.maxXValue - self.minXValue; + if (xAxisWidth <= 0 || totalValueRangeWidth <= 0) return NO; + + CGFloat newValueRangeWidth = (totalValueRangeWidth) / newScale; + CGFloat displayRatio = xAxisWidth/newValueRangeWidth; + + CGFloat newZoomCenterLocation = self.zoomCenterLocation +(newPanMovement - self.panMovement); + CGFloat newMinXDisplayed = self.zoomCenterValue - newZoomCenterLocation/displayRatio; + + CGFloat deltaPan = NAN; + CGFloat newMaxXLocation = (self.maxXValue - newMinXDisplayed) * displayRatio; + CGFloat newMinXLocation = (self.minXValue - newMinXDisplayed) * displayRatio; + if (newMaxXLocation < xAxisWidth ) { + //clamping High + deltaPan = xAxisWidth - newMaxXLocation; + } else if (newMinXLocation > 0) { + //clamping low + deltaPan = -newMinXLocation; + } + if (!isnan(deltaPan) ) { + //now recalculate new geometry with clamped panMovement + newPanMovement += deltaPan; + newZoomCenterLocation += deltaPan; + newMinXDisplayed -= deltaPan/displayRatio; } - if (closestDot.tag >= DotFirstTag100 && [closestDot isMemberOfClass:[BEMCircle class]]) { - if ([self.delegate respondsToSelector:@selector(lineGraph:didTouchGraphWithClosestIndex:)] && self.enableTouchReport == YES) { - [self.delegate lineGraph:self didTouchGraphWithClosestIndex:((NSUInteger)closestDot.tag - DotFirstTag100)]; + if (fabs(self.zoomScale - newScale ) > 0.01 || + fabs(self.panMovement - newPanMovement) > 0.5 ) { + if (!checkDelegate || + ![self.delegate respondsToSelector:@selector(lineGraph:shouldScaleFrom:to:showingFromXMinValue:toXMaxValue:)] || + [self.delegate lineGraph: self + shouldScaleFrom: self.zoomScale + to: newScale + showingFromXMinValue: newMinXDisplayed + toXMaxValue: newMinXDisplayed + newValueRangeWidth]) { + + _zoomScale = newScale; + _panMovement = newPanMovement; + self.zoomCenterLocation = newZoomCenterLocation; + self.minXDisplayedValue = newMinXDisplayed; + _maxXDisplayedValue = newMinXDisplayed + newValueRangeWidth; + CGFloat saveAnimation = self.animationGraphEntranceTime; + self.animationGraphEntranceTime = 0; + [self reloadGraph]; + self.animationGraphEntranceTime = saveAnimation; + return YES; + } else { + return NO; + } + } else { + return NO; + } +} + +- (void)handleDoubleTapGestureAction:(UITapGestureRecognizer *)recognizer { + + if (self.zoomScale < 1.01) { + self.zoomCenterLocation = [recognizer locationInView:self].x; + self.zoomCenterValue = [self xValueForLocation:self.zoomCenterLocation]; + if ([self handleZoom:self.doubleTapScale orMovement:0 checkDelegate:YES]) { + self.doubleTapScale = 1.0; + } + } else { + CGFloat oldZoom = self.zoomScale; + if ([self handleZoom:1.0 orMovement:0 checkDelegate:YES]) { + self.doubleTapScale = oldZoom; } } + [self reloadGraph]; +} - // ON RELEASE - if (recognizer.state == UIGestureRecognizerStateEnded) { - if ([self.delegate respondsToSelector:@selector(lineGraph:didReleaseTouchFromGraphWithClosestIndex:)]) { - [self.delegate lineGraph:self didReleaseTouchFromGraphWithClosestIndex:(closestDot.tag - DotFirstTag100)]; +- (void)handleGestureAction:(UIGestureRecognizer *)recognizer { + CGFloat translation = [recognizer locationInView:self.labelsView].x; + CGFloat leftEdge = CGRectGetMinX(self.labelsView.bounds); + CGFloat rightEdge = CGRectGetMaxX(self.labelsView.bounds); + if (translation >= leftEdge && translation <= rightEdge) { // To make sure the vertical line doesn't go beyond the frame of the graph. + self.touchInputLine.frame = CGRectMake(translation - self.widthTouchInputLine/2, 0, self.widthTouchInputLine, CGRectGetMaxY(self.labelsView.bounds)); + } + + self.touchInputLine.alpha = self.alphaTouchInputLine; + + BEMCircle *closestDot = [self closestDotFromTouchInputLine:self.touchInputLine]; + NSInteger index = closestDot.tag - DotFirstTag100; + closestDot.alpha = 0.8f; + + if (recognizer.state != UIGestureRecognizerStateEnded) { + //ON START OR MOVE + if (index < 0) return; //something's very wrong + if (self.enablePopUpReport == YES && self.alwaysDisplayPopUpLabels == NO) { + if ([self.delegate respondsToSelector:@selector(popUpViewForLineGraph:)] ) { + UIView * newCustom = [self.delegate popUpViewForLineGraph:self]; + if (newCustom != self.customPopUpView) { + [self.customPopUpView removeFromSuperview]; + self.customPopUpView = newCustom; + } + } + if (self.customPopUpView) { + [self.labelsView addSubview:self.customPopUpView]; + [self adjustXLocForLabel:self.customPopUpView avoidingDot:closestDot.frame]; + [self adjustYLocForLabel:self.customPopUpView atIndex: -1 avoidingDot:closestDot.frame andLabels:self.permanentPopups ]; + if ([self.delegate respondsToSelector:@selector(lineGraph:modifyPopupView:forIndex:)]) { + self.customPopUpView.alpha = 1.0f; + [self.delegate lineGraph:self modifyPopupView:self.customPopUpView forIndex:index]; + } else { + [UIView animateWithDuration:0.2f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.customPopUpView.alpha = 1.0f; + } completion:nil]; + } + } else { + if (!self.popUpLabel) { + self.popUpLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + } + [self.labelsView addSubview: self.popUpLabel ]; + self.popUpLabel = [self configureLabel:self.popUpLabel forPoint:closestDot avoiding:self.permanentPopups]; + [self adjustXLocForLabel:self.popUpLabel avoidingDot:closestDot.frame]; + [self adjustYLocForLabel:self.popUpLabel atIndex: -1 avoidingDot:closestDot.frame andLabels:self.permanentPopups]; + [UIView animateWithDuration:0.2f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.popUpLabel.alpha = 1.0f; + } completion:nil]; + } + } + if (self.enableTouchReport && [self.delegate respondsToSelector:@selector(lineGraph:didTouchGraphWithClosestIndex:)]) { + [self.delegate lineGraph:self didTouchGraphWithClosestIndex:index]; + } + } else { + // ON RELEASE + if (index >= 0 && self.enableTouchReport && [self.delegate respondsToSelector:@selector(lineGraph:didReleaseTouchFromGraphWithClosestIndex:)]) { + [self.delegate lineGraph:self didReleaseTouchFromGraphWithClosestIndex:index]; } [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - if (self.alwaysDisplayDots == NO && self.displayDotsOnly == NO) { + if (self.alwaysDisplayDots == NO && self.displayDotsOnly == NO && self.numberOfPoints > 1) { closestDot.alpha = 0; } - self.touchInputLine.alpha = 0; - if (self.enablePopUpReport == YES) { - self.popUpLabel.alpha = 0; - if (self.popUpView) self.popUpView.alpha = 0; - } - } completion:nil]; + self.popUpLabel.alpha = 0; + self.customPopUpView.alpha = 0; + } completion:^(BOOL finished) { + [self.customPopUpView removeFromSuperview]; + self.customPopUpView = nil; + }]; } } #pragma mark - Graph Calculations - (BEMCircle *)closestDotFromTouchInputLine:(UIView *)touchInputLine { - BEMCircle *closestDot = nil; + BEMCircle * closestDot = nil; CGFloat currentlyCloser = CGFLOAT_MAX; for (BEMCircle *point in self.circleDots) { - if (point.tag >= DotFirstTag100) { - if (self.alwaysDisplayDots == NO && self.displayDotsOnly == NO) { - point.alpha = 0; - } - CGFloat distance = (CGFloat)fabs(point.center.x - touchInputLine.center.x) ; - if (distance < currentlyCloser) { - currentlyCloser = distance; - closestDot = point; - } + if (!point.superview) continue; + if (self.alwaysDisplayDots == NO && self.displayDotsOnly == NO && self.numberOfPoints > 1) { + point.alpha = 0; + } + CGFloat distance = (CGFloat)fabs(point.center.x - touchInputLine.center.x) ; + if (distance < currentlyCloser) { + currentlyCloser = distance; + closestDot = point; } } return closestDot; } -- (CGFloat)getMaximumValue { +- (NSArray *)getData { + // Remove all data points before adding them to the array + NSMutableArray * newDataPoints = [NSMutableArray arrayWithCapacity:(NSUInteger)self.numberOfPoints]; + CGFloat lastXValue = -INFINITY; + for (NSInteger index = 0; index < self.numberOfPoints; index++) { + CGFloat dotValue = 0; + +#ifndef TARGET_INTERFACE_BUILDER + if ([self.dataSource respondsToSelector:@selector(lineGraph:valueForPointAtIndex:)]) { + dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:index]; + + } else { + [NSException raise:@"lineGraph:valueForPointAtIndex: protocol method is not implemented in the data source. Throwing exception here before the system throws a CALayerInvalidGeometry Exception." format:@"Value for point %f at index %lu is invalid. CALayer position may contain NaN: [0 nan]", dotValue, (unsigned long)index]; + } +#else + dotValue = (int)(arc4random() % 10000); +#endif + CGFloat xValue = index; + if ([self.dataSource respondsToSelector:@selector(lineGraph:locationForPointAtIndex:)]){ + xValue = [self.dataSource lineGraph:self locationForPointAtIndex:index]; + if (xValue <= lastXValue) { + NSLog(@"Warning: X Values must increase across graph; at index %ld: %0.2f < %0.2f", (long)index, xValue, lastXValue); + } + lastXValue = xValue; + } + [newDataPoints addObject:[NSValue valueWithCGPoint:CGPointMake(xValue, dotValue)]]; + } + return [newDataPoints copy]; +} + +- (void)calculateMinMax { +#ifndef TARGET_INTERFACE_BUILDER + BOOL usingIndex = ![self.dataSource respondsToSelector:@selector(lineGraph:locationForPointAtIndex:)]; + + self.maxYValue = [self getMaximumYValue]; if (self.maxYValue <= -FLT_MAX) self.maxYValue = 0; + self.minYValue = [self getMinimumYValue]; if (self.minYValue >= INFINITY) self.minYValue = 0; + if (usingIndex) { + self.minXValue = 0; + self.maxXValue = self.numberOfPoints-1; + self.maxZoom = self.numberOfPoints/2; + //don't allow scaling beyond showing less than 2 points + } else { + self.maxXValue = [self getMaximumXValue]; if (self.maxXValue <= -FLT_MAX) self.maxXValue = 0; + self.minXValue = [self getMinimumXValue]; if (self.minXValue >= INFINITY) self.minXValue = 0; + if (self.maxYValue < self.minYValue) self.maxYValue = self.minYValue; + if (self.maxXValue < self.minXValue) self.maxXValue = self.minXValue; + CGFloat minInterval = [self getMinimumInterval]; + CGFloat valueRange = self.maxXValue - self.minXValue; + if (valueRange > 0 && minInterval > 0) { + self.maxZoom = valueRange/(minInterval*10); //allow smallest gap between points to be zoom + } else { + self.maxZoom = self.numberOfPoints/2; + } + } + if ((self.zoomScale <= 1.0) || + isnan(self.minXDisplayedValue) || + (self.minXDisplayedValue > self.maxXDisplayedValue) || + (self.minXDisplayedValue < self.minXValue || self.maxXDisplayedValue > self.maxXValue) || + (usingIndex && (self.maxXDisplayedValue > self.numberOfPoints ))) { + _zoomScale = 1.0; + _minXDisplayedValue = self.minXValue; + _maxXDisplayedValue = self.maxXValue; + _panMovement = 0; + } +#else + self.minYValue = 0.0f; + self.maxYValue = 10000.0f; + self.minXValue = 0; + self.maxXValue = self.numberOfPoints-1; + _zoomScale = 1.0; + _minXDisplayedValue = self.minXValue; + _maxXDisplayedValue = self.maxXValue; + _panMovement = 0; +#endif +} + +- (NSArray *)layoutPoints { + //now calculate point locations in view + NSMutableArray * newLocs = [NSMutableArray arrayWithCapacity:(NSUInteger)self.numberOfPoints]; + + CGFloat xAxisWidth = CGRectGetMaxX(self.dotsView.bounds); + CGFloat totalValueRangeWidth = self.maxXValue - self.minXValue; + CGFloat valueRangeWidth = totalValueRangeWidth / self.zoomScale; + if (totalValueRangeWidth <= 0) { //all x values same, e.g. only one point + valueRangeWidth = xAxisWidth; + } + CGFloat displayRatio = xAxisWidth/valueRangeWidth; + + for (NSValue * value in self.dataPoints) { + CGFloat positionOnXAxis = (value.CGPointValue.x - self.minXDisplayedValue) * displayRatio ; + if (totalValueRangeWidth <= 0) positionOnXAxis = xAxisWidth/2; + CGFloat positionOnYAxis = [self yPositionForDotValue:value.CGPointValue.y]; + [newLocs addObject:[NSValue valueWithCGPoint: CGPointMake(positionOnXAxis, positionOnYAxis)]]; + } + return [newLocs copy]; +} + +- (CGFloat)getMaximumYValue { if ([self.delegate respondsToSelector:@selector(maxValueForLineGraph:)]) { return [self.delegate maxValueForLineGraph:self]; } else { - CGFloat dotValue; CGFloat maxValue = -FLT_MAX; - - @autoreleasepool { - for (NSUInteger i = 0; i < numberOfPoints; i++) { - if ([self.dataSource respondsToSelector:@selector(lineGraph:valueForPointAtIndex:)]) { - dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:i]; - } else dotValue = 0; - - if (dotValue >= BEMNullGraphValue) continue; - if (dotValue > maxValue) maxValue = dotValue; - } + for (NSValue * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.y; + if (dotValue >= BEMNullGraphValue) continue; + if (dotValue > maxValue) maxValue = dotValue; } return maxValue; } } -- (CGFloat)getMinimumValue { +- (CGFloat)getMinimumYValue { if ([self.delegate respondsToSelector:@selector(minValueForLineGraph:)]) { return [self.delegate minValueForLineGraph:self]; } else { - CGFloat dotValue; CGFloat minValue = INFINITY; + for (NSNumber * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.y; + if (dotValue >= BEMNullGraphValue) continue; + if (dotValue < minValue) minValue = dotValue; + } + return minValue; + } +} - @autoreleasepool { - for (NSUInteger i = 0; i < numberOfPoints; i++) { - if ([self.dataSource respondsToSelector:@selector(lineGraph:valueForPointAtIndex:)]) { - dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:i]; - - } else dotValue = 0; +- (CGFloat)getMaximumXValue { + if ([self.delegate respondsToSelector:@selector(maxXValueForLineGraph:)]) { + return [self.delegate maxXValueForLineGraph:self]; + } else { + CGFloat maxValue = -FLT_MAX; + for (NSValue * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.x; + if (dotValue >= BEMNullGraphValue) continue; + if (dotValue > maxValue) maxValue = dotValue; + } + return maxValue; + } +} - if (dotValue >= BEMNullGraphValue) continue; - if (dotValue < minValue) minValue = dotValue; - } +- (CGFloat)getMinimumXValue { + if ([self.delegate respondsToSelector:@selector(minXValueForLineGraph:)]) { + return [self.delegate minXValueForLineGraph:self]; + } else { + CGFloat minValue = INFINITY; + for (NSValue * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.x; + if (dotValue >= BEMNullGraphValue) continue; + if (dotValue < minValue) minValue = dotValue; } return minValue; } } -- (CGFloat)yPositionForDotValue:(CGFloat)dotValue { - if (dotValue >= BEMNullGraphValue) { - return BEMNullGraphValue; +-(CGFloat) getMinimumInterval { + CGFloat minInterval = INFINITY; + CGFloat lastValue = -INFINITY; + for (NSValue * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.x; + if (dotValue >= BEMNullGraphValue) continue; + if (dotValue - lastValue < minInterval) minInterval = dotValue-lastValue; + lastValue = dotValue; + } + return minInterval; +} + +- (CGFloat)getAverageValue { + if ([self.delegate respondsToSelector:@selector(averageValueForLineGraph:)]) { + return [self.delegate averageValueForLineGraph:self]; + } else { + CGFloat sumValue = 0.0f; + int numPoints = 0; + for (NSValue * value in self.dataPoints) { + CGFloat dotValue = value.CGPointValue.y; + if (dotValue >= BEMNullGraphValue) continue; + sumValue += dotValue; + numPoints++; + } + if (numPoints > 0) { + return sumValue/numPoints; + } else { + return NAN; + } } +} +- (CGFloat)yPositionForDotValue:(CGFloat)dotValue { + CGFloat height = self.dotsView.bounds.size.height; CGFloat positionOnYAxis; // The position on the Y-axis of the point currently being created. - CGFloat padding = MIN(90.0f,self.frame.size.height/2); + CGFloat padding = MIN(90.0f, height/2); if ([self.delegate respondsToSelector:@selector(staticPaddingForLineGraph:)]) { padding = [self.delegate staticPaddingForLineGraph:self]; } - self.XAxisLabelYOffset = self.enableXAxisLabel ? self.backgroundXAxis.frame.size.height : 0.0f; - if (self.autoScaleYAxis) { - if (self.minValue >= self.maxValue ) { - positionOnYAxis = self.frame.size.height/2.0f; + if (self.minYValue >= self.maxYValue ) { + positionOnYAxis = height/2; } else { - CGFloat percentValue = (dotValue - self.minValue) / (self.maxValue - self.minValue); - CGFloat topOfChart = self.frame.size.height - padding/2.0f; - CGFloat sizeOfChart = self.frame.size.height - padding; - positionOnYAxis = topOfChart - percentValue * sizeOfChart + self.XAxisLabelYOffset/2; + if (isnan(dotValue) || dotValue >= BEMNullGraphValue) { + return BEMNullGraphValue; + } + CGFloat percentValue = (dotValue - self.minYValue) / (self.maxYValue - self.minYValue); + CGFloat bottomOfChart = CGRectGetMaxY(self.dotsView.bounds)- padding/2.0f; + CGFloat sizeOfChart = CGRectGetMaxY(self.dotsView.bounds) - padding; + positionOnYAxis = bottomOfChart - percentValue * sizeOfChart; } } else { - positionOnYAxis = ((self.frame.size.height) - dotValue); + positionOnYAxis = (height - dotValue); } - positionOnYAxis -= self.XAxisLabelYOffset; - return positionOnYAxis; } #pragma mark - Deprecated Methods + - (NSNumber *)calculatePointValueSum { [self printDeprecationTransitionWarningForOldMethod:@"calculatePointValueSum" replacementMethod:@"calculatePointValueSumOnGraph:" newObject:@"BEMGraphCalculator" sharedInstance:YES]; return [[BEMGraphCalculator sharedCalculator] calculatePointValueSumOnGraph:self]; diff --git a/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj b/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj index 7ff92b5..5353dad 100644 --- a/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj +++ b/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj @@ -7,6 +7,33 @@ objects = { /* Begin PBXBuildFile section */ + 1E03A03A1EC8DE8900CA4247 /* UIButton+Switch.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E03A0371EC8DE8900CA4247 /* UIButton+Switch.m */; }; + 1E03A03B1EC8DE8900CA4247 /* UITextField+Numbers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E03A0391EC8DE8900CA4247 /* UITextField+Numbers.m */; }; + 1E960A851E7DF942000E2BB8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A841E7DF942000E2BB8 /* main.m */; }; + 1E960A881E7DF942000E2BB8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A871E7DF942000E2BB8 /* AppDelegate.m */; }; + 1E960A8E1E7DF942000E2BB8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E960A8C1E7DF942000E2BB8 /* Main.storyboard */; }; + 1E960A901E7DF942000E2BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1E960A8F1E7DF942000E2BB8 /* Assets.xcassets */; }; + 1E960A931E7DF942000E2BB8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1E960A911E7DF942000E2BB8 /* LaunchScreen.storyboard */; }; + 1E960AA01E7DF9BC000E2BB8 /* ARFontPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A991E7DF9BB000E2BB8 /* ARFontPickerViewController.m */; }; + 1E960AA11E7DF9BC000E2BB8 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A9B1E7DF9BB000E2BB8 /* DetailViewController.m */; }; + 1E960AA21E7DF9BC000E2BB8 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A9D1E7DF9BB000E2BB8 /* MasterViewController.m */; }; + 1E960AA31E7DF9BC000E2BB8 /* StatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960A9F1E7DF9BC000E2BB8 /* StatsViewController.m */; }; + 1E960AA41E7E094D000E2BB8 /* BEMSimpleLineGraphView.m in Sources */ = {isa = PBXBuildFile; fileRef = C3B90A5D187D15F7003E407D /* BEMSimpleLineGraphView.m */; }; + 1E960AA51E7E097C000E2BB8 /* BEMCircle.m in Sources */ = {isa = PBXBuildFile; fileRef = C3B90A59187D15F7003E407D /* BEMCircle.m */; }; + 1E960AA61E7E097C000E2BB8 /* BEMLine.m in Sources */ = {isa = PBXBuildFile; fileRef = C3B90A5B187D15F7003E407D /* BEMLine.m */; }; + 1E960AA71E7E097C000E2BB8 /* BEMAverageLine.m in Sources */ = {isa = PBXBuildFile; fileRef = A63990B41AD4923900B14D88 /* BEMAverageLine.m */; }; + 1E960AA81E7E0990000E2BB8 /* BEMGraphCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = A6AC895A1C5882DD0052AB1C /* BEMGraphCalculator.m */; }; + 1E960B121E7F9C80000E2BB8 /* MSColorComponentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960AFD1E7F9C80000E2BB8 /* MSColorComponentView.m */; }; + 1E960B131E7F9C80000E2BB8 /* MSColorSelectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B001E7F9C80000E2BB8 /* MSColorSelectionView.m */; }; + 1E960B141E7F9C80000E2BB8 /* MSColorSelectionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B021E7F9C80000E2BB8 /* MSColorSelectionViewController.m */; }; + 1E960B151E7F9C80000E2BB8 /* MSColorUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B041E7F9C80000E2BB8 /* MSColorUtils.m */; }; + 1E960B161E7F9C80000E2BB8 /* MSColorWheelView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B071E7F9C80000E2BB8 /* MSColorWheelView.m */; }; + 1E960B171E7F9C80000E2BB8 /* MSHSBView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B091E7F9C80000E2BB8 /* MSHSBView.m */; }; + 1E960B181E7F9C80000E2BB8 /* MSRGBView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B0B1E7F9C80000E2BB8 /* MSRGBView.m */; }; + 1E960B191E7F9C80000E2BB8 /* MSSliderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B0D1E7F9C80000E2BB8 /* MSSliderView.m */; }; + 1E960B1A1E7F9C80000E2BB8 /* MSThumbView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B0F1E7F9C80000E2BB8 /* MSThumbView.m */; }; + 1E960B1B1E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E960B111E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.m */; }; + 1EA1648A1E9433FD00D77C4A /* NSUserDefaults+Color.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EA164891E9433FD00D77C4A /* NSUserDefaults+Color.m */; }; 99B15643187B412400B24591 /* StatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 99B15642187B412400B24591 /* StatsViewController.m */; }; 99B3FA3A1877898B00539A7B /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 99B3FA381877898B00539A7B /* LICENSE */; }; 99B3FA3B1877898B00539A7B /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 99B3FA391877898B00539A7B /* README.md */; }; @@ -44,21 +71,65 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1E03A0361EC8DE8900CA4247 /* UIButton+Switch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIButton+Switch.h"; sourceTree = ""; }; + 1E03A0371EC8DE8900CA4247 /* UIButton+Switch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIButton+Switch.m"; sourceTree = ""; }; + 1E03A0381EC8DE8900CA4247 /* UITextField+Numbers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextField+Numbers.h"; sourceTree = ""; }; + 1E03A0391EC8DE8900CA4247 /* UITextField+Numbers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITextField+Numbers.m"; sourceTree = ""; }; + 1E960A811E7DF942000E2BB8 /* TestBed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestBed.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E960A841E7DF942000E2BB8 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 1E960A861E7DF942000E2BB8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 1E960A871E7DF942000E2BB8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 1E960A8D1E7DF942000E2BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1E960A8F1E7DF942000E2BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1E960A921E7DF942000E2BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1E960A941E7DF942000E2BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1E960A981E7DF9BB000E2BB8 /* ARFontPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARFontPickerViewController.h; sourceTree = ""; }; + 1E960A991E7DF9BB000E2BB8 /* ARFontPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARFontPickerViewController.m; sourceTree = ""; }; + 1E960A9A1E7DF9BB000E2BB8 /* DetailViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; + 1E960A9B1E7DF9BB000E2BB8 /* DetailViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = DetailViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 1E960A9C1E7DF9BB000E2BB8 /* MasterViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; }; + 1E960A9D1E7DF9BB000E2BB8 /* MasterViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MasterViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 1E960A9E1E7DF9BB000E2BB8 /* StatsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatsViewController.h; sourceTree = ""; }; + 1E960A9F1E7DF9BC000E2BB8 /* StatsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = StatsViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 1E960AFC1E7F9C80000E2BB8 /* MSColorComponentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorComponentView.h; sourceTree = ""; }; + 1E960AFD1E7F9C80000E2BB8 /* MSColorComponentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSColorComponentView.m; sourceTree = ""; }; + 1E960AFE1E7F9C80000E2BB8 /* MSColorPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorPicker.h; sourceTree = ""; }; + 1E960AFF1E7F9C80000E2BB8 /* MSColorSelectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorSelectionView.h; sourceTree = ""; }; + 1E960B001E7F9C80000E2BB8 /* MSColorSelectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSColorSelectionView.m; sourceTree = ""; }; + 1E960B011E7F9C80000E2BB8 /* MSColorSelectionViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorSelectionViewController.h; sourceTree = ""; }; + 1E960B021E7F9C80000E2BB8 /* MSColorSelectionViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSColorSelectionViewController.m; sourceTree = ""; }; + 1E960B031E7F9C80000E2BB8 /* MSColorUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorUtils.h; sourceTree = ""; }; + 1E960B041E7F9C80000E2BB8 /* MSColorUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSColorUtils.m; sourceTree = ""; }; + 1E960B051E7F9C80000E2BB8 /* MSColorView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorView.h; sourceTree = ""; }; + 1E960B061E7F9C80000E2BB8 /* MSColorWheelView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSColorWheelView.h; sourceTree = ""; }; + 1E960B071E7F9C80000E2BB8 /* MSColorWheelView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSColorWheelView.m; sourceTree = ""; }; + 1E960B081E7F9C80000E2BB8 /* MSHSBView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSHSBView.h; sourceTree = ""; }; + 1E960B091E7F9C80000E2BB8 /* MSHSBView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSHSBView.m; sourceTree = ""; }; + 1E960B0A1E7F9C80000E2BB8 /* MSRGBView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSRGBView.h; sourceTree = ""; }; + 1E960B0B1E7F9C80000E2BB8 /* MSRGBView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSRGBView.m; sourceTree = ""; }; + 1E960B0C1E7F9C80000E2BB8 /* MSSliderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSSliderView.h; sourceTree = ""; }; + 1E960B0D1E7F9C80000E2BB8 /* MSSliderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSSliderView.m; sourceTree = ""; }; + 1E960B0E1E7F9C80000E2BB8 /* MSThumbView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSThumbView.h; sourceTree = ""; }; + 1E960B0F1E7F9C80000E2BB8 /* MSThumbView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSThumbView.m; sourceTree = ""; }; + 1E960B101E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIControl+HitTestEdgeInsets.h"; sourceTree = ""; }; + 1E960B111E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIControl+HitTestEdgeInsets.m"; sourceTree = ""; }; + 1EA164881E9433FD00D77C4A /* NSUserDefaults+Color.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = "NSUserDefaults+Color.h"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 1EA164891E9433FD00D77C4A /* NSUserDefaults+Color.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = "NSUserDefaults+Color.m"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 99B15641187B412400B24591 /* StatsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StatsViewController.h; sourceTree = ""; }; 99B15642187B412400B24591 /* StatsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StatsViewController.m; sourceTree = ""; }; 99B3FA381877898B00539A7B /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 99B3FA391877898B00539A7B /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = README.md; path = ../README.md; sourceTree = ""; }; A63990B31AD4923900B14D88 /* BEMAverageLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BEMAverageLine.h; sourceTree = ""; }; - A63990B41AD4923900B14D88 /* BEMAverageLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BEMAverageLine.m; sourceTree = ""; }; + A63990B41AD4923900B14D88 /* BEMAverageLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = BEMAverageLine.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; A64594511BAB257B00D6B8FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "Base.lproj/Launch Screen.storyboard"; sourceTree = ""; }; A6AC89591C5882DD0052AB1C /* BEMGraphCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BEMGraphCalculator.h; sourceTree = ""; }; A6AC895A1C5882DD0052AB1C /* BEMGraphCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BEMGraphCalculator.m; sourceTree = ""; }; C3B90A58187D15F7003E407D /* BEMCircle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BEMCircle.h; sourceTree = ""; }; C3B90A59187D15F7003E407D /* BEMCircle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BEMCircle.m; sourceTree = ""; }; C3B90A5A187D15F7003E407D /* BEMLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BEMLine.h; sourceTree = ""; }; - C3B90A5B187D15F7003E407D /* BEMLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BEMLine.m; sourceTree = ""; }; - C3B90A5C187D15F7003E407D /* BEMSimpleLineGraphView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BEMSimpleLineGraphView.h; sourceTree = ""; }; - C3B90A5D187D15F7003E407D /* BEMSimpleLineGraphView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BEMSimpleLineGraphView.m; sourceTree = ""; }; + C3B90A5B187D15F7003E407D /* BEMLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = BEMLine.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + C3B90A5C187D15F7003E407D /* BEMSimpleLineGraphView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = BEMSimpleLineGraphView.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + C3B90A5D187D15F7003E407D /* BEMSimpleLineGraphView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = BEMSimpleLineGraphView.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; C3BCA7E61B8ECCA6007E6090 /* CustomizationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomizationTests.m; sourceTree = ""; }; C3BCA7E81B8ECE4E007E6090 /* contantsTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = contantsTests.h; sourceTree = ""; }; C3FD8155186DFD9A00FD8ED3 /* SimpleLineChart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimpleLineChart.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -83,6 +154,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 1E960A7E1E7DF942000E2BB8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3FD8152186DFD9A00FD8ED3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -106,6 +184,72 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1E960A821E7DF942000E2BB8 /* TestBed */ = { + isa = PBXGroup; + children = ( + 1E960A861E7DF942000E2BB8 /* AppDelegate.h */, + 1E960A871E7DF942000E2BB8 /* AppDelegate.m */, + 1E960A8C1E7DF942000E2BB8 /* Main.storyboard */, + 1E960A981E7DF9BB000E2BB8 /* ARFontPickerViewController.h */, + 1E960A991E7DF9BB000E2BB8 /* ARFontPickerViewController.m */, + 1E960A9A1E7DF9BB000E2BB8 /* DetailViewController.h */, + 1E960A9B1E7DF9BB000E2BB8 /* DetailViewController.m */, + 1E960A9C1E7DF9BB000E2BB8 /* MasterViewController.h */, + 1E960A9D1E7DF9BB000E2BB8 /* MasterViewController.m */, + 1EA164881E9433FD00D77C4A /* NSUserDefaults+Color.h */, + 1EA164891E9433FD00D77C4A /* NSUserDefaults+Color.m */, + 1E960A9E1E7DF9BB000E2BB8 /* StatsViewController.h */, + 1E960A9F1E7DF9BC000E2BB8 /* StatsViewController.m */, + 1E03A0361EC8DE8900CA4247 /* UIButton+Switch.h */, + 1E03A0371EC8DE8900CA4247 /* UIButton+Switch.m */, + 1E03A0381EC8DE8900CA4247 /* UITextField+Numbers.h */, + 1E03A0391EC8DE8900CA4247 /* UITextField+Numbers.m */, + 1E960AFB1E7F9C80000E2BB8 /* MSColorPicker */, + 1E960A8F1E7DF942000E2BB8 /* Assets.xcassets */, + 1E960A831E7DF942000E2BB8 /* Supporting Files */, + ); + path = TestBed; + sourceTree = ""; + }; + 1E960A831E7DF942000E2BB8 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1E960A941E7DF942000E2BB8 /* Info.plist */, + 1E960A911E7DF942000E2BB8 /* LaunchScreen.storyboard */, + 1E960A841E7DF942000E2BB8 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1E960AFB1E7F9C80000E2BB8 /* MSColorPicker */ = { + isa = PBXGroup; + children = ( + 1E960AFC1E7F9C80000E2BB8 /* MSColorComponentView.h */, + 1E960AFD1E7F9C80000E2BB8 /* MSColorComponentView.m */, + 1E960AFE1E7F9C80000E2BB8 /* MSColorPicker.h */, + 1E960AFF1E7F9C80000E2BB8 /* MSColorSelectionView.h */, + 1E960B001E7F9C80000E2BB8 /* MSColorSelectionView.m */, + 1E960B011E7F9C80000E2BB8 /* MSColorSelectionViewController.h */, + 1E960B021E7F9C80000E2BB8 /* MSColorSelectionViewController.m */, + 1E960B031E7F9C80000E2BB8 /* MSColorUtils.h */, + 1E960B041E7F9C80000E2BB8 /* MSColorUtils.m */, + 1E960B051E7F9C80000E2BB8 /* MSColorView.h */, + 1E960B061E7F9C80000E2BB8 /* MSColorWheelView.h */, + 1E960B071E7F9C80000E2BB8 /* MSColorWheelView.m */, + 1E960B081E7F9C80000E2BB8 /* MSHSBView.h */, + 1E960B091E7F9C80000E2BB8 /* MSHSBView.m */, + 1E960B0A1E7F9C80000E2BB8 /* MSRGBView.h */, + 1E960B0B1E7F9C80000E2BB8 /* MSRGBView.m */, + 1E960B0C1E7F9C80000E2BB8 /* MSSliderView.h */, + 1E960B0D1E7F9C80000E2BB8 /* MSSliderView.m */, + 1E960B0E1E7F9C80000E2BB8 /* MSThumbView.h */, + 1E960B0F1E7F9C80000E2BB8 /* MSThumbView.m */, + 1E960B101E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.h */, + 1E960B111E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.m */, + ); + path = MSColorPicker; + sourceTree = ""; + }; C3B90A55187D15F7003E407D /* Classes */ = { isa = PBXGroup; children = ( @@ -115,8 +259,8 @@ C3B90A59187D15F7003E407D /* BEMCircle.m */, C3B90A5A187D15F7003E407D /* BEMLine.h */, C3B90A5B187D15F7003E407D /* BEMLine.m */, - A63990B31AD4923900B14D88 /* BEMAverageLine.h */, A63990B41AD4923900B14D88 /* BEMAverageLine.m */, + A63990B31AD4923900B14D88 /* BEMAverageLine.h */, A6AC89591C5882DD0052AB1C /* BEMGraphCalculator.h */, A6AC895A1C5882DD0052AB1C /* BEMGraphCalculator.m */, ); @@ -132,6 +276,7 @@ C3B90A55187D15F7003E407D /* Classes */, C3FD815E186DFD9A00FD8ED3 /* SimpleLineChart */, C3FD817D186DFD9A00FD8ED3 /* SimpleLineChartTests */, + 1E960A821E7DF942000E2BB8 /* TestBed */, C3FD8157186DFD9A00FD8ED3 /* Frameworks */, C3FD8156186DFD9A00FD8ED3 /* Products */, ); @@ -142,6 +287,7 @@ children = ( C3FD8155186DFD9A00FD8ED3 /* SimpleLineChart.app */, C3FD8176186DFD9A00FD8ED3 /* SimpleLineChartTests.xctest */, + 1E960A811E7DF942000E2BB8 /* TestBed.app */, ); name = Products; sourceTree = ""; @@ -208,6 +354,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 1E960A801E7DF942000E2BB8 /* TestBed */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1E960A971E7DF942000E2BB8 /* Build configuration list for PBXNativeTarget "TestBed" */; + buildPhases = ( + 1E960A7D1E7DF942000E2BB8 /* Sources */, + 1E960A7E1E7DF942000E2BB8 /* Frameworks */, + 1E960A7F1E7DF942000E2BB8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TestBed; + productName = TestBed; + productReference = 1E960A811E7DF942000E2BB8 /* TestBed.app */; + productType = "com.apple.product-type.application"; + }; C3FD8154186DFD9A00FD8ED3 /* SimpleLineChart */ = { isa = PBXNativeTarget; buildConfigurationList = C3FD8187186DFD9A00FD8ED3 /* Build configuration list for PBXNativeTarget "SimpleLineChart" */; @@ -252,6 +415,14 @@ LastUpgradeCheck = 0800; ORGANIZATIONNAME = "Boris Emorine"; TargetAttributes = { + 1E960A801E7DF942000E2BB8 = { + CreatedOnToolsVersion = 8.2.1; + DevelopmentTeam = E8XXXD4S77; + ProvisioningStyle = Automatic; + }; + C3FD8154186DFD9A00FD8ED3 = { + DevelopmentTeam = E8XXXD4S77; + }; C3FD8175186DFD9A00FD8ED3 = { TestTargetID = C3FD8154186DFD9A00FD8ED3; }; @@ -272,11 +443,22 @@ targets = ( C3FD8154186DFD9A00FD8ED3 /* SimpleLineChart */, C3FD8175186DFD9A00FD8ED3 /* SimpleLineChartTests */, + 1E960A801E7DF942000E2BB8 /* TestBed */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 1E960A7F1E7DF942000E2BB8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E960A931E7DF942000E2BB8 /* LaunchScreen.storyboard in Resources */, + 1E960A901E7DF942000E2BB8 /* Assets.xcassets in Resources */, + 1E960A8E1E7DF942000E2BB8 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3FD8153186DFD9A00FD8ED3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -301,6 +483,37 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 1E960A7D1E7DF942000E2BB8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E960B181E7F9C80000E2BB8 /* MSRGBView.m in Sources */, + 1E960AA41E7E094D000E2BB8 /* BEMSimpleLineGraphView.m in Sources */, + 1E960AA51E7E097C000E2BB8 /* BEMCircle.m in Sources */, + 1E960B1A1E7F9C80000E2BB8 /* MSThumbView.m in Sources */, + 1E960B151E7F9C80000E2BB8 /* MSColorUtils.m in Sources */, + 1EA1648A1E9433FD00D77C4A /* NSUserDefaults+Color.m in Sources */, + 1E03A03B1EC8DE8900CA4247 /* UITextField+Numbers.m in Sources */, + 1E960AA61E7E097C000E2BB8 /* BEMLine.m in Sources */, + 1E960AA71E7E097C000E2BB8 /* BEMAverageLine.m in Sources */, + 1E960AA81E7E0990000E2BB8 /* BEMGraphCalculator.m in Sources */, + 1E960AA01E7DF9BC000E2BB8 /* ARFontPickerViewController.m in Sources */, + 1E03A03A1EC8DE8900CA4247 /* UIButton+Switch.m in Sources */, + 1E960B171E7F9C80000E2BB8 /* MSHSBView.m in Sources */, + 1E960B121E7F9C80000E2BB8 /* MSColorComponentView.m in Sources */, + 1E960A881E7DF942000E2BB8 /* AppDelegate.m in Sources */, + 1E960AA31E7DF9BC000E2BB8 /* StatsViewController.m in Sources */, + 1E960AA21E7DF9BC000E2BB8 /* MasterViewController.m in Sources */, + 1E960B141E7F9C80000E2BB8 /* MSColorSelectionViewController.m in Sources */, + 1E960B161E7F9C80000E2BB8 /* MSColorWheelView.m in Sources */, + 1E960A851E7DF942000E2BB8 /* main.m in Sources */, + 1E960AA11E7DF9BC000E2BB8 /* DetailViewController.m in Sources */, + 1E960B131E7F9C80000E2BB8 /* MSColorSelectionView.m in Sources */, + 1E960B1B1E7F9C80000E2BB8 /* UIControl+HitTestEdgeInsets.m in Sources */, + 1E960B191E7F9C80000E2BB8 /* MSSliderView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3FD8151186DFD9A00FD8ED3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -337,6 +550,22 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 1E960A8C1E7DF942000E2BB8 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1E960A8D1E7DF942000E2BB8 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1E960A911E7DF942000E2BB8 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1E960A921E7DF942000E2BB8 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; A64594501BAB257B00D6B8FD /* Launch Screen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -372,6 +601,45 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 1E960A951E7DF942000E2BB8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = E8XXXD4S77; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = TestBed/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = BorisEmorine.TestBed; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1E960A961E7DF942000E2BB8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = E8XXXD4S77; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = TestBed/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = BorisEmorine.TestBed; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; C3FD8185186DFD9A00FD8ED3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -385,6 +653,7 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; @@ -420,6 +689,11 @@ "-Wno-unused-parameter", "-Wno-objc-missing-property-synthesis", "-Wpartial-availability", + "-Wno-double-promotion", + "-Wno-direct-ivar-access", + "-Wno-gnu-conditional-omitted-operand", + "-Wno-gnu-statement-expression", + "-Wno-auto-import", ); }; name = Debug; @@ -437,6 +711,7 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; @@ -466,6 +741,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = E8XXXD4S77; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SimpleLineChart/SimpleLineChart-Prefix.pch"; INFOPLIST_FILE = "SimpleLineChart/SimpleLineChart-Info.plist"; @@ -492,6 +768,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = E8XXXD4S77; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SimpleLineChart/SimpleLineChart-Prefix.pch"; INFOPLIST_FILE = "SimpleLineChart/SimpleLineChart-Info.plist"; @@ -560,6 +837,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 1E960A971E7DF942000E2BB8 /* Build configuration list for PBXNativeTarget "TestBed" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1E960A951E7DF942000E2BB8 /* Debug */, + 1E960A961E7DF942000E2BB8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C3FD8150186DFD9A00FD8ED3 /* Build configuration list for PBXProject "SimpleLineChart" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Sample Project/SimpleLineChart/Base.lproj/Launch Screen.storyboard b/Sample Project/SimpleLineChart/Base.lproj/Launch Screen.storyboard index 9523d07..796ac7a 100644 --- a/Sample Project/SimpleLineChart/Base.lproj/Launch Screen.storyboard +++ b/Sample Project/SimpleLineChart/Base.lproj/Launch Screen.storyboard @@ -1,9 +1,12 @@ - - + + + + + - - + + @@ -15,17 +18,17 @@ - + - + diff --git a/Sample Project/SimpleLineChart/Images.xcassets/AppIcon.appiconset/Contents.json b/Sample Project/SimpleLineChart/Images.xcassets/AppIcon.appiconset/Contents.json index 7bed748..ab33c73 100644 --- a/Sample Project/SimpleLineChart/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Sample Project/SimpleLineChart/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,22 +1,47 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "size" : "29x29", "idiom" : "iphone", "filename" : "AppIcon29x29@2x.png", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, { "size" : "40x40", "idiom" : "iphone", "filename" : "AppIcon40x40@2x.png", "scale" : "2x" }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, { "size" : "60x60", "idiom" : "iphone", "filename" : "AppIcon60x60@2x.png", "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" } ], "info" : { diff --git a/Sample Project/SimpleLineChart/ViewController.h b/Sample Project/SimpleLineChart/ViewController.h index d2d44cf..e41afd2 100644 --- a/Sample Project/SimpleLineChart/ViewController.h +++ b/Sample Project/SimpleLineChart/ViewController.h @@ -14,8 +14,8 @@ @property (weak, nonatomic) IBOutlet BEMSimpleLineGraphView *myGraph; -@property (strong, nonatomic) NSMutableArray *arrayOfValues; -@property (strong, nonatomic) NSMutableArray *arrayOfDates; +@property (strong, nonatomic) NSMutableArray *arrayOfValues; +@property (strong, nonatomic) NSMutableArray *arrayOfDates; @property (strong, nonatomic) IBOutlet UILabel *labelValues; @property (strong, nonatomic) IBOutlet UILabel *labelDates; @@ -29,4 +29,4 @@ - (IBAction)displayStatistics:(id)sender; -@end \ No newline at end of file +@end diff --git a/Sample Project/SimpleLineChart/ViewController.m b/Sample Project/SimpleLineChart/ViewController.m index 985040d..e00497d 100644 --- a/Sample Project/SimpleLineChart/ViewController.m +++ b/Sample Project/SimpleLineChart/ViewController.m @@ -41,9 +41,12 @@ - (void)viewDidLoad { }; // Apply the gradient to the bottom portion of the graph - self.myGraph.gradientBottom = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations); + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations); + self.myGraph.gradientBottom = gradient; + CGColorSpaceRelease(colorspace); - // Note: clang analyzer will complain about leak of gradient, but we use assign on gradient properties to avoid leak + CGGradientRelease(gradient); + // Enable and disable various graph properties and axis displays self.myGraph.enableTouchReport = YES; @@ -98,7 +101,7 @@ - (void)hydrateDatasets { BOOL showNullValue = YES; // Add objects to the array based on the stepper value - for (int i = 0; i < 9; i++) { + for (NSUInteger i = 0; i < 9; i++) { [self.arrayOfValues addObject:@([self getRandomFloat])]; // Random values for the graph if (i == 0) { [self.arrayOfDates addObject:baseDate]; // Dates for the X-Axis of the graph @@ -120,7 +123,7 @@ - (NSDate *)dateForGraphAfterDate:(NSDate *)date { } - (NSString *)labelForDateAtIndex:(NSInteger)index { - NSDate *date = self.arrayOfDates[index]; + NSDate *date = self.arrayOfDates[(NSUInteger)index]; NSDateFormatter *df = [[NSDateFormatter alloc] init]; df.dateFormat = @"MM/dd"; NSString *label = [df stringFromDate:date]; @@ -200,28 +203,28 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { #pragma mark - SimpleLineGraph Data Source -- (NSUInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView *)graph { +- (NSInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView *)graph { return (int)[self.arrayOfValues count]; } -- (CGFloat)lineGraph:(BEMSimpleLineGraphView *)graph valueForPointAtIndex:(NSUInteger)index { - return [[self.arrayOfValues objectAtIndex:index] doubleValue]; +- (CGFloat)lineGraph:(BEMSimpleLineGraphView *)graph valueForPointAtIndex:(NSInteger)index { + return self.arrayOfValues [(NSUInteger)index].doubleValue; } #pragma mark - SimpleLineGraph Delegate -- (NSUInteger)numberOfGapsBetweenLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph { +- (NSInteger)numberOfGapsBetweenLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph { return 2; } -- (NSString *)lineGraph:(BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSUInteger)index { +- (NSString *)lineGraph:(BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSInteger)index { NSString *label = [self labelForDateAtIndex:index]; return [label stringByReplacingOccurrencesOfString:@" " withString:@"\n"]; } -- (void)lineGraph:(BEMSimpleLineGraphView *)graph didTouchGraphWithClosestIndex:(NSUInteger)index { - self.labelValues.text = [NSString stringWithFormat:@"%@", [self.arrayOfValues objectAtIndex:index]]; +- (void)lineGraph:(BEMSimpleLineGraphView *)graph didTouchGraphWithClosestIndex:(NSInteger)index { + self.labelValues.text = [NSString stringWithFormat:@"%@", self.arrayOfValues [(NSUInteger)index]]; self.labelDates.text = [NSString stringWithFormat:@"in %@", [self labelForDateAtIndex:index]]; } @@ -231,7 +234,7 @@ - (void)lineGraph:(BEMSimpleLineGraphView *)graph didReleaseTouchFromGraphWithCl self.labelDates.alpha = 0.0f; } completion:^(BOOL finished) { self.labelValues.text = [NSString stringWithFormat:@"%i", [[[BEMGraphCalculator sharedCalculator] calculatePointValueSumOnGraph:self.myGraph] intValue]]; - self.labelDates.text = [NSString stringWithFormat:@"between %@ and %@", [self labelForDateAtIndex:0], [self labelForDateAtIndex:self.arrayOfDates.count - 1]]; + self.labelDates.text = [NSString stringWithFormat:@"between %@ and %@", [self labelForDateAtIndex:0], [self labelForDateAtIndex:(NSInteger)(self.arrayOfDates.count) - 1]]; [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ self.labelValues.alpha = 1.0f; @@ -244,7 +247,7 @@ - (void)lineGraphDidFinishLoading:(BEMSimpleLineGraphView *)graph { if (self.arrayOfValues.count > 0) { NSNumber *pointSum = [[BEMGraphCalculator sharedCalculator] calculatePointValueSumOnGraph:self.myGraph]; self.labelValues.text = [NSString stringWithFormat:@"%i", [pointSum intValue]]; - self.labelDates.text = [NSString stringWithFormat:@"between %@ and %@", [self labelForDateAtIndex:0], [self labelForDateAtIndex:self.arrayOfDates.count - 1]]; + self.labelDates.text = [NSString stringWithFormat:@"between %@ and %@", [self labelForDateAtIndex:0], [self labelForDateAtIndex:(NSInteger)(self.arrayOfDates.count) - 1]]; } else { self.labelValues.text = @"No data"; self.labelDates.text = @""; diff --git a/Sample Project/SimpleLineChartTests/CustomizationTests.m b/Sample Project/SimpleLineChartTests/CustomizationTests.m index 4aad044..c98af20 100644 --- a/Sample Project/SimpleLineChartTests/CustomizationTests.m +++ b/Sample Project/SimpleLineChartTests/CustomizationTests.m @@ -12,7 +12,7 @@ #pragma clang diagnostic ignored "-Wfloat-equal" @interface BEMSimpleLineGraphView () -// Allow tester to get to internal properties +//Allow tester to get to internal properties /// All of the dataPoint labels @property (strong, nonatomic) NSMutableArray *permanentPopups; @@ -32,7 +32,7 @@ @implementation CustomizationTests - (void)setUp { [super setUp]; - + self.lineGraph = [[BEMSimpleLineGraphView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]; self.lineGraph.delegate = self; self.lineGraph.dataSource = self; @@ -40,15 +40,15 @@ - (void)setUp { #pragma mark BEMSimpleLineGraph Data Source -- (NSUInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView * __nonnull)graph { - return numberOfPoints; +- (NSInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView * __nonnull)graph { + return (NSInteger)numberOfPoints; } -- (CGFloat)lineGraph:(BEMSimpleLineGraphView * __nonnull)graph valueForPointAtIndex:(NSUInteger)index { +- (CGFloat)lineGraph:(BEMSimpleLineGraphView * __nonnull)graph valueForPointAtIndex:(NSInteger)index { return pointValue; } -- (NSString *)lineGraph:(nonnull BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSUInteger)index { +- (NSString *)lineGraph:(nonnull BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSInteger)index { return xAxisLabelString; } @@ -64,14 +64,14 @@ - (NSString *)popUpSuffixForlineGraph:(BEMSimpleLineGraphView * __nonnull)graph - (void)testDotCustomization { CGFloat sizePoint = 20.0; - + self.lineGraph.alwaysDisplayDots = YES; self.lineGraph.animationGraphEntranceTime = 0.0; self.lineGraph.sizePoint = sizePoint; self.lineGraph.colorPoint = [UIColor greenColor]; [self.lineGraph reloadGraph]; - - NSArray *dots = self.lineGraph.circleDots; + + NSArray *dots = self.lineGraph.circleDots; XCTAssert(dots.count == numberOfPoints, @"There should be as many BEMCircle views in the graph's subviews as the data source method 'numberOfPointsInLineGraph:' returns"); @@ -90,10 +90,10 @@ - (void)testXAxisCustomization { self.lineGraph.labelFont = font; self.lineGraph.colorXaxisLabel = [UIColor greenColor]; [self.lineGraph reloadGraph]; - - NSArray *labels = [self.lineGraph graphLabelsForXAxis]; + + NSArray *labels = [self.lineGraph graphLabelsForXAxis]; XCTAssert(labels.count == numberOfPoints, @"The number of X-Axis labels should be the same as the number of points on the graph"); - + for (UILabel *XAxisLabel in labels) { XCTAssert([XAxisLabel isMemberOfClass:[UILabel class]], @"The array returned by 'graphLabelsForXAxis' should only return UILabels"); XCTAssert([XAxisLabel.text isEqualToString:xAxisLabelString], @"The X-Axis label's strings should be the same as the one returned by the data source method 'labelOnXAxisForIndex:'"); @@ -110,17 +110,17 @@ - (void)testPopUps { UIFont *font = [UIFont systemFontOfSize:25.0]; self.lineGraph.labelFont = font; [self.lineGraph reloadGraph]; - - NSMutableArray *popUps = self.lineGraph.permanentPopups; - + + NSMutableArray *popUps = self.lineGraph.permanentPopups; + XCTAssert(popUps.count == numberOfPoints, @"We should have a popup above each and every dot"); - NSString *expectedLabelText = [NSString stringWithFormat:@"%@%.f%@", popUpPrefix,pointValue,popUpSuffix]; + NSString *expectedLabelText = [NSString stringWithFormat:@"%@%.f%@", popUpPrefix,pointValue,popUpSuffix]; for (UILabel *popUp in popUps) { XCTAssert([popUp isMemberOfClass:[UILabel class]],@"Popups must be label class"); // XCTAssert(popUp.backgroundColor == [UIColor greenColor],@"") // XCTAssert(popUp.layer.alpha >= 0.69 && popUp.alpha <= 0.71, @"The popups should always be displayed and have an alpha of 0.7"); - UIColor * expectedColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.7f]; - XCTAssert(CGColorEqualToColor(popUp.layer.backgroundColor, expectedColor.CGColor), @"The popups background color should be the one set by the property"); + UIColor * expectedColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.7f]; + XCTAssert(CGColorEqualToColor(popUp.layer.backgroundColor, expectedColor.CGColor), @"The popups background color should be the one set by the property"); XCTAssert([popUp.text isEqualToString:expectedLabelText], @"The popup labels should display the value of the dot and the suffix and prefix returned by the delegate"); XCTAssert(popUp.font == font, @"The popup label's font is expected to be the customized one"); XCTAssert(popUp.backgroundColor == [UIColor clearColor], @"The popup label's background color should always be clear color"); diff --git a/Sample Project/SimpleLineChartTests/SimpleLineChartTests.m b/Sample Project/SimpleLineChartTests/SimpleLineChartTests.m index c86e757..7f21a07 100644 --- a/Sample Project/SimpleLineChartTests/SimpleLineChartTests.m +++ b/Sample Project/SimpleLineChartTests/SimpleLineChartTests.m @@ -33,7 +33,7 @@ @implementation SimpleLineGraphTests - (void)setUp { [super setUp]; - + self.lineGraph = [[BEMSimpleLineGraphView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]; self.lineGraph.delegate = self; self.lineGraph.dataSource = self; @@ -41,15 +41,15 @@ - (void)setUp { #pragma mark BEMSimpleLineGraph Data Source -- (NSUInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView * __nonnull)graph { - return numberOfPoints; +- (NSInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView * __nonnull)graph { + return (NSInteger)numberOfPoints; } -- (CGFloat)lineGraph:(BEMSimpleLineGraphView * __nonnull)graph valueForPointAtIndex:(NSUInteger)index { +- (CGFloat)lineGraph:(BEMSimpleLineGraphView * __nonnull)graph valueForPointAtIndex:(NSInteger)index { return pointValue; } -- (NSString *)lineGraph:(nonnull BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSUInteger)index { +- (NSString *)lineGraph:(nonnull BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSInteger)index { return xAxisLabelString; } @@ -71,10 +71,10 @@ - (void)testReloadDataPerformance { - (void)testGraphValuesForXAxis { [self.lineGraph reloadGraph]; - - NSArray *xAxisStrings = [self.lineGraph graphValuesForXAxis]; + + NSArray *xAxisStrings = [self.lineGraph graphValuesForXAxis]; XCTAssert(xAxisStrings.count == numberOfPoints, @"The number of strings on the X-Axis should be equal to the number returned by the data source method 'numberOfPointsInLineGraph:'"); - + for (NSString *xAxisString in xAxisStrings) { XCTAssert([xAxisString isKindOfClass:[NSString class]], @"The array returned by 'graphValuesForXAxis' should only return NSStrings"); XCTAssert([xAxisString isEqualToString:xAxisLabelString], @"The X-Axis strings should be the same as the one returned by the data source method 'labelOnXAxisForIndex:'"); @@ -83,11 +83,11 @@ - (void)testGraphValuesForXAxis { - (void)testGraphValuesForDataPoints { [self.lineGraph reloadGraph]; - - NSArray *values = [self.lineGraph graphValuesForDataPoints]; + + NSArray *values = [self.lineGraph graphValuesForDataPoints]; XCTAssert(values.count == numberOfPoints, @"The number of data points should be equal to the number returned by the data source method 'numberOfPointsInLineGraph:'"); - - NSMutableArray *mockedValues = [NSMutableArray new]; + + NSMutableArray *mockedValues = [NSMutableArray new]; for (NSUInteger i = 0; i < numberOfPoints; i++) { [mockedValues addObject:[NSNumber numberWithFloat:pointValue]]; } @@ -97,15 +97,17 @@ - (void)testGraphValuesForDataPoints { - (void)testDrawnPoints { self.lineGraph.animationGraphEntranceTime = 0.0; [self.lineGraph reloadGraph]; - - NSMutableArray *dots = self.lineGraph.circleDots; + + NSMutableArray *dots = self.lineGraph.circleDots; XCTAssert(dots.count == numberOfPoints, @"There should be as many BEMCircle views in the graph's subviews as the data source method 'numberOfPointsInLineGraph:' returns"); - + for (BEMCircle *dot in dots) { XCTAssert(dot.bounds.size.width == 10.0, @"Dots are expected to have a default width of 10.0"); XCTAssert(dot.bounds.size.height == 10.0, @"Dots are expected to have a default height of 10.0"); - XCTAssert([dot.color isEqual:[UIColor colorWithWhite:1.0f alpha:0.7f]], @"Dots are expected to be white at alpha 0.7 by default"); + //following ugliness necessary for Extended Grey space on newer devices + CGFloat alpha, white; + XCTAssert([dot.color getWhite: &white alpha:&alpha] && fabs(white - 1.0) < .00001 && fabs(alpha - 0.7) < .00001, @"Dots are expected to be white at alpha 0.7 by default"); XCTAssert(dot.absoluteValue == pointValue, @"Dots are expected to have a value equal to the value returned by the data source method 'valueForPointAtIndex:'"); XCTAssert(dot.alpha == 0.0, @"Dots are expected to not be displayed by default (alpha of 0)"); XCTAssert([dot.backgroundColor isEqual:[UIColor clearColor]], @"Dots are expected to have a clearColor background color by default"); @@ -115,15 +117,15 @@ - (void)testDrawnPoints { - (void)testGraphLabelsForXAxis { self.lineGraph.enableXAxisLabel = NO; [self.lineGraph reloadGraph]; - + XCTAssert([self.lineGraph graphLabelsForXAxis].count == 0, @"Should be no labels on XAxis"); - + self.lineGraph.enableXAxisLabel = YES; [self.lineGraph reloadGraph]; - - NSArray *labels = [self.lineGraph graphLabelsForXAxis]; + + NSArray *labels = [self.lineGraph graphLabelsForXAxis]; XCTAssert(labels.count == numberOfPoints, @"The number of X-Axis labels should be the same as the number of points on the graph"); - + for (UILabel *XAxisLabel in labels) { XCTAssert([XAxisLabel isMemberOfClass:[UILabel class]], @"The array returned by 'graphLabelsForXAxis' should only return UILabels"); XCTAssert([XAxisLabel.text isEqualToString:xAxisLabelString], @"The X-Axis label's strings should be the same as the one returned by the data source method 'labelOnXAxisForIndex:'"); @@ -142,9 +144,9 @@ - (void)testYAxisLabels { self.lineGraph.enableYAxisLabel = YES; [self.lineGraph reloadGraph]; - + NSString *value = [NSString stringWithFormat:@"%.f", pointValue]; - NSMutableArray * yAxisLabels = [NSMutableArray array]; + NSMutableArray * yAxisLabels = [NSMutableArray array]; for (UILabel *label in [self.lineGraph graphLabelsForYAxis]) { if (label.superview) { [yAxisLabels addObject:label]; @@ -153,7 +155,7 @@ - (void)testYAxisLabels { XCTAssert([label.textColor isEqual:[UIColor blackColor]], @"The Y-Axis label is expected to have a text color of black by default"); XCTAssert([label.backgroundColor isEqual:[UIColor clearColor]], @"The Y-Axis label is expected to have a background color of clear by default"); } - + XCTAssert(yAxisLabels.count == 1, @"With all the dots having the same value, we only expect one Y axis label"); } diff --git a/Sample Project/TestBed/ARFontPickerViewController.h b/Sample Project/TestBed/ARFontPickerViewController.h new file mode 100644 index 0000000..b1a9e4f --- /dev/null +++ b/Sample Project/TestBed/ARFontPickerViewController.h @@ -0,0 +1,46 @@ +// +// ARFontPickerViewController.h +// +// Created by Alexander Repty on 15.03.10. +// +// Copyright (c) 2010, Alexander Repty +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// Neither the name of Alexander Repty nor the names of his contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +@import UIKit; + +@class ARFontPickerViewController; + +@protocol ARFontPickerViewControllerDelegate + +- (void)fontPickerViewController:(ARFontPickerViewController *)fontPicker didSelectFont:(NSString *)fontName; + +@end + +@interface ARFontPickerViewController : UITableViewController + +@property(nonatomic,weak) id delegate; + +@end diff --git a/Sample Project/TestBed/ARFontPickerViewController.m b/Sample Project/TestBed/ARFontPickerViewController.m new file mode 100644 index 0000000..9895196 --- /dev/null +++ b/Sample Project/TestBed/ARFontPickerViewController.m @@ -0,0 +1,98 @@ +// +// ARFontPickerViewController.m +// +// Created by Alexander Repty on 15.03.10. +// +// Copyright (c) 2010, Alexander Repty +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// Neither the name of Alexander Repty nor the names of his contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#import "ARFontPickerViewController.h" + +static NSString * kARFontPickerViewControllerCellIdentifier = @"ARFontPickerViewControllerCellIdentifier"; + +@implementation ARFontPickerViewController + +#pragma mark - +#pragma mark UITableViewController methods + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return (NSInteger)[[UIFont familyNames] count]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSString *familyName = [self fontFamilyForSection:section]; + return (NSInteger)[[UIFont fontNamesForFamilyName:familyName] count]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return [self fontFamilyForSection:section]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kARFontPickerViewControllerCellIdentifier]; + + NSString *familyName = [self fontFamilyForSection:indexPath.section]; + NSString *fontName = [self fontNameForRow:indexPath.row inFamily:familyName]; + UIFont *font = [UIFont fontWithName:fontName size:[UIFont smallSystemFontSize]]; + + cell.textLabel.text = fontName; + cell.textLabel.font = font; + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + if (self.delegate != nil) { + NSString *familyName = [self fontFamilyForSection:indexPath.section]; + NSString *fontName = [self fontNameForRow:indexPath.row inFamily:familyName]; + [self.delegate fontPickerViewController:self didSelectFont:fontName]; + } +} + +- (NSString *)fontFamilyForSection:(NSInteger)section { + if (section < 0) return nil; + NSUInteger realSection = (NSUInteger) section; + NSArray < NSString *> *fontNames = [UIFont familyNames]; + if (realSection < fontNames.count) { + return fontNames[realSection]; + } else { + return nil; + } +} + +- (NSString *)fontNameForRow:(NSInteger)row inFamily:(NSString *)family { + if (row < 0) return nil; + NSUInteger realRow = (NSUInteger) row; + NSArray < NSString *> *fontNames = [UIFont fontNamesForFamilyName:family]; + if (realRow < fontNames.count) { + return fontNames[realRow]; + } else { + return nil; + } +} + +@end + diff --git a/Sample Project/TestBed/AppDelegate.h b/Sample Project/TestBed/AppDelegate.h new file mode 100644 index 0000000..8429d01 --- /dev/null +++ b/Sample Project/TestBed/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// TestBed +// +// Created by Hugh Mackworth on 3/18/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +@import UIKit; + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/Sample Project/TestBed/AppDelegate.m b/Sample Project/TestBed/AppDelegate.m new file mode 100644 index 0000000..326b587 --- /dev/null +++ b/Sample Project/TestBed/AppDelegate.m @@ -0,0 +1,60 @@ +// +// AppDelegate.m +// TestBed +// +// Created by Hugh Mackworth on 3/18/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +//- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder { +// return YES; +//} +// +//- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder { +// return YES; +//} +// + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. +} + + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + +} + + +@end diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png new file mode 100644 index 0000000..87a937d Binary files /dev/null and b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png differ diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png new file mode 100644 index 0000000..a7a3d39 Binary files /dev/null and b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png differ diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png new file mode 100644 index 0000000..5c7a532 Binary files /dev/null and b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png differ diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png new file mode 100644 index 0000000..6526a78 Binary files /dev/null and b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x.png differ diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x.png b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x.png new file mode 100644 index 0000000..3c77090 Binary files /dev/null and b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x.png differ diff --git a/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d548d25 --- /dev/null +++ b/Sample Project/TestBed/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,133 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon29x29@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon40x40@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon60x60@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "AppIcon83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Sample Project/TestBed/Assets.xcassets/Contents.json b/Sample Project/TestBed/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Sample Project/TestBed/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Sample Project/TestBed/Base.lproj/LaunchScreen.storyboard b/Sample Project/TestBed/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f6372cf --- /dev/null +++ b/Sample Project/TestBed/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sample Project/TestBed/Base.lproj/Main.storyboard b/Sample Project/TestBed/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f0756af --- /dev/null +++ b/Sample Project/TestBed/Base.lproj/Main.storyboard @@ -0,0 +1,3391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sample Project/TestBed/DetailViewController.h b/Sample Project/TestBed/DetailViewController.h new file mode 100644 index 0000000..968ff16 --- /dev/null +++ b/Sample Project/TestBed/DetailViewController.h @@ -0,0 +1,52 @@ +// +// DetailViewController.h +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/19/16. +// Copyright © 2016 Boris Emorine. All rights reserved. +// + +@import UIKit; +#import "BEMSimpleLineGraphView.h" + +@interface DetailViewController : UIViewController + +@property (weak, nonatomic) IBOutlet BEMSimpleLineGraphView *myGraph; + +- (void)addPointToGraph; +- (void)removePointFromGraph; + +- (IBAction)refresh:(id)sender; + +@property (readonly) NSDate * oldestDate, * newestDate; +@property (readonly) CGFloat smallestValue, biggestValue; +@property (assign, nonatomic) NSInteger numberOfPoints; +@property (assign, nonatomic) float percentNulls; + +//data needed to implement delegate methods +//@"" or negative float or NSNotFound or FALSE to indicate don't provide a delegate method +@property (strong, nonatomic) NSString *popUpText; +@property (strong, nonatomic) NSString *popUpPrefix; +@property (strong, nonatomic) NSString *popUpSuffix; +@property (assign, nonatomic) BOOL testAlwaysDisplayPopup; +@property (assign, nonatomic) CGFloat maxValue; +@property (assign, nonatomic) CGFloat minValue; +@property (assign, nonatomic) CGFloat maxXValue; +@property (assign, nonatomic) CGFloat minXValue; +@property (assign, nonatomic) BOOL variableXAxis; +@property (assign, nonatomic) NSInteger numberofXAxisLabels; +@property (assign, nonatomic) BOOL noDataLabel; +@property (strong, nonatomic) NSString *noDataText; +@property (assign, nonatomic) CGFloat staticPaddingValue; +@property (assign, nonatomic) BOOL provideCustomView; +@property (assign, nonatomic) NSInteger numberOfGapsBetweenLabels; +@property (assign, nonatomic) NSInteger baseIndexForXAxis; +@property (assign, nonatomic) NSInteger incrementIndexForXAxis; +@property (assign, nonatomic) BOOL provideIncrementPositionsForXAxis; +@property (assign, nonatomic) NSInteger numberOfYAxisLabels; +@property (strong, nonatomic) NSString *yAxisPrefix; +@property (strong, nonatomic) NSString *yAxisSuffix; +@property (assign, nonatomic) CGFloat baseValueForYAxis; +@property (assign, nonatomic) CGFloat incrementValueForYAxis; + +@end diff --git a/Sample Project/TestBed/DetailViewController.m b/Sample Project/TestBed/DetailViewController.m new file mode 100644 index 0000000..454875a --- /dev/null +++ b/Sample Project/TestBed/DetailViewController.m @@ -0,0 +1,507 @@ +// +// DetailViewController.m +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/19/16. +// Copyright © 2016 Boris Emorine. All rights reserved. +// + +#import "DetailViewController.h" +#import "StatsViewController.h" +#import "BEMGraphCalculator.h" + +@interface DetailViewController () + +@property (strong, nonatomic) NSDate * oldestDate, * newestDate; +@property (assign, nonatomic) CGFloat smallestValue, biggestValue; + +@property (weak, nonatomic) IBOutlet UIStepper *graphObjectIncrement; +@property (strong, nonatomic) IBOutlet UIActivityIndicatorView * activity; + +@property (strong, nonatomic) NSMutableArray *arrayOfValues; +@property (strong, nonatomic) NSMutableArray *arrayOfDates; + +@property (strong, nonatomic) IBOutlet UILabel *labelValues; +@property (strong, nonatomic) IBOutlet UILabel *labelDates; + +@property (nonatomic) NSInteger totalNumber; +@property (strong, nonatomic) NSDateFormatter * dateFormatter; +@property (strong, nonatomic) IBOutlet UIView * customView; +@property (weak, nonatomic) IBOutlet UILabel * customViewLabel; + +@property (strong, nonatomic) NSDateFormatter * dateFormatterYears; +@property (strong, nonatomic) NSDateFormatter * dateFormatterMonths; +@property (strong, nonatomic) NSDateFormatter * dateFormatterDays; +@property (strong, nonatomic) NSDateFormatter * dateFormatterHours; + +@end + +@implementation DetailViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.activity = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + self.navigationItem.leftItemsSupplementBackButton = YES; + UIBarButtonItem * activityButton = [[UIBarButtonItem alloc] initWithCustomView:self.activity]; + self.navigationItem.rightBarButtonItems = [self.navigationItem.rightBarButtonItems arrayByAddingObject:activityButton]; + + self.maxValue = -1.0; + self.minValue = -1.0; + self.maxXValue = -1.0; + self.minXValue = -1.0; + self.staticPaddingValue = -1.0; + self.numberOfGapsBetweenLabels = -1; + self.baseIndexForXAxis = -1; + self.incrementIndexForXAxis = -1; + self.numberOfYAxisLabels = -1; + self.baseValueForYAxis = -1.0; + self.incrementValueForYAxis = -1.0; + self.dateFormatterYears = [[NSDateFormatter alloc] init]; self.dateFormatterYears.dateFormat = @"M-YY"; + self.dateFormatterMonths = [[NSDateFormatter alloc] init]; self.dateFormatterMonths.dateFormat = @"MMM/dd"; + self.dateFormatterDays = [[NSDateFormatter alloc] init]; self.dateFormatterDays.dateFormat = @"dd@HH"; + self.dateFormatterHours = [[NSDateFormatter alloc] init]; self.dateFormatterHours.dateFormat = @"HH:mm"; + self.dateFormatter = [[NSDateFormatter alloc] init]; self.dateFormatter.dateFormat = @"M/d/yy"; + // self.dateFormatter = self.dateFormatterYears; + self.variableXAxis = NO; + self.myGraph.adaptiveDataPoints = YES; + _percentNulls = .2; + + // Do any additional setup after loading the view. + + _numberOfPoints = 10; + + [self hydrateDatasets]; + + [self updateLabelsBelowGraph:self.myGraph]; +} + +#pragma mark Data management + +- (void)setNumberOfPoints:(NSInteger)numberOfPoints { + if (numberOfPoints != _numberOfPoints) { + NSInteger oldNumberOfPoints = _numberOfPoints; + + _numberOfPoints = numberOfPoints; + + if (numberOfPoints == oldNumberOfPoints + 1) { + [self addPointToGraph]; + } else if (numberOfPoints == oldNumberOfPoints - 1) { + [self removePointFromGraph]; + } else { + [self hydrateDatasets]; + } + [self.myGraph reloadGraph]; + } +} + +float randomProbability () { + return (float) ((double)(arc4random())) / UINT32_MAX; +} + +- (void)hydrateDatasets { + // Reset the arrays of values (Y-Axis points) and dates (X-Axis points / labels) + if (!self.arrayOfValues) self.arrayOfValues = [[NSMutableArray alloc] init]; + if (!self.arrayOfDates) self.arrayOfDates = [[NSMutableArray alloc] init]; + [self.arrayOfValues removeAllObjects]; + [self.arrayOfDates removeAllObjects]; + + NSDate *date = [NSDate date]; + // Add objects to the array based on the stepper value + CGFloat lastValue = 5000; + for (int i = 0; i < self.numberOfPoints; i++) { + if (randomProbability() < self.percentNulls) { + [self.arrayOfValues addObject: @(BEMNullGraphValue)]; + } else { + CGFloat value =MAX(lastValue + [self getRandomFloat]-500, 500); + [self.arrayOfValues addObject:@(value)]; // Random values for the graph + lastValue = value; + } + [self.arrayOfDates addObject:date]; // Dates for the X-Axis of the graph + date = [self dateForGraphAfterDate:date]; + } + [self checkMaximums]; + self.graphObjectIncrement.value = self.numberOfPoints; +} + +- (void)checkMaximums { + self.oldestDate = [NSDate distantFuture]; + self.newestDate = [NSDate distantPast]; + self.biggestValue = -INFINITY; + self.smallestValue = INFINITY; + self.totalNumber = 0; + for (NSInteger i = 0; i < self.numberOfPoints; i++) { + CGFloat value = self.arrayOfValues[(NSUInteger)i].floatValue; + if (value < BEMNullGraphValue) { + self.totalNumber = self.totalNumber + value; + self.biggestValue = MAX(self.biggestValue,value ); + self.smallestValue = MIN(self.smallestValue,value ); + } + } + if (self.arrayOfDates.count > 0) self.oldestDate = self.arrayOfDates[0]; + self.newestDate = [self.arrayOfDates lastObject]; //needs to be last for notification + +} +- (NSDate *)dateForGraphAfterDate:(NSDate *)date { + CGFloat zeroToOne = arc4random() / (float) UINT_MAX; + CGFloat exponentialSeconds = -log(1-zeroToOne) ; //exponential dist with 1 second mean + NSDate *newDate = [date dateByAddingTimeInterval:exponentialSeconds* (30 * 24 * 60 * 60)]; + return newDate; +} + +- (NSString *)labelForDateAtIndex:(NSInteger)index { + NSDate *date = self.arrayOfDates[(NSUInteger)index]; + NSString *label = [self.dateFormatter stringFromDate:date]; + return label; +} + +- (void)setPercentNulls:(float)percentNulls { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(refresh:) object:nil]; + _percentNulls = percentNulls; + [self performSelector:@selector(refresh:) withObject:nil afterDelay:0.25]; +} + +#pragma mark - Graph Actions + +// Refresh the line graph using the specified properties +- (IBAction)refresh:(id)sender { + [self hydrateDatasets]; + self.myGraph.zoomScale = 1.0; + [self.myGraph reloadGraph]; +} + +- (float)getRandomFloat { + float i1 = (float)(arc4random() % 1000) ; + return i1; +} + +- (IBAction)addOrRemovePointFromGraph:(id)sender { + self.numberOfPoints = self.graphObjectIncrement.value; +} + +- (void)addPointToGraph { + // Add point + NSNumber * newValue ; + if (randomProbability() < self.percentNulls) { + newValue = @(BEMNullGraphValue); + } else { + newValue = @([self getRandomFloat]); + self.biggestValue = MAX(self.biggestValue, newValue.floatValue ); + self.smallestValue = MIN(self.smallestValue, newValue.floatValue ); + } + [self.arrayOfValues addObject:newValue]; + NSDate *lastDate = self.arrayOfDates.count > 0 ? [self.arrayOfDates lastObject]: [NSDate date]; + NSDate *newDate = [self dateForGraphAfterDate:lastDate]; + self.oldestDate = newDate; + [self.arrayOfDates addObject:newDate]; +} + +- (void)removePointFromGraph { + if (self.arrayOfValues.count > 0) { + // Remove point + [self.arrayOfValues removeObjectAtIndex:0]; + [self.arrayOfDates removeObjectAtIndex:0]; + [self checkMaximums]; + } +} + +- (NSString *)formatNumber:(NSNumber *)number { + return [NSNumberFormatter localizedStringFromNumber:number + numberStyle:NSNumberFormatterDecimalStyle]; +} + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + [super prepareForSegue:segue sender:sender]; + + if ([segue.identifier isEqualToString:@"showStats"]) { + BEMGraphCalculator * calc = [BEMGraphCalculator sharedCalculator]; + StatsViewController *controller = (StatsViewController *)((UINavigationController *)segue.destinationViewController).topViewController; + controller.standardDeviation = [self formatNumber:[calc calculateStandardDeviationOnGraph:self.myGraph]]; + controller.average = [self formatNumber:[calc calculatePointValueAverageOnGraph:self.myGraph]]; + controller.median = [self formatNumber:[calc calculatePointValueMedianOnGraph: self.myGraph]]; + controller.mode = [self formatNumber:[calc calculatePointValueModeOnGraph: self.myGraph]]; + controller.minimum = [self formatNumber:[calc calculateMinimumPointValueOnGraph:self.myGraph]]; + controller.maximum = [self formatNumber:[calc calculateMaximumPointValueOnGraph:self.myGraph]]; + controller.area = [self formatNumber:[calc calculateAreaUsingIntegrationMethod: BEMIntegrationMethodLeftReimannSum onGraph:self.myGraph xAxisScale:@(1)]]; + controller.correlation = [self formatNumber:[calc calculateCorrelationCoefficientUsingCorrelationMethod:BEMCorrelationMethodPearson onGraph:self.myGraph xAxisScale:@(1)]]; + controller.snapshotImage = [self.myGraph graphSnapshotImage]; + } +} + + +#pragma mark - SimpleLineGraph Data Source + +- (NSInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView *)graph { + return (NSInteger)[self.arrayOfValues count]; +} + +- (CGFloat)lineGraph:(BEMSimpleLineGraphView *)graph valueForPointAtIndex:(NSInteger)index { + return self.arrayOfValues [(NSUInteger)index].doubleValue; +} + +- (NSInteger)numberOfXAxisLabelsOnLineGraph:graph { + return self.numberofXAxisLabels; +} + +- (CGFloat)lineGraph:(BEMSimpleLineGraphView *)graph locationForPointAtIndex:(NSInteger)index { + + return [self.arrayOfDates [(NSUInteger)index] timeIntervalSinceReferenceDate]; +} + +#pragma mark - SimpleLineGraph Delegate + +- (BOOL)respondsToSelector:(SEL)aSelector { + if (aSelector == @selector(popUpTextForlineGraph:atIndex:)) { + return self.popUpText.length > 0; + } else if (aSelector == @selector(popUpPrefixForlineGraph:)) { + return self.popUpPrefix.length > 0; + } else if (aSelector == @selector(popUpSuffixForlineGraph:)) { + return self.popUpSuffix.length > 0; + } else if (aSelector == @selector(lineGraph:alwaysDisplayPopUpAtIndex:)) { + return self.testAlwaysDisplayPopup; + } else if (aSelector == @selector(maxValueForLineGraph:)) { + return self.maxValue >= 0.0; + } else if (aSelector == @selector(minValueForLineGraph:)) { + return self.minValue >= 0.0; + } else if (aSelector == @selector(maxXValueForLineGraph:)) { + return self.maxXValue >= 0.0; + } else if (aSelector == @selector(minXValueForLineGraph:)) { + return self.minXValue >= 0.0; + } else if (aSelector == @selector(lineGraph:locationForPointAtIndex:)) { + return self.variableXAxis; + } else if (aSelector == @selector(numberOfXAxisLabelsOnLineGraph:)) { + return self.numberofXAxisLabels > 0; + } else if (aSelector == @selector(noDataLabelTextForLineGraph:)) { + return self.noDataText.length > 0; + } else if (aSelector == @selector(staticPaddingForLineGraph:)) { + return self.staticPaddingValue > 0; + } else if (aSelector == @selector(popUpViewForLineGraph:)) { + return self.provideCustomView; + } else if (aSelector == @selector(lineGraph:modifyPopupView:forIndex:)) { + return self.provideCustomView; + } else if (aSelector == @selector(numberOfGapsBetweenLabelsOnLineGraph:)) { + return self.numberOfGapsBetweenLabels >= 0; + } else if (aSelector == @selector(baseIndexForXAxisOnLineGraph:)) { + return self.baseIndexForXAxis >= 0; + } else if (aSelector == @selector(incrementIndexForXAxisOnLineGraph:)) { + return self.incrementIndexForXAxis >= 0; + } else if (aSelector == @selector(incrementPositionsForXAxisOnLineGraph:)) { + return self.provideIncrementPositionsForXAxis; + } else if (aSelector == @selector(numberOfYAxisLabelsOnLineGraph:)) { + return self.numberOfYAxisLabels >= 0; + } else if (aSelector == @selector(yAxisPrefixOnLineGraph:)) { + return self.yAxisPrefix.length > 0; + } else if (aSelector == @selector(yAxisSuffixOnLineGraph:)) { + return self.yAxisSuffix.length > 0; + } else if (aSelector == @selector(baseValueForYAxisOnLineGraph:)) { + return self.baseValueForYAxis >= 0; + } else if (aSelector == @selector(incrementValueForYAxisOnLineGraph:)) { + return self.baseValueForYAxis >= 0.0; + } else { + return [super respondsToSelector:aSelector]; + } +} + +- (void)lineGraphDidBeginLoading:(BEMSimpleLineGraphView *)graph { + [self.activity startAnimating]; + +} + +- (void)lineGraphDidFinishDrawing:(BEMSimpleLineGraphView *)graph { + [self.activity stopAnimating]; + +} + + +- (NSString *)lineGraph:(BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSInteger)index { +// return [NSString stringWithFormat:@"%lu", (long)index]; + NSDate *date = self.arrayOfDates[(NSUInteger)index]; + NSString *label = [self.dateFormatter stringFromDate:date]; + return [label stringByReplacingOccurrencesOfString:@" " withString:@"\n"]; +} + +- (nullable NSString *)lineGraph:(nonnull BEMSimpleLineGraphView *)graph labelOnXAxisForLocation:(CGFloat)location atLabelIndex:(NSInteger)labelIndex{ + NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:location]; + NSString *label = [self.dateFormatter stringFromDate:date]; + return [label stringByAppendingString:@" " ]; +} + + +- (NSString *)popUpSuffixForlineGraph:(BEMSimpleLineGraphView *)graph { + return self.popUpSuffix; +} + +- (NSString *)popUpPrefixForlineGraph:(BEMSimpleLineGraphView *)graph { + return self.popUpPrefix; +} + +- (NSString *)popUpTextForlineGraph:(BEMSimpleLineGraphView *)graph atIndex:(NSInteger)index { + if (!self.popUpText) return @"Empty format string"; + @try { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" + return [NSString stringWithFormat: self.popUpText, index]; +#pragma clang diagnostic pop + } @catch (NSException *exception) { + return [NSString stringWithFormat:@"Invalid format string: %@", exception ]; + } + +} + +- (BOOL)lineGraph:(BEMSimpleLineGraphView *)graph alwaysDisplayPopUpAtIndex:(NSInteger)index { + return (index % 3 != 0); +} + +- (CGFloat)maxValueForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.maxValue; +} + +- (CGFloat)minValueForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.minValue; +} + +- (BOOL)noDataLabelEnableForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.noDataLabel; +} +- (NSString *)noDataLabelTextForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.noDataText; +} + +- (CGFloat)staticPaddingForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.staticPaddingValue; +} + +- (UIView *)popUpViewForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.customView; +} + +- (void)lineGraph:(BEMSimpleLineGraphView *)graph modifyPopupView:(UIView *)popupView forIndex:(NSInteger)index { + NSAssert (popupView == self.customView, @"View problem"); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" + if (!self.myGraph.formatStringForValues.length) return; + CGFloat dotValue = [self lineGraph:graph valueForPointAtIndex:index] ; + if (dotValue >= BEMNullGraphValue) return; + self.customViewLabel.text = [NSString stringWithFormat:self.myGraph.formatStringForValues, dotValue]; +#pragma pop +} + +//----- X AXIS -----// + +- (NSInteger)numberOfGapsBetweenLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.numberOfGapsBetweenLabels; +} + +- (NSInteger)baseIndexForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.baseIndexForXAxis; +} + +- (NSInteger)incrementIndexForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.incrementIndexForXAxis; +} + +- (NSArray *)incrementPositionsForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + NSMutableArray * positions = [NSMutableArray array]; + for (NSUInteger index = 0; index < self.arrayOfValues.count; index++ ) { + if (arc4random() % 4 == 0) [positions addObject:@(index)]; + } + return positions; +} + +- (CGFloat)maxXValueForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.maxXValue; +} + +- (CGFloat)minXValueForLineGraph:(BEMSimpleLineGraphView *)graph { + return self.minXValue; +} + + +//----- Y AXIS -----// + +- (NSInteger)numberOfYAxisLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.numberOfYAxisLabels; +} + +- (NSString *)yAxisPrefixOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.yAxisPrefix; +} + +- (NSString *)yAxisSuffixOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.yAxisSuffix; +} + +- (CGFloat)baseValueForYAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.baseValueForYAxis; +} + +- (CGFloat)incrementValueForYAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.incrementValueForYAxis; +} + +#pragma mark Touch handling + +- (void)lineGraph:(BEMSimpleLineGraphView *)graph didTouchGraphWithClosestIndex:(NSInteger)index { + NSNumber * value = self.arrayOfValues[(NSUInteger)index]; + if (value.floatValue < BEMNullGraphValue) { + self.labelValues.text = [self formatNumber:value]; + self.labelDates.text = [NSString stringWithFormat:@"on %@", [self labelForDateAtIndex:index]]; + } +} + +- (void)lineGraph:(BEMSimpleLineGraphView *)graph didReleaseTouchFromGraphWithClosestIndex:(CGFloat)index { + [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.labelValues.alpha = 0.0; + self.labelDates.alpha = 0.0; + } completion:^(BOOL finished) { + [self updateLabelsBelowGraph:graph]; + [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.labelValues.alpha = 1.0; + self.labelDates.alpha = 1.0; + } completion:nil]; + }]; +} +- (BOOL)lineGraph:(BEMSimpleLineGraphView *)graph shouldScaleFrom:(CGFloat)oldScale to:(CGFloat)newScale showingFromXMinValue:(CGFloat)displayMinXValue toXMaxValue:(CGFloat)displayMaxXValue { + // NSLog(@"Scaling %0.2f from %f to %f", newScale, displayMinXValue, displayMaxXValue); + NSTimeInterval displayedRange = 0; + if (self.variableXAxis) { + displayedRange = MAX(displayMaxXValue - displayMinXValue,0); + } else { + NSUInteger minIndex = ceil(displayMinXValue); + NSUInteger maxIndex = ceil(displayMaxXValue); + if (minIndex < self.arrayOfDates.count && maxIndex < self.arrayOfDates.count) { + displayedRange = [self.arrayOfDates[maxIndex] timeIntervalSinceDate:self.arrayOfDates[minIndex]]; + } else { + displayedRange = 0; + } + } + if (displayedRange <= 0) { + //problem, so use default + self.dateFormatter = self.dateFormatterYears; + } else if (displayedRange > 365*24*60*60) { + self.dateFormatter = self.dateFormatterYears; + } else if (displayedRange > 30*24*60*60) { + self.dateFormatter = self.dateFormatterMonths; + } else if (displayedRange > 4*24*60*60) { + self.dateFormatter = self.dateFormatterDays; + } else { // if (displayedRange > 24*60*60) { + self.dateFormatter = self.dateFormatterHours; + } + return YES; +} + +- (void)updateLabelsBelowGraph:(BEMSimpleLineGraphView *)graph { + if (self.arrayOfValues.count > 0) { + NSNumber * sum = [[BEMGraphCalculator sharedCalculator] calculatePointValueSumOnGraph:graph]; + self.labelValues.text =[self formatNumber:sum]; + self.labelDates.text = [NSString stringWithFormat:@"between %@ and %@", [self labelForDateAtIndex:0], [self labelForDateAtIndex:(NSInteger)(self.arrayOfDates.count) - 1]]; + } else { + self.labelValues.text = @"No data"; + self.labelDates.text = @""; + } +} + +- (void)lineGraphDidFinishLoading:(BEMSimpleLineGraphView *)graph { + [self updateLabelsBelowGraph:graph]; +} + +@end diff --git a/Sample Project/TestBed/Info.plist b/Sample Project/TestBed/Info.plist new file mode 100644 index 0000000..3e33804 --- /dev/null +++ b/Sample Project/TestBed/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Sample Project/TestBed/MSColorPicker/MSColorComponentView.h b/Sample Project/TestBed/MSColorPicker/MSColorComponentView.h new file mode 100644 index 0000000..0d5008f --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorComponentView.h @@ -0,0 +1,69 @@ +// +// MSColorComponentView.h +// +// Created by Maksym Shcheglov on 2014-02-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +@class MSSliderView; + +/** + * The view to edit a color component. + */ +@interface MSColorComponentView : UIControl + +/** + * The title. + */ +@property (nonatomic, copy) NSString *title; + +/** + * The current value. The default value is 0.0. + */ +@property (nonatomic, assign) CGFloat value; + +/** + * The minimum value. The default value is 0.0. + */ +@property (nonatomic, assign) CGFloat minimumValue; + +/** + * The maximum value. The default value is 255.0. + */ +@property (nonatomic, assign) CGFloat maximumValue; + +/** + * The format string to use apply for textfield value. \c %.f by default. + */ +@property (nonatomic, copy) NSString *format; + +/** + * Sets the array of CGColorRef objects defining the color of each gradient stop on a slider's track. + * The location of each gradient stop is evaluated with formula: i * width_of_the_track / number_of_colors. + * + * @param colors An array of CGColorRef objects. + */ +- (void)setColors:(NSArray *)colors __attribute__((nonnull(1))); + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorComponentView.m b/Sample Project/TestBed/MSColorPicker/MSColorComponentView.m new file mode 100644 index 0000000..4ef9122 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorComponentView.m @@ -0,0 +1,222 @@ +// +// MSColorComponentView.m +// +// Created by Maksym Shcheglov on 2014-02-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSColorComponentView.h" +#import "MSSliderView.h" + +// Temporary disabled the color component editing via text field +#define COLOR_TEXT_FIELD_ENABLED + +extern CGFloat const MSRGBColorComponentMaxValue; +static CGFloat const MSColorComponentViewSpacing = 5.0f; +static CGFloat const MSColorComponentLabelWidth = 60.0f; +static CGFloat const MSColorComponentTextFieldWidth = 50.0f; + +@interface MSColorComponentView () +{ + @private + + UILabel *_label; + MSSliderView *_slider; // The color slider to edit color component. + UITextField *_textField; +} + +@end + +@implementation MSColorComponentView + ++ (BOOL)requiresConstraintBasedLayout +{ + return YES; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (void)setTitle:(NSString *)title +{ + _label.text = title; +} + +- (void)setMinimumValue:(CGFloat)minimumValue +{ + _slider.minimumValue = minimumValue; +} + +- (void)setMaximumValue:(CGFloat)maximumValue +{ + _slider.maximumValue = maximumValue; +} + +- (void)setValue:(CGFloat)value +{ + _slider.value = value; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" + + _textField.text = [NSString stringWithFormat:_format, value]; +#pragma clang diagnostic pop +} + +- (NSString *)title +{ + return _label.text; +} + +- (CGFloat)minimumValue +{ + return _slider.minimumValue; +} + +- (CGFloat)maximumValue +{ + return _slider.maximumValue; +} + +- (CGFloat)value +{ + return _slider.value; +} + +#pragma mark - UITextFieldDelegate methods + +- (void)textFieldDidEndEditing:(UITextField *)textField +{ + [self setValue:[textField.text floatValue]]; + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + [textField resignFirstResponder]; + return YES; +} + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +{ + NSString *newString = [textField.text stringByReplacingCharactersInRange:range withString:string]; + + //first, check if the new string is numeric only. If not, return NO; + NSCharacterSet *characterSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789,."] invertedSet]; + + if ([newString rangeOfCharacterFromSet:characterSet].location != NSNotFound) { + return NO; + } + + return [newString floatValue] <= _slider.maximumValue; +} + +- (void)setColors:(NSArray *)colors +{ + NSParameterAssert(colors); + [_slider setColors:colors]; +} + +#pragma mark - Private methods + +- (void)ms_baseInit +{ + self.accessibilityLabel = @"color_component_view"; + + _format = @"%.f"; + + _label = [[UILabel alloc] init]; + _label.translatesAutoresizingMaskIntoConstraints = NO; + _label.adjustsFontSizeToFitWidth = YES; + [self addSubview:_label]; + + _slider = [[MSSliderView alloc] init]; + _slider.maximumValue = MSRGBColorComponentMaxValue; + _slider.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_slider]; + +#ifdef COLOR_TEXT_FIELD_ENABLED + _textField = [[UITextField alloc] init]; + _textField.borderStyle = UITextBorderStyleRoundedRect; + _textField.translatesAutoresizingMaskIntoConstraints = NO; + [_textField setKeyboardType:UIKeyboardTypeNumbersAndPunctuation]; + [self addSubview:_textField]; +#endif + + [self setValue:0.0f]; + [_slider addTarget:self action:@selector(ms_didChangeSliderValue:) forControlEvents:UIControlEventValueChanged]; + [_textField setDelegate:self]; + + [self ms_installConstraints]; +} + +- (void)ms_didChangeSliderValue:(MSSliderView *)sender +{ + [self setValue:sender.value]; + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +- (void)ms_installConstraints +{ +#ifdef COLOR_TEXT_FIELD_ENABLED + NSDictionary *views = @{ @"label": _label, @"slider": _slider, @"textField": _textField }; + NSDictionary *metrics = @{ @"spacing": @(MSColorComponentViewSpacing), + @"label_width": @(MSColorComponentLabelWidth), + @"textfield_width": @(MSColorComponentTextFieldWidth) }; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label(label_width)]-spacing-[slider]-spacing-[textField(textfield_width)]|" + options:NSLayoutFormatAlignAllCenterY + metrics:metrics + views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|" options:0 metrics:nil views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[textField]|" options:0 metrics:nil views:views]]; +#else + NSDictionary *views = @{ @"label": _label, @"slider": _slider }; + NSDictionary *metrics = @{ @"spacing": @(MSColorComponentViewSpacing), + @"label_width": @(MSColorComponentLabelWidth) }; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label(label_width)]-spacing-[slider]-spacing-|" + options:NSLayoutFormatAlignAllCenterY + metrics:metrics + views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]|" options:0 metrics:nil views:views]]; + +#endif +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorPicker.h b/Sample Project/TestBed/MSColorPicker/MSColorPicker.h new file mode 100644 index 0000000..5cc8e02 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorPicker.h @@ -0,0 +1,29 @@ +// +// MSColorPicker.h +// +// Created by Maksym Shcheglov on 2015-03-06. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +#import + +#import "MSColorSelectionViewController.h" +#import "MSColorUtils.h" diff --git a/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.h b/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.h new file mode 100644 index 0000000..e1b3830 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.h @@ -0,0 +1,63 @@ +// +// MSColorSelectionView.h +// +// Created by Maksym Shcheglov on 2015-04-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import "MSColorView.h" + +/** + * The enum to define the MSColorView's types. + */ +typedef NS_ENUM(NSUInteger, MSSelectedColorView) { + /** + * The RGB color view type. + */ + MSSelectedColorViewRGB, + /** + * The HSB color view type. + */ + MSSelectedColorViewHSB +}; + +/** + * The MSColorSelectionView aggregates views that should be used to edit color components. + */ +@interface MSColorSelectionView : UIView + +/** + * The selected color view + */ +@property (nonatomic, assign, readonly) MSSelectedColorView selectedIndex; + +/** + * Makes a color component view (rgb or hsb) visible according to the index. + * + * @param index This index define a view to show. + * @param animated If YES, the view is being appeared using an animation. + */ +- (void)setSelectedIndex:(MSSelectedColorView)index animated:(BOOL)animated; + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.m b/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.m new file mode 100644 index 0000000..39f7b72 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorSelectionView.m @@ -0,0 +1,126 @@ +// +// MSColorSelectionView.m +// +// Created by Maksym Shcheglov on 2015-04-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSColorSelectionView.h" +#import "MSRGBView.h" +#import "MSHSBView.h" + +@interface MSColorSelectionView () + +@property (nonatomic, strong) UIView *rgbColorView; +@property (nonatomic, strong) UIView *hsbColorView; +@property (nonatomic, assign) MSSelectedColorView selectedIndex; + +@end + +@implementation MSColorSelectionView + +@synthesize color = _color; +@synthesize delegate; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self != nil) { + [self ms_init]; + } + + return self; +} + +- (id)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + + if (self != nil) { + [self ms_init]; + } + + return self; +} + +- (void)setColor:(UIColor *)color +{ + _color = color; + [[self selectedView] setColor:color]; +} + +- (void)setSelectedIndex:(MSSelectedColorView)index animated:(BOOL)animated +{ + self.selectedIndex = index; + if (self.color) self.selectedView.color = self.color; + [UIView animateWithDuration:animated ? .5 : 0.0 animations:^{ + self.rgbColorView.alpha = index == 0 ? 1.0 : 0.0; + self.hsbColorView.alpha = index == 1 ? 1.0 : 0.0; + } completion:nil]; +} + +- (UIView *)selectedView +{ + return self.selectedIndex == 0 ? self.rgbColorView : self.hsbColorView; +} + +- (void)addColorView:(UIView *)view +{ + view.delegate = self; + [self addSubview:view]; + view.translatesAutoresizingMaskIntoConstraints = NO; + NSDictionary *views = NSDictionaryOfVariableBindings(view); + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:views]]; +} + +- (void)updateConstraints +{ + [self.rgbColorView setNeedsUpdateConstraints]; + [self.hsbColorView setNeedsUpdateConstraints]; + [super updateConstraints]; +} + +#pragma mark - FBColorViewDelegate methods + +- (void)colorView:(id)colorView didChangeColor:(UIColor *)color +{ + self.color = color; + [self.delegate colorView:self didChangeColor:self.color]; +} + +#pragma mark - Private + +- (void)ms_init +{ + self.accessibilityLabel = @"color_selection_view"; + + self.backgroundColor = [UIColor whiteColor]; + self.rgbColorView = [[MSRGBView alloc] init]; + self.hsbColorView = [[MSHSBView alloc] init]; + [self addColorView:self.rgbColorView]; + [self addColorView:self.hsbColorView]; + [self setSelectedIndex:0 animated:NO]; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.h b/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.h new file mode 100644 index 0000000..0f4fe7f --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.h @@ -0,0 +1,64 @@ +// +// MSColorSelectionViewController.h +// +// Created by Maksym Shcheglov on 2015-04-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class MSColorSelectionViewController; + +/** + * The delegate of a MSColorSelectionViewController object must adopt the MSColorSelectionViewController protocol. + * Methods of the protocol allow the delegate to handle color value changes. + */ +@protocol MSColorSelectionViewControllerDelegate + +@required + +/** + * Tells the data source to return the color components. + * + * @param colorViewCntroller The color view. + * @param color The new color value. + */ +- (void)colorViewController:(MSColorSelectionViewController *)colorViewCntroller didChangeColor:(UIColor *)color; + +@end + +@interface MSColorSelectionViewController : UIViewController + +/** + * The controller's delegate. Controller notifies a delegate on color change. + */ +@property (nonatomic, weak, nullable) id delegate; +/** + * The current color value. + */ +@property (nonatomic, strong) UIColor *color; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.m b/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.m new file mode 100644 index 0000000..ad3c345 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorSelectionViewController.m @@ -0,0 +1,92 @@ +// +// MSColorSelectionViewController.m +// +// Created by Maksym Shcheglov on 2015-04-12. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSColorSelectionViewController.h" +#import "MSColorSelectionView.h" + +#import "MSColorPicker.h" + +@interface MSColorSelectionViewController () + +@end + +@implementation MSColorSelectionViewController + +- (void)loadView +{ + MSColorSelectionView *colorSelectionView = [[MSColorSelectionView alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + + self.view = colorSelectionView; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + UISegmentedControl *segmentControl = [[UISegmentedControl alloc] initWithItems:@[NSLocalizedString(@"RGB", ), NSLocalizedString(@"HSB", )]]; + [segmentControl addTarget:self action:@selector(segmentControlDidChangeValue:) forControlEvents:UIControlEventValueChanged]; + segmentControl.selectedSegmentIndex = 0; + self.navigationItem.titleView = segmentControl; + + [self.colorSelectionView setSelectedIndex:0 animated:NO]; + self.colorSelectionView.delegate = self; + self.edgesForExtendedLayout = UIRectEdgeNone; +} + +- (IBAction)segmentControlDidChangeValue:(UISegmentedControl *)segmentedControl +{ + [self.colorSelectionView setSelectedIndex:(NSUInteger)segmentedControl.selectedSegmentIndex animated:YES]; +} + +- (void)setColor:(UIColor *)color +{ + self.colorSelectionView.color = color; +} + +- (UIColor *)color +{ + return self.colorSelectionView.color; +} + +- (void)viewWillLayoutSubviews +{ + [self.colorSelectionView setNeedsUpdateConstraints]; + [self.colorSelectionView updateConstraintsIfNeeded]; +} + +- (MSColorSelectionView *)colorSelectionView +{ + return (MSColorSelectionView *)self.view; +} + +#pragma mark - MSColorViewDelegate + +- (void)colorView:(id)colorView didChangeColor:(UIColor *)color +{ + [self.delegate colorViewController:self didChangeColor:color]; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorUtils.h b/Sample Project/TestBed/MSColorPicker/MSColorUtils.h new file mode 100644 index 0000000..3a6df99 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorUtils.h @@ -0,0 +1,96 @@ +// +// MSColorUtils.h +// +// Created by Maksym Shcheglov on 2014-02-13. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +/** + * The structure to represent a color in the Red-Green-Blue-Alpha color space. + */ +typedef struct { CGFloat red, green, blue, alpha; } RGB; +/** + * The structure to represent a color in the hue-saturation-brightness color space. + */ +typedef struct { CGFloat hue, saturation, brightness, alpha; } HSB; + +/** + * The maximum value of the RGB color components. + */ +extern CGFloat const MSRGBColorComponentMaxValue; +/** + * The maximum value of the alpha component. + */ +extern CGFloat const MSAlphaComponentMaxValue; +/** + * The maximum value of the HSB color components. + */ +extern CGFloat const MSHSBColorComponentMaxValue; + +/** + * Converts an RGB color value to HSV. + * Assumes r, g, and b are contained in the set [0, 1] and + * returns h, s, and b in the set [0, 1]. + * + * @param rgb The rgb color values + * @return The hsb color values + */ +extern HSB MSRGB2HSB(RGB rgb); + +/** + * Converts an HSB color value to RGB. + * Assumes h, s, and b are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + * + * @param hsb The hsb color values + * @return The rgb color values + */ +extern RGB MSHSB2RGB(HSB hsb); + +/** + * Returns the rgb values of the color components. + * + * @param color The color value. + * + * @return The values of the color components (including alpha). + */ +extern RGB MSRGBColorComponents(UIColor *color); + +/** + * Converts hex string to the UIColor representation. + * + * @param color The color value. + * + * @return The hex string color value. + */ +extern NSString * MSHexStringFromColor(UIColor *color); + +/** + * Converts UIColor value to the hex string. + * + * @param hexString The hex string color value. + * + * @return The color value. + */ +extern UIColor * MSColorFromHexString(NSString *hexString); diff --git a/Sample Project/TestBed/MSColorPicker/MSColorUtils.m b/Sample Project/TestBed/MSColorPicker/MSColorUtils.m new file mode 100644 index 0000000..91d09d1 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorUtils.m @@ -0,0 +1,177 @@ +// +// MSColorUtils.m +// +// Created by Maksym Shcheglov on 2014-02-13. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSColorUtils.h" + +CGFloat const MSRGBColorComponentMaxValue = 255.0f; +CGFloat const MSAlphaComponentMaxValue = 100.0f; +CGFloat const MSHSBColorComponentMaxValue = 1.0f; + +extern HSB MSRGB2HSB(RGB rgb) +{ + HSB hsb = { 0.0f, 0.0f, 0.0f, 0.0f }; + double rd = (double)rgb.red; + double gd = (double)rgb.green; + double bd = (double)rgb.blue; + double max = fmax(rd, fmax(gd, bd)); + double min = fmin(rd, fmin(gd, bd)); + double h = 0, s, b = max; + + double d = max - min; + + s = max <= 0 ? 0 : d / max; + + if (max <= min) { + h = 0; // achromatic + } else { + if (max <= rd) { + h = (gd - bd) / d + (gd < bd ? 6 : 0); + } else if (max <= gd) { + h = (bd - rd) / d + 2; + } else if (max <= bd) { + h = (rd - gd) / d + 4; + } + + h /= 6; + } + + hsb.hue = h; + hsb.saturation = s; + hsb.brightness = b; + hsb.alpha = rgb.alpha; + return hsb; +} + +extern RGB MSHSB2RGB(HSB hsb) +{ + RGB rgb = { 0.0f, 0.0f, 0.0f, 0.0f }; + double r, g, b; + + int i = hsb.hue * 6; + double f = hsb.hue * 6 - i; + double p = hsb.brightness * (1 - hsb.saturation); + double q = hsb.brightness * (1 - f * hsb.saturation); + double t = hsb.brightness * (1 - (1 - f) * hsb.saturation); + + switch (i % 6) { + case 0: r = hsb.brightness; g = t; b = p; break; + + case 1: r = q; g = hsb.brightness; b = p; break; + + case 2: r = p; g = hsb.brightness; b = t; break; + + case 3: r = p; g = q; b = hsb.brightness; break; + + case 4: r = t; g = p; b = hsb.brightness; break; + + case 5: + default: + r = hsb.brightness; g = p; b = q; break; + } + + rgb.red = r; + rgb.green = g; + rgb.blue = b; + rgb.alpha = hsb.alpha; + return rgb; +} + +extern RGB MSRGBColorComponents(UIColor *color) +{ + RGB result = {0.0, 0.0, 0.0, 0.0}; + CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)); + + if (colorSpaceModel != kCGColorSpaceModelRGB && colorSpaceModel != kCGColorSpaceModelMonochrome) { + return result; + } + + const CGFloat *components = CGColorGetComponents(color.CGColor); + + if (colorSpaceModel == kCGColorSpaceModelMonochrome) { + result.red = result.green = result.blue = components[0]; + result.alpha = components[1]; + } else { + result.red = components[0]; + result.green = components[1]; + result.blue = components[2]; + result.alpha = components[3]; + } + + return result; +} + +extern NSString * MSHexStringFromColor(UIColor *color) +{ + CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(CGColorGetColorSpace(color.CGColor)); + + if (colorSpaceModel != kCGColorSpaceModelRGB && colorSpaceModel != kCGColorSpaceModelMonochrome) { + return nil; + } + + const CGFloat *components = CGColorGetComponents(color.CGColor); + CGFloat red, green, blue, alpha; + + if (colorSpaceModel == kCGColorSpaceModelMonochrome) { + red = green = blue = components[0]; + alpha = components[1]; + } else { + red = components[0]; + green = components[1]; + blue = components[2]; + alpha = components[3]; + } + + NSString *hexColorString = [NSString stringWithFormat:@"#%02lX%02lX%02lX%02lX", + (unsigned long)(red * MSRGBColorComponentMaxValue), + (unsigned long)(green * MSRGBColorComponentMaxValue), + (unsigned long)(blue * MSRGBColorComponentMaxValue), + (unsigned long)(alpha * MSRGBColorComponentMaxValue)]; + return hexColorString; +} + +extern UIColor * MSColorFromHexString(NSString *hexColor) +{ + if (![hexColor hasPrefix:@"#"]) { + return nil; + } + + NSScanner *scanner = [NSScanner scannerWithString:hexColor]; + [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@"#"]]; + + unsigned hexNum; + + if (![scanner scanHexInt:&hexNum]) return nil; + + int r = (hexNum >> 24) & 0xFF; + int g = (hexNum >> 16) & 0xFF; + int b = (hexNum >> 8) & 0xFF; + int a = (hexNum) & 0xFF; + + return [UIColor colorWithRed:r / MSRGBColorComponentMaxValue + green:g / MSRGBColorComponentMaxValue + blue:b / MSRGBColorComponentMaxValue + alpha:a / MSRGBColorComponentMaxValue]; +} diff --git a/Sample Project/TestBed/MSColorPicker/MSColorView.h b/Sample Project/TestBed/MSColorPicker/MSColorView.h new file mode 100644 index 0000000..14e8692 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorView.h @@ -0,0 +1,65 @@ +// +// MSColorView.h +// +// Created by Maksym Shcheglov on 2014-02-15. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +@protocol MSColorViewDelegate; + +/** + * The \c MSColorView protocol declares a view's interface for displaying and editing color value. + */ +@protocol MSColorView + +@required + +/** + * The object that acts as the delegate of the receiving color selection view. + */ +@property (nonatomic, weak) id delegate; +/** + * The current color. + */ +@property (nonatomic, strong) UIColor* color; + +@end + +/** + * The delegate of a MSColorView object must adopt the MSColorViewDelegate protocol. + * Methods of the protocol allow the delegate to handle color value changes. + */ +@protocol MSColorViewDelegate + +@required + +/** + * Tells the data source to return the color components. + * + * @param colorView The color view. + * @param color The new color value. + */ +- (void)colorView:(id)colorView didChangeColor:(UIColor *)color; + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorWheelView.h b/Sample Project/TestBed/MSColorPicker/MSColorWheelView.h new file mode 100644 index 0000000..2e61117 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorWheelView.h @@ -0,0 +1,45 @@ +// +// MSColorWheelView.h +// +// Created by Maksym Shcheglov on 2014-02-04. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +/** + * The color wheel view. + */ +@interface MSColorWheelView : UIControl + +/** + * The hue value. + */ +@property (nonatomic, assign) CGFloat hue; + +/** + * The saturation value. + */ +@property (nonatomic, assign) CGFloat saturation; + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSColorWheelView.m b/Sample Project/TestBed/MSColorPicker/MSColorWheelView.m new file mode 100644 index 0000000..803be2d --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSColorWheelView.m @@ -0,0 +1,240 @@ +// +// MSColorWheelView.m +// +// Created by Maksym Shcheglov on 2014-02-04. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSColorWheelView.h" +#import "MSColorUtils.h" + +@interface MSColorWheelView () +{ + @private + + CALayer *_indicatorLayer; + CGFloat _hue; + CGFloat _saturation; +} + +@end + +@implementation MSColorWheelView + ++ (BOOL)requiresConstraintBasedLayout +{ + return YES; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + _hue = 0.0f; + _saturation = 0.0f; + + self.accessibilityLabel = @"color_wheel_view"; + + self.layer.delegate = self; + [self.layer addSublayer:[self indicatorLayer]]; + + // [self setSelectedPoint:CGPointMake(dimension / 2, dimension / 2)]; + } + + return self; +} + +- (CALayer *)indicatorLayer +{ + if (!_indicatorLayer) { + CGFloat dimension = 33; + UIColor *edgeColor = [UIColor colorWithWhite:0.9 alpha:0.8]; + _indicatorLayer = [CALayer layer]; + _indicatorLayer.cornerRadius = dimension / 2; + _indicatorLayer.borderColor = edgeColor.CGColor; + _indicatorLayer.borderWidth = 2; + _indicatorLayer.backgroundColor = [UIColor whiteColor].CGColor; + _indicatorLayer.bounds = CGRectMake(0, 0, dimension, dimension); + _indicatorLayer.position = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); + _indicatorLayer.shadowColor = [UIColor blackColor].CGColor; + _indicatorLayer.shadowOffset = CGSizeZero; + _indicatorLayer.shadowRadius = 1; + _indicatorLayer.shadowOpacity = 0.5f; + } + + return _indicatorLayer; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + CGPoint position = [[touches anyObject] locationInView:self]; + + [self onTouchEventWithPosition:position]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + CGPoint position = [[touches anyObject] locationInView:self]; + + [self onTouchEventWithPosition:position]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + CGPoint position = [[touches anyObject] locationInView:self]; + + [self onTouchEventWithPosition:position]; +} + +- (void)onTouchEventWithPosition:(CGPoint)point +{ + CGFloat radius = CGRectGetWidth(self.bounds) / 2; + CGFloat dist = sqrtf((radius - point.x) * (radius - point.x) + (radius - point.y) * (radius - point.y)); + + if (dist <= radius) { + [self ms_colorWheelValueWithPosition:point hue:&_hue saturation:&_saturation]; + [self setSelectedPoint:point]; + [self sendActionsForControlEvents:UIControlEventValueChanged]; + } +} + +- (void)setSelectedPoint:(CGPoint)point +{ + UIColor *selectedColor = [UIColor colorWithHue:_hue saturation:_saturation brightness:1.0f alpha:1.0f]; + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + self.indicatorLayer.position = point; + self.indicatorLayer.backgroundColor = selectedColor.CGColor; + [CATransaction commit]; +} + +- (void)setHue:(CGFloat)hue +{ + _hue = hue; + [self setSelectedPoint:[self ms_selectedPoint]]; + [self setNeedsDisplay]; +} + +- (void)setSaturation:(CGFloat)saturation +{ + _saturation = saturation; + [self setSelectedPoint:[self ms_selectedPoint]]; + [self setNeedsDisplay]; +} + +#pragma mark - CALayerDelegate methods + +- (void)displayLayer:(CALayer *)layer +{ + CGFloat dimension = MIN(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame)); + CFMutableDataRef bitmapData = CFDataCreateMutable(NULL, 0); + + CFDataSetLength(bitmapData, dimension * dimension * 4); + [self ms_colorWheelBitmap:CFDataGetMutableBytePtr(bitmapData) withSize:CGSizeMake(dimension, dimension)]; + id image = [self ms_imageWithRGBAData:bitmapData width:dimension height:dimension]; + CFRelease(bitmapData); + self.layer.contents = image; +} + +- (void)layoutSublayersOfLayer:(CALayer *)layer +{ + if (layer == self.layer) { + [self setSelectedPoint:[self ms_selectedPoint]]; + [self.layer setNeedsDisplay]; + } +} + +#pragma mark - Private methods + +- (CGPoint)ms_selectedPoint +{ + CGFloat dimension = MIN(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame)); + CGFloat radius = _saturation * dimension / 2; + CGFloat x = dimension / 2 + radius * cosf(_hue * M_PI * 2.0f); + CGFloat y = dimension / 2 + radius * sinf(_hue * M_PI * 2.0f); + + return CGPointMake(x, y); +} + +- (void)ms_colorWheelBitmap:(out UInt8 *)bitmap withSize:(CGSize)size +{ + for (NSUInteger y = 0; y < size.width; y++) { + for (NSUInteger x = 0; x < size.height; x++) { + CGFloat hue, saturation, a = 0.0f; + [self ms_colorWheelValueWithPosition:CGPointMake(x, y) hue:&hue saturation:&saturation]; + RGB rgb = { 0.0f, 0.0f, 0.0f, 0.0f }; + + if (saturation < 1.0) { + // Antialias the edge of the circle. + if (saturation > 0.99) { + a = (1.0 - saturation) * 100; + } else { + a = 1.0; + } + + HSB hsb = { hue, saturation, 1.0f, a }; + rgb = MSHSB2RGB(hsb); + } + + NSInteger i = 4 * (x + y * size.width); + bitmap[i] = rgb.red * 0xff; + bitmap[i + 1] = rgb.green * 0xff; + bitmap[i + 2] = rgb.blue * 0xff; + bitmap[i + 3] = rgb.alpha * 0xff; + } + } +} + +- (void)ms_colorWheelValueWithPosition:(CGPoint)position hue:(out CGFloat *)hue saturation:(out CGFloat *)saturation +{ + NSInteger c = CGRectGetWidth(self.bounds) / 2; + CGFloat dx = (float)(position.x - c) / c; + CGFloat dy = (float)(position.y - c) / c; + CGFloat d = sqrtf((float)(dx * dx + dy * dy)); + + *saturation = d; + + if (d <= 0) { + *hue = 0; + } else { + *hue = acosf((float)dx / d) / M_PI / 2.0f; + + if (dy < 0) { + *hue = 1.0 - *hue; + } + } +} + +- (id)ms_imageWithRGBAData:(CFDataRef)data width:(NSUInteger)width height:(NSUInteger)height +{ + CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(data); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGImageRef imageRef = CGImageCreate(width, height, 8, 32, width * 4, colorSpace, (CGBitmapInfo) (kCGBitmapByteOrderDefault | kCGImageAlphaLast), dataProvider, NULL, 0, kCGRenderingIntentDefault); + + CGDataProviderRelease(dataProvider); + CGColorSpaceRelease(colorSpace); + return (__bridge_transfer id)imageRef; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSHSBView.h b/Sample Project/TestBed/MSColorPicker/MSHSBView.h new file mode 100644 index 0000000..ca996db --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSHSBView.h @@ -0,0 +1,36 @@ +// +// MSHSBView.h +// +// Created by Maksym Shcheglov on 2014-02-17. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import "MSColorView.h" + +/** + * The view to edit HSB color components. + */ +@interface MSHSBView : UIView + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSHSBView.m b/Sample Project/TestBed/MSColorPicker/MSHSBView.m new file mode 100644 index 0000000..3578779 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSHSBView.m @@ -0,0 +1,229 @@ +// +// MSHSBView.m +// +// Created by Maksym Shcheglov on 2014-02-17. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSHSBView.h" +#import "MSColorWheelView.h" +#import "MSColorComponentView.h" +#import "MSSliderView.h" +#import "MSColorUtils.h" + +extern CGFloat const MSAlphaComponentMaxValue; +extern CGFloat const MSHSBColorComponentMaxValue; + +static CGFloat const MSColorSampleViewHeight = 30.0f; +static CGFloat const MSViewMargin = 20.0f; +static CGFloat const MSColorWheelDimension = 200.0f; + +@interface MSHSBView () +{ + @private + + MSColorWheelView *_colorWheel; + MSColorComponentView *_brightnessView; + UIView *_colorSample; + + HSB _colorComponents; + NSArray *_layoutConstraints; +} + +@end + +@implementation MSHSBView + +@synthesize delegate = _delegate; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (void)reloadData +{ + [_colorSample setBackgroundColor:self.color]; + [_colorSample setAccessibilityValue:MSHexStringFromColor(self.color)]; + [self ms_reloadViewsWithColorComponents:_colorComponents]; +} + +- (void)setColor:(UIColor *)color +{ + _colorComponents = MSRGB2HSB(MSRGBColorComponents(color)); + [self reloadData]; +} + +- (UIColor *)color +{ + return [UIColor colorWithHue:_colorComponents.hue saturation:_colorComponents.saturation brightness:_colorComponents.brightness alpha:_colorComponents.alpha]; +} + +- (void)updateConstraints +{ + [self ms_updateConstraints]; + [super updateConstraints]; +} + +#pragma mark - Private methods + +- (void)ms_baseInit +{ + self.accessibilityLabel = @"hsb_view"; + + _colorSample = [[UIView alloc] init]; + _colorSample.accessibilityLabel = @"color_sample"; + _colorSample.layer.borderColor = [UIColor blackColor].CGColor; + _colorSample.layer.borderWidth = .5f; + _colorSample.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_colorSample]; + + _colorWheel = [[MSColorWheelView alloc] init]; + _colorWheel.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_colorWheel]; + + _brightnessView = [[MSColorComponentView alloc] init]; + _brightnessView.title = NSLocalizedString(@"Brightness", ); + _brightnessView.maximumValue = MSHSBColorComponentMaxValue; + _brightnessView.format = @"%.2f"; + _brightnessView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_brightnessView]; + + [_colorWheel addTarget:self action:@selector(ms_colorDidChangeValue:) forControlEvents:UIControlEventValueChanged]; + [_brightnessView addTarget:self action:@selector(ms_brightnessDidChangeValue:) forControlEvents:UIControlEventValueChanged]; + + [self setNeedsUpdateConstraints]; +} + +- (void)ms_updateConstraints +{ + // remove all constraints first + if (_layoutConstraints != nil) { + [self removeConstraints:_layoutConstraints]; + } + + _layoutConstraints = self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact ? [self ms_constraintsForCompactVerticalSizeClass] : [self ms_constraintsForRegularVerticalSizeClass]; + [self addConstraints:_layoutConstraints]; +} + +- (NSArray *)ms_constraintsForRegularVerticalSizeClass +{ + NSDictionary *metrics = @{ @"margin": @(MSViewMargin), + @"height": @(MSColorSampleViewHeight), + @"color_wheel_dimension": @(MSColorWheelDimension) }; + + NSDictionary *views = NSDictionaryOfVariableBindings(_colorSample, _colorWheel, _brightnessView); + NSMutableArray *layoutConstraints = [NSMutableArray array]; + + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_colorSample]-margin-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_colorWheel(>=color_wheel_dimension)]-margin-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_brightnessView]-margin-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-margin-[_colorSample(height)]-margin-[_colorWheel]-margin-[_brightnessView]-(>=margin@250)-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObject:[NSLayoutConstraint + constraintWithItem:_colorWheel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:_colorWheel + attribute:NSLayoutAttributeHeight + multiplier:1.0f + constant:0]]; + return layoutConstraints; +} + +- (NSArray *)ms_constraintsForCompactVerticalSizeClass +{ + NSDictionary *metrics = @{ @"margin": @(MSViewMargin), + @"height": @(MSColorSampleViewHeight), + @"color_wheel_dimension": @(MSColorWheelDimension) }; + + NSDictionary *views = NSDictionaryOfVariableBindings(_colorSample, _colorWheel, _brightnessView); + NSMutableArray *layoutConstraints = [NSMutableArray array]; + + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_colorSample]-margin-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_colorWheel(>=color_wheel_dimension)]-margin-[_brightnessView]-(margin@500)-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-margin-[_colorSample(height)]-margin-[_colorWheel]-(margin@500)-|" options:0 metrics:metrics views:views]]; + [layoutConstraints addObject:[NSLayoutConstraint + constraintWithItem:_colorWheel + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:_colorWheel + attribute:NSLayoutAttributeHeight + multiplier:1.0f + constant:0]]; + [layoutConstraints addObject:[NSLayoutConstraint + constraintWithItem:_brightnessView + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self + attribute:NSLayoutAttributeCenterY + multiplier:1.0f + constant:0]]; + return layoutConstraints; +} + +- (void)ms_reloadViewsWithColorComponents:(HSB)colorComponents +{ + _colorWheel.hue = colorComponents.hue; + _colorWheel.saturation = colorComponents.saturation; + [self ms_updateSlidersWithColorComponents:colorComponents]; +} + +- (void)ms_updateSlidersWithColorComponents:(HSB)colorComponents +{ + [_brightnessView setValue:colorComponents.brightness]; + UIColor *tmp = [UIColor colorWithHue:colorComponents.hue saturation:colorComponents.saturation brightness:1.0f alpha:1.0f]; + [_brightnessView setColors:@[(id)[UIColor blackColor].CGColor, (id)tmp.CGColor]]; +} + +- (void)ms_colorDidChangeValue:(MSColorWheelView *)sender +{ + _colorComponents.hue = sender.hue; + _colorComponents.saturation = sender.saturation; + [self.delegate colorView:self didChangeColor:self.color]; + [self reloadData]; +} + +- (void)ms_brightnessDidChangeValue:(MSColorComponentView *)sender +{ + _colorComponents.brightness = sender.value; + [self.delegate colorView:self didChangeColor:self.color]; + [self reloadData]; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSRGBView.h b/Sample Project/TestBed/MSColorPicker/MSRGBView.h new file mode 100644 index 0000000..7ac1d27 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSRGBView.h @@ -0,0 +1,36 @@ +// +// MSRGBView.h +// +// Created by Maksym Shcheglov on 2014-02-16. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import "MSColorView.h" + +/** + * The view to edit RGBA color components. + */ +@interface MSRGBView : UIView + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSRGBView.m b/Sample Project/TestBed/MSColorPicker/MSRGBView.m new file mode 100644 index 0000000..f9e71f9 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSRGBView.m @@ -0,0 +1,225 @@ +// +// MSRGBView.m +// +// Created by Maksym Shcheglov on 2014-02-16. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSRGBView.h" +#import "MSColorComponentView.h" +#import "MSSliderView.h" +#import "MSColorUtils.h" + +extern CGFloat const MSRGBColorComponentMaxValue; + +static CGFloat const MSColorSampleViewHeight = 30.0f; +static CGFloat const MSViewMargin = 20.0f; +static CGFloat const MSSliderViewMargin = 30.0f; +static NSInteger const MSRGBColorComponentsSize = 3; + +@interface MSRGBView () +{ + @private + + UIView *_colorSample; + NSArray *_colorComponentViews; + RGB _colorComponents; +} + +@end + +@implementation MSRGBView + +@synthesize delegate = _delegate; + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super initWithCoder:aDecoder]; + + if (self) { + [self ms_baseInit]; + } + + return self; +} + +- (void)reloadData +{ + [_colorSample setBackgroundColor:self.color]; + [_colorSample setAccessibilityValue:MSHexStringFromColor(self.color)]; + [self ms_reloadColorComponentViews:_colorComponents]; +} + +- (void)setColor:(UIColor *)color +{ + _colorComponents = MSRGBColorComponents(color); + [self reloadData]; +} + +- (UIColor *)color +{ + return [UIColor colorWithRed:_colorComponents.red green:_colorComponents.green blue:_colorComponents.blue alpha:_colorComponents.alpha]; +} + +#pragma mark - Private methods + +- (void)ms_baseInit +{ + self.accessibilityLabel = @"rgb_view"; + + _colorSample = [[UIView alloc] init]; + _colorSample.accessibilityLabel = @"color_sample"; + _colorSample.layer.borderColor = [UIColor blackColor].CGColor; + _colorSample.layer.borderWidth = .5f; + _colorSample.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:_colorSample]; + + NSMutableArray *tmp = [NSMutableArray array]; + NSArray *titles = @[NSLocalizedString(@"Red", ), NSLocalizedString(@"Green", ), NSLocalizedString(@"Blue", )]; + NSArray *maxValues = @[@(MSRGBColorComponentMaxValue), @(MSRGBColorComponentMaxValue), @(MSRGBColorComponentMaxValue)]; + + for (NSUInteger i = 0; i < MSRGBColorComponentsSize; ++i) { + UIControl *colorComponentView = [self ms_colorComponentViewWithTitle:titles[i] tag:(NSInteger)i maxValue:[maxValues[i] floatValue]]; + [self addSubview:colorComponentView]; + [colorComponentView addTarget:self action:@selector(ms_colorComponentDidChangeValue:) forControlEvents:UIControlEventValueChanged]; + [tmp addObject:colorComponentView]; + } + + _colorComponentViews = [tmp copy]; + [self ms_installConstraints]; +} + +- (IBAction)ms_colorComponentDidChangeValue:(MSColorComponentView *)sender +{ + [self ms_setColorComponentValue:sender.value / sender.maximumValue atIndex:sender.tag]; + [self.delegate colorView:self didChangeColor:self.color]; + [self reloadData]; +} + +- (void)ms_setColorComponentValue:(CGFloat)value atIndex:(NSInteger)index +{ + switch (index) { + case 0: + _colorComponents.red = value; + break; + + case 1: + _colorComponents.green = value; + break; + + case 2: + _colorComponents.blue = value; + break; + + default: + _colorComponents.alpha = value; + break; + } +} + +- (UIControl *)ms_colorComponentViewWithTitle:(NSString *)title tag:(NSInteger)tag maxValue:(CGFloat)maxValue +{ + MSColorComponentView *colorComponentView = [[MSColorComponentView alloc] init]; + + colorComponentView.title = title; + colorComponentView.translatesAutoresizingMaskIntoConstraints = NO; + colorComponentView.tag = tag; + colorComponentView.maximumValue = maxValue; + return colorComponentView; +} + +- (void)ms_installConstraints +{ + NSDictionary *metrics = @{ @"margin": @(MSViewMargin), + @"height": @(MSColorSampleViewHeight), + @"slider_margin": @(MSSliderViewMargin) }; + + __block NSDictionary *views = NSDictionaryOfVariableBindings(_colorSample); + + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[_colorSample]-margin-|" options:0 metrics:metrics views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-margin-[_colorSample(height)]" options:0 metrics:metrics views:views]]; + + __block UIView *previousView = _colorSample; + [_colorComponentViews enumerateObjectsUsingBlock:^(UIView *colorComponentView, NSUInteger idx, BOOL *stop) { + views = NSDictionaryOfVariableBindings(previousView, colorComponentView); + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[colorComponentView]-margin-|" + options:0 + metrics:metrics + views:views]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[previousView]-slider_margin-[colorComponentView]" + options:0 + metrics:metrics + views:views]]; + previousView = colorComponentView; + }]; + views = NSDictionaryOfVariableBindings(previousView); + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[previousView]-(>=margin)-|" options:0 metrics:metrics views:views]]; +} + +- (NSArray *)ms_colorComponentsWithRGB:(RGB)rgb +{ + return @[@(rgb.red), @(rgb.green), @(rgb.blue), @(rgb.alpha)]; +} + +- (void)ms_reloadColorComponentViews:(RGB)colorComponents +{ + NSArray *components = [self ms_colorComponentsWithRGB:colorComponents]; + + [_colorComponentViews enumerateObjectsUsingBlock:^(MSColorComponentView *colorComponentView, NSUInteger idx, BOOL *stop) { + [colorComponentView setColors:[self ms_colorsWithColorComponents:components + currentColorIndex:(NSUInteger)colorComponentView.tag]]; + colorComponentView.value = [components[idx] floatValue] * colorComponentView.maximumValue; + }]; +} + +- (NSArray *)ms_colorsWithColorComponents:(NSArray *)colorComponents currentColorIndex:(NSUInteger)colorIndex +{ + CGFloat currentColorValue = [colorComponents[colorIndex] floatValue]; + CGFloat colors[12]; + + for (NSUInteger i = 0; i < MSRGBColorComponentsSize; i++) { + colors[i] = [colorComponents[i] floatValue]; + colors[i + 4] = [colorComponents[i] floatValue]; + colors[i + 8] = [colorComponents[i] floatValue]; + } + + colors[colorIndex] = 0; + colors[colorIndex + 4] = currentColorValue; + colors[colorIndex + 8] = 1.0; + UIColor *start = [UIColor colorWithRed:colors[0] green:colors[1] blue:colors[2] alpha:1.0f]; + UIColor *middle = [UIColor colorWithRed:colors[4] green:colors[5] blue:colors[6] alpha:1.0f]; + UIColor *end = [UIColor colorWithRed:colors[8] green:colors[9] blue:colors[10] alpha:1.0f]; + return @[(id)start.CGColor, (id)middle.CGColor, (id)end.CGColor]; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSSliderView.h b/Sample Project/TestBed/MSColorPicker/MSSliderView.h new file mode 100644 index 0000000..e30f423 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSSliderView.h @@ -0,0 +1,54 @@ +// +// MSSliderView.h +// +// Created by Maksym Shcheglov on 2014-01-31. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +/** + * The slider with a gradient track. + */ +@interface MSSliderView : UIControl + +/** + * The slider's current value. The default value is 0.0. + */ +@property (nonatomic, assign) CGFloat value; +/** + * The minimum value of the slider. The default value is 0.0. + */ +@property (nonatomic, assign) CGFloat minimumValue; +/** + * The maximum value of the slider. The default value is 1.0. + */ +@property (nonatomic, assign) CGFloat maximumValue; +/** + * Sets the array of CGColorRef objects defining the color of each gradient stop on the track. + * The location of each gradient stop is evaluated with formula: i * width_of_the_track / number_of_colors. + * + * @param colors An array of CGColorRef objects. + */ +- (void)setColors:(NSArray *)colors __attribute__((nonnull(1))); + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSSliderView.m b/Sample Project/TestBed/MSColorPicker/MSSliderView.m new file mode 100644 index 0000000..e49a3db --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSSliderView.m @@ -0,0 +1,187 @@ +// +// MSSliderView.m +// +// Created by Maksym Shcheglov on 2014-01-31. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "MSSliderView.h" +#import "MSThumbView.h" +#import "UIControl+HitTestEdgeInsets.h" + +static const CGFloat MSSliderViewHeight = 28.0f; +static const CGFloat MSSliderViewMinWidth = 150.0f; +static const CGFloat MSSliderViewTrackHeight = 3.0f; +static const CGFloat MSThumbViewEdgeInset = -10.0f; + +@interface MSSliderView () { + @private + + MSThumbView *_thumbView; + CAGradientLayer *_trackLayer; +} + +@end + +@implementation MSSliderView + ++ (BOOL)requiresConstraintBasedLayout +{ + return YES; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + self.accessibilityLabel = @"color_slider"; + + _minimumValue = 0.0f; + _maximumValue = 1.0f; + _value = 0.0f; + + self.layer.delegate = self; + + _trackLayer = [CAGradientLayer layer]; + _trackLayer.cornerRadius = MSSliderViewTrackHeight / 2.0f; + _trackLayer.startPoint = CGPointMake(0.0f, 0.5f); + _trackLayer.endPoint = CGPointMake(1.0f, 0.5f); + [self.layer addSublayer:_trackLayer]; + + _thumbView = [[MSThumbView alloc] init]; + _thumbView.hitTestEdgeInsets = UIEdgeInsetsMake(MSThumbViewEdgeInset, MSThumbViewEdgeInset, MSThumbViewEdgeInset, MSThumbViewEdgeInset); + [_thumbView.gestureRecognizer addTarget:self action:@selector(ms_didPanThumbView:)]; + [self addSubview:_thumbView]; + + __attribute__((objc_precise_lifetime)) id color = (__bridge id)[UIColor blueColor].CGColor; + [self setColors:@[color, color]]; + } + + return self; +} + +- (CGSize)intrinsicContentSize +{ + return CGSizeMake(MSSliderViewMinWidth, MSSliderViewHeight); +} + +- (void)setValue:(CGFloat)value +{ + if (value < _minimumValue) { + _value = _minimumValue; + } else if (value > _maximumValue) { + _value = _maximumValue; + } else { + _value = value; + } + + [self ms_updateThumbPositionWithValue:_value]; +} + +- (void)setColors:(NSArray *)colors +{ + NSParameterAssert(colors); + _trackLayer.colors = colors; + [self ms_updateLocations]; +} + +- (void)layoutSubviews +{ + [self ms_updateThumbPositionWithValue:_value]; + [self ms_updateTrackLayer]; +} + +#pragma mark - UIControl touch tracking events + +- (void)ms_didPanThumbView:(UIPanGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer.state != UIGestureRecognizerStateBegan && gestureRecognizer.state != UIGestureRecognizerStateChanged) { + return; + } + + CGPoint translation = [gestureRecognizer translationInView:self]; + [gestureRecognizer setTranslation:CGPointZero inView:self]; + + [self ms_setValueWithTranslation:translation.x]; +} + +- (void)ms_updateTrackLayer +{ + CGFloat height = MSSliderViewHeight; + CGFloat width = CGRectGetWidth(self.bounds); + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _trackLayer.bounds = CGRectMake(0, 0, width, MSSliderViewTrackHeight); + _trackLayer.position = CGPointMake(CGRectGetWidth(self.bounds) / 2, height / 2); + [CATransaction commit]; +} + +#pragma mark - Private methods + +- (void)ms_setValueWithTranslation:(CGFloat)translation +{ + CGFloat width = CGRectGetWidth(self.bounds) - CGRectGetWidth(_thumbView.bounds); + CGFloat valueRange = (_maximumValue - _minimumValue); + CGFloat value = _value + valueRange * translation / width; + + [self setValue:value]; + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +- (void)ms_updateLocations +{ + NSUInteger size = [_trackLayer.colors count]; + + if (size == [_trackLayer.locations count]) { + return; + } + + CGFloat step = 1.0f / (size - 1); + NSMutableArray *locations = [NSMutableArray array]; + [locations addObject:@(0.0f)]; + + for (NSUInteger i = 1; i < size - 1; ++i) { + [locations addObject:@(i * step)]; + } + + [locations addObject:@(1.0f)]; + _trackLayer.locations = [locations copy]; +} + +- (void)ms_updateThumbPositionWithValue:(CGFloat)value +{ + CGFloat thumbWidth = CGRectGetWidth(_thumbView.bounds); + CGFloat thumbHeight = CGRectGetHeight(_thumbView.bounds); + CGFloat width = CGRectGetWidth(self.bounds) - thumbWidth; + + if (width <= 0) { + return; + } + + CGFloat percentage = (value - _minimumValue) / (_maximumValue - _minimumValue); + CGFloat position = width * percentage; + _thumbView.frame = CGRectMake(position, 0, thumbWidth, thumbHeight); +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSThumbView.h b/Sample Project/TestBed/MSColorPicker/MSThumbView.h new file mode 100644 index 0000000..235ceaf --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSThumbView.h @@ -0,0 +1,14 @@ +// +// MSThumbView.h +// +// Created by Maksym Shcheglov on 2016-05-25. +// Copyright (c) 2016 Maksym Shcheglov. +// License: http://opensource.org/licenses/MIT +// + +#import +#import + +@interface MSThumbView : UIControl +@property (nonatomic, strong, readonly) UIGestureRecognizer *gestureRecognizer; +@end diff --git a/Sample Project/TestBed/MSColorPicker/MSThumbView.m b/Sample Project/TestBed/MSColorPicker/MSThumbView.m new file mode 100644 index 0000000..88c05b4 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/MSThumbView.m @@ -0,0 +1,56 @@ +// +// MSThumbView.m +// +// Created by Maksym Shcheglov on 2016-05-25. +// Copyright (c) 2016 Maksym Shcheglov. +// License: http://opensource.org/licenses/MIT +// + +#import "MSThumbView.h" + +static const CGFloat MSSliderViewThumbDimension = 28.0f; + +@interface MSThumbView () +@property (nonatomic, strong) CALayer *thumbLayer; +@property (nonatomic, strong) UIGestureRecognizer *gestureRecognizer; +@end + +@implementation MSThumbView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, MSSliderViewThumbDimension, MSSliderViewThumbDimension)]; + + if (self) { + self.thumbLayer = [CALayer layer]; + + self.thumbLayer.borderColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.4].CGColor; + self.thumbLayer.borderWidth = .5; + self.thumbLayer.cornerRadius = MSSliderViewThumbDimension / 2; + self.thumbLayer.backgroundColor = [UIColor whiteColor].CGColor; + self.thumbLayer.shadowColor = [UIColor blackColor].CGColor; + self.thumbLayer.shadowOffset = CGSizeMake(0.0, 3.0); + self.thumbLayer.shadowRadius = 2; + self.thumbLayer.shadowOpacity = 0.3f; + [self.layer addSublayer:self.thumbLayer]; + self.gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:nil action:nil]; + [self addGestureRecognizer:self.gestureRecognizer]; + } + + return self; +} + +- (void)layoutSublayersOfLayer:(CALayer *)layer +{ + if (layer != self.layer) { + return; + } + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + self.thumbLayer.bounds = CGRectMake(0, 0, MSSliderViewThumbDimension, MSSliderViewThumbDimension); + self.thumbLayer.position = CGPointMake(MSSliderViewThumbDimension / 2, MSSliderViewThumbDimension / 2); + [CATransaction commit]; +} + +@end diff --git a/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.h b/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.h new file mode 100644 index 0000000..f02e571 --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.h @@ -0,0 +1,37 @@ +// +// UIControl+HitTestEdgeInsets.h +// +// Created by Maksym Shcheglov on 18/05/16. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#import + +@interface UIControl (HitTestEdgeInsets) + +/** + * Edge inset values are applied to a view bounds to shrink or expand the touchable area. + */ +@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; + +@end diff --git a/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.m b/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.m new file mode 100644 index 0000000..085c23f --- /dev/null +++ b/Sample Project/TestBed/MSColorPicker/UIControl+HitTestEdgeInsets.m @@ -0,0 +1,65 @@ +// +// UIControl+HitTestEdgeInsets.m +// +// Created by Maksym Shcheglov on 18/05/16. +// +// The MIT License (MIT) +// Copyright (c) 2015 Maksym Shcheglov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + + +#import "UIControl+HitTestEdgeInsets.h" + +#import + +@implementation UIControl (HitTestEdgeInsets) + +- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets +{ + NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)]; + + objc_setAssociatedObject(self, @selector(hitTestEdgeInsets), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (UIEdgeInsets)hitTestEdgeInsets +{ + NSValue *value = objc_getAssociatedObject(self, @selector(hitTestEdgeInsets)); + + if (value) { + UIEdgeInsets edgeInsets; + [value getValue:&edgeInsets]; + return edgeInsets; + } + + return UIEdgeInsetsZero; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden || !self.userInteractionEnabled || self.alpha <= 0) return [super pointInside:point withEvent:event]; + + CGRect relativeFrame = self.bounds; + CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets); + + return CGRectContainsPoint(hitFrame, point); +} + +@end diff --git a/Sample Project/TestBed/MasterViewController.h b/Sample Project/TestBed/MasterViewController.h new file mode 100644 index 0000000..2df18db --- /dev/null +++ b/Sample Project/TestBed/MasterViewController.h @@ -0,0 +1,30 @@ +// +// MasterViewController.h +// TestBed2 +// +// Created by Hugh Mackworth on 5/18/16. +// Copyright © 2016 Boris Emorine. All rights reserved. +// + +@import UIKit; + + + +@class DetailViewController; + +@interface MasterViewController : UITableViewController + +@property (strong, nonatomic) DetailViewController *detailViewController; + + +@end + +@interface CustomTableViewCell : UITableViewCell + +@property (nonatomic) IBOutlet UILabel * title; + +@end + +@implementation CustomTableViewCell + +@end diff --git a/Sample Project/TestBed/MasterViewController.m b/Sample Project/TestBed/MasterViewController.m new file mode 100644 index 0000000..4a2616f --- /dev/null +++ b/Sample Project/TestBed/MasterViewController.m @@ -0,0 +1,1188 @@ +// +// MasterViewController.m +// TestBed2 +// +// Created by Hugh Mackworth on 5/18/16. +// Copyright © 2016 Boris Emorine. All rights reserved. +// + +#import "MasterViewController.h" +#import "DetailViewController.h" +#import "ARFontPickerViewController.h" +#import "MSColorSelectionViewController.h" +#import "NSUserDefaults+Color.h" +#import "BEMGraphCalculator.h" +#import "UITextField+Numbers.h" +#import "UIButton+Switch.m" + +//some convenience extensions for setting and reading + +@interface MasterViewController () + +@property (assign) BEMLineAnimation saveAnimationSetting; +@property (strong, nonatomic) UIColor * saveColorSetting; +@property (strong, nonatomic) NSString * currentColorKey; +@property (strong, nonatomic) UIView * currentColorChip; +@end + +@interface MasterViewController () + +@property (strong, nonatomic) IBOutlet BEMSimpleLineGraphView *myGraph; + +@property (strong, nonatomic) NSDictionary *methodList; +@property (strong, nonatomic) IBOutlet UITextField *numberOfPointsField; + +@property (strong, nonatomic) IBOutlet UITextField *widthLine; +@property (strong, nonatomic) IBOutlet UITextField *staticPaddingField; +@property (strong, nonatomic) IBOutlet UISwitch *bezierSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *interpolateNullValuesSwitch; +@property (strong, nonatomic) IBOutlet UISlider *percentNullSlider; + +@property (strong, nonatomic) IBOutlet UISwitch *xAxisSwitch; +@property (strong, nonatomic) IBOutlet UITextField *numberOfGapsBetweenLabelsField; +@property (strong, nonatomic) IBOutlet UITextField *baseIndexForXAxisField; +@property (strong, nonatomic) IBOutlet UITextField *incrementIndexForXAxisField; +@property (strong, nonatomic) IBOutlet UISwitch *arrayOfIndicesForXAxis; +@property (strong, nonatomic) IBOutlet UISwitch *variableXAxisSwitch; +@property (strong, nonatomic) IBOutlet UITextField *numberofXAxisLabelsField; +@property (strong, nonatomic) IBOutlet UITextField *maxXValueField; +@property (strong, nonatomic) IBOutlet UITextField *minXValueField; + +@property (strong, nonatomic) IBOutlet UISwitch *yAxisSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *yAxisRightSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *xAxisTopSwitch; +@property (strong, nonatomic) IBOutlet UITextField *minValueField; +@property (strong, nonatomic) IBOutlet UITextField *maxValueField; +@property (strong, nonatomic) IBOutlet UITextField *numberofYAxisField; +@property (strong, nonatomic) IBOutlet UITextField *yAxisPrefixField; +@property (strong, nonatomic) IBOutlet UITextField *yAxisSuffixField; +@property (strong, nonatomic) IBOutlet UITextField *baseValueForYAxis; +@property (strong, nonatomic) IBOutlet UITextField *incrementValueForYAxis; + +@property (strong, nonatomic) IBOutlet UISwitch *enableAverageLineSwitch; +@property (strong, nonatomic) IBOutlet UITextField *averageLineTitleField; +@property (strong, nonatomic) IBOutlet UITextField *averageLineWidthField; + +@property (strong, nonatomic) IBOutlet UITextField *widthReferenceLinesField; +@property (strong, nonatomic) IBOutlet UISwitch *xRefLinesSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *yRefLinesSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *enableReferenceAxisSwitch; +@property (strong, nonatomic) IBOutlet CustomTableViewCell *frameReferenceAxesCell; +@property (strong, nonatomic) IBOutlet UIButton *leftFrameButton; +@property (strong, nonatomic) IBOutlet UIButton *rightFrameButton; +@property (strong, nonatomic) IBOutlet UIButton *topFrameButton; +@property (strong, nonatomic) IBOutlet UIButton *bottomFrameButton; + +@property (strong, nonatomic) IBOutlet UISwitch *displayDotsSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *displayDotsOnlySwitch; +@property (strong, nonatomic) IBOutlet UITextField *sizePointField; +@property (strong, nonatomic) IBOutlet UISwitch *displayLabelsSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *popupReportSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *testDisplayPopupCallBack; +@property (strong, nonatomic) IBOutlet UITextField *labelTextFormat; +@property (strong, nonatomic) IBOutlet UITextField *popupLabelPrefix; +@property (strong, nonatomic) IBOutlet UITextField *poupLabelSuffix; +@property (strong, nonatomic) IBOutlet UISwitch *enableCustomViewSwitch; +@property (strong, nonatomic) IBOutlet UITextField *noDataLabelTextField; +@property (strong, nonatomic) IBOutlet UISwitch *enableNoDataLabelSwitch; + +@property (strong, nonatomic) IBOutlet UIButton *animationGraphStyleButton; +@property (strong, nonatomic) IBOutlet UITextField *animationEntranceTime; +@property (strong, nonatomic) IBOutlet UISwitch *dotsWhileAnimateSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *userScalingSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *touchReportSwitch; +@property (strong, nonatomic) IBOutlet UITextField *widthTouchInputLineField; + +@property (strong, nonatomic) IBOutlet UIButton *fontNameButton; +@property (strong, nonatomic) IBOutlet UITextField *fontSizeField; +@property (strong, nonatomic) IBOutlet UITextField *numberFormatField; + +@property (strong, nonatomic) IBOutlet UIView *colorTopChip; +@property (strong, nonatomic) IBOutlet UISwitch *gradientTopSwitch; +@property (strong, nonatomic) IBOutlet UIView *colorBottomChip; +@property (strong, nonatomic) IBOutlet UISwitch *gradientBottomSwitch; +@property (strong, nonatomic) IBOutlet UIView *colorLineChip; +@property (strong, nonatomic) IBOutlet UIView *colorPointChip; +@property (strong, nonatomic) IBOutlet UIView *colorTouchInputLineChip; +@property (strong, nonatomic) IBOutlet UIView *colorXaxisLabelChip; +@property (strong, nonatomic) IBOutlet UIView *colorBackgroundXaxisChip; +@property (strong, nonatomic) IBOutlet UIView *colorYaxisLabelChip; +@property (strong, nonatomic) IBOutlet UIView *colorBackgroundYaxisChip; +@property (strong, nonatomic) IBOutlet UIView *colorBackgroundPopUpLabelChip; +@property (strong, nonatomic) IBOutlet UISwitch *gradientLineSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *gradientHorizSwitch; + +@property (strong, nonatomic) IBOutlet UITextField *alphaTopField; +@property (strong, nonatomic) IBOutlet UITextField *alphaBottomField; +@property (strong, nonatomic) IBOutlet UITextField *alphaLineField; +@property (strong, nonatomic) IBOutlet UITextField *alphaTouchInputLineField; +@property (strong, nonatomic) IBOutlet UITextField *alphaBackgroundXaxisField; +@property (strong, nonatomic) IBOutlet UITextField *alphaBackgroundYaxisField; + +@end + +@implementation MasterViewController + +CGGradientRef static createGradient (void) { + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); + size_t num_locations = 2; + CGFloat locations[2] = { 0.0, 1.0 }; + CGFloat components[8] = { + 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 0.0 + }; + CGGradientRef result = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations); + CGColorSpaceRelease(colorspace); + return result; +} + +static NSDateFormatter *dateFormatter = nil; + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + dateFormatter = [[NSDateFormatter alloc]init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd"]; + self.title = @"Options"; + + self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; + [self.detailViewController loadViewIfNeeded]; + self.myGraph = self.detailViewController.myGraph; + + UIApplication *app = [UIApplication sharedApplication]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResign:) + name:UIApplicationWillResignActiveNotification + object:app]; + [self.detailViewController addObserver:self forKeyPath:@"newestDate" options:NSKeyValueObservingOptionNew context:nil]; + [self restoreProperties]; + [self restoreUI]; + [self.detailViewController addObserver:self forKeyPath:@"numberOfPoints" options:NSKeyValueObservingOptionNew context:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:nil]; + [self showDetailTargetDidChange:self]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:@"newestDate"]) { + [self rangePlaceHolders:self]; + } else if ([keyPath isEqualToString:@"numberOfPoints"]) { + self.numberOfPointsField.intValue = self.detailViewController.numberOfPoints ; + } +} + +- (void)applicationWillResign:(id)sender { + [self saveProperties]; +} + +- (void)restoreProperties { + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; + +#define RestoreProperty(property, type) \ +if ([defaults objectForKey:@#property] != nil) { \ +self.myGraph.property = [defaults type ##ForKey:@#property]; \ +}\ + + //#pragma clang diagnostic push + //#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + + RestoreProperty (animationGraphEntranceTime, float); + RestoreProperty (animationGraphStyle, integer); + + RestoreProperty (colorXaxisLabel, color); + RestoreProperty (colorYaxisLabel, color); + RestoreProperty (colorTop, color); + RestoreProperty (colorLine, color); + RestoreProperty (colorBottom, color); + RestoreProperty (colorPoint, color); + RestoreProperty (colorTouchInputLine, color); + RestoreProperty (colorBackgroundPopUplabel, color); + RestoreProperty (colorBackgroundYaxis, color); + RestoreProperty (colorBackgroundXaxis, color); + RestoreProperty (averageLine.color, color); + + RestoreProperty (alphaTop, float); + RestoreProperty (alphaLine, float); + RestoreProperty (alphaTouchInputLine, float); + RestoreProperty (alphaBackgroundXaxis, float); + RestoreProperty (alphaBackgroundYaxis, float); + + RestoreProperty (widthLine, float); + RestoreProperty (widthReferenceLines, float); + RestoreProperty (sizePoint, float); + RestoreProperty (widthTouchInputLine, float); + RestoreProperty (zoomScale, float); + RestoreProperty (minXDisplayedValue, float); + + RestoreProperty (enableUserScaling, bool); + RestoreProperty (enableTouchReport, bool); + RestoreProperty (enablePopUpReport, bool); + RestoreProperty (enableBezierCurve, bool); + RestoreProperty (enableXAxisLabel, bool); + RestoreProperty (enableYAxisLabel, bool); + RestoreProperty (autoScaleYAxis, bool); + RestoreProperty (positionYAxisRight, bool); + RestoreProperty (positionXAxisTop, bool); + RestoreProperty (alwaysDisplayDots, bool); + RestoreProperty (alwaysDisplayPopUpLabels, bool); + RestoreProperty (enableReferenceAxisFrame, bool); + RestoreProperty (enableLeftReferenceAxisFrameLine, bool); + RestoreProperty (enableBottomReferenceAxisFrameLine, bool); + RestoreProperty (enableTopReferenceAxisFrameLine, bool); + RestoreProperty (enableRightReferenceAxisFrameLine, bool); + RestoreProperty (interpolateNullValues, bool); + RestoreProperty (displayDotsOnly, bool); + RestoreProperty (displayDotsWhileAnimating, bool); + + RestoreProperty (touchReportFingersRequired, integer); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + RestoreProperty (formatStringForValues, object); + + //RestoreProperty (averageLine, object); +#pragma clang diagnostic pop + + NSString * labelFontName = [defaults objectForKey:@"labelFontName"] ; + if (labelFontName) { + CGFloat labelFontSize = [defaults floatForKey:@"labelFontSize"] ; + [self updateFont:labelFontName atSize:labelFontSize]; + } +#define RestoreDetail(property, type) \ +if ([defaults objectForKey:@#property]) { \ +self.detailViewController.property = [defaults type ##ForKey:@#property]; \ +} + + RestoreDetail (numberOfPoints, integer ); + RestoreDetail (percentNulls, float ); + RestoreDetail (popUpText, object); + RestoreDetail (popUpPrefix, object); + RestoreDetail (popUpSuffix, object); + RestoreDetail (testAlwaysDisplayPopup, bool ); + RestoreDetail (maxValue, float ); + RestoreDetail (minValue, float ); + RestoreDetail (maxXValue, float ); + RestoreDetail (minXValue, float ); + RestoreDetail (variableXAxis, bool ); + RestoreDetail (numberofXAxisLabels, integer ); + RestoreDetail (noDataLabel, bool ); + RestoreDetail (noDataText, object); + RestoreDetail (staticPaddingValue, float ); + RestoreDetail (provideCustomView, bool ); + RestoreDetail (numberOfGapsBetweenLabels, integer ); + RestoreDetail (baseIndexForXAxis, integer ); + RestoreDetail (incrementIndexForXAxis, integer ); + RestoreDetail (provideIncrementPositionsForXAxis, bool ); + RestoreDetail (numberOfYAxisLabels, integer ); + RestoreDetail (yAxisPrefix, object); + RestoreDetail (yAxisSuffix, object); + RestoreDetail (baseValueForYAxis, float ); + RestoreDetail (incrementValueForYAxis, float );} + + +- (void)saveProperties{ + + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; +#define EncodeProperty(property, type) [defaults set ## type: self.myGraph.property forKey:@#property] + + EncodeProperty (animationGraphEntranceTime, Float); + EncodeProperty (animationGraphStyle, Integer); + + EncodeProperty (colorXaxisLabel, Color); + EncodeProperty (colorYaxisLabel, Color); + EncodeProperty (colorTop, Color); + EncodeProperty (colorLine, Color); + EncodeProperty (colorBottom, Color); + EncodeProperty (colorPoint, Color); + EncodeProperty (colorTouchInputLine, Color); + EncodeProperty (colorBackgroundPopUplabel, Color); + EncodeProperty (colorBackgroundYaxis, Color); + EncodeProperty (colorBackgroundXaxis, Color); + EncodeProperty (averageLine.color, Color); + + EncodeProperty (alphaTop, Float); + EncodeProperty (alphaLine, Float); + EncodeProperty (alphaTouchInputLine, Float); + EncodeProperty (alphaBackgroundXaxis, Float); + EncodeProperty (alphaBackgroundYaxis, Float); + + EncodeProperty (widthLine, Float); + EncodeProperty (widthReferenceLines, Float); + EncodeProperty (sizePoint, Float); + EncodeProperty (widthTouchInputLine, Float); + EncodeProperty (zoomScale, Float); + EncodeProperty (minXDisplayedValue, Float); + + EncodeProperty (enableUserScaling, Bool); + EncodeProperty (enableTouchReport, Bool); + EncodeProperty (enablePopUpReport, Bool); + EncodeProperty (enableBezierCurve, Bool); + EncodeProperty (enableXAxisLabel, Bool); + EncodeProperty (enableYAxisLabel, Bool); + EncodeProperty (positionYAxisRight, Bool); + EncodeProperty (positionXAxisTop, Bool); + EncodeProperty (autoScaleYAxis, Bool); + EncodeProperty (alwaysDisplayDots, Bool); + EncodeProperty (alwaysDisplayPopUpLabels, Bool); + EncodeProperty (enableReferenceAxisFrame, Bool); + EncodeProperty (enableLeftReferenceAxisFrameLine, Bool); + EncodeProperty (enableBottomReferenceAxisFrameLine, Bool); + EncodeProperty (enableTopReferenceAxisFrameLine, Bool); + EncodeProperty (enableRightReferenceAxisFrameLine, Bool); + EncodeProperty (interpolateNullValues, Bool); + EncodeProperty (displayDotsOnly, Bool); + EncodeProperty (displayDotsWhileAnimating, Bool); + EncodeProperty (touchReportFingersRequired, Integer); + + EncodeProperty (formatStringForValues, Object); + // EncodeProperty (averageLine, Object); + + [defaults setObject:self.myGraph.labelFont.fontName forKey:@"labelFontName"] ; + [defaults setFloat:self.myGraph.labelFont.pointSize forKey:@"labelFontSize"] ; + +#define EncodeDetail(property, type) [defaults set ## type: self.detailViewController.property forKey:@#property] + EncodeDetail (numberOfPoints, Integer ); + EncodeDetail (percentNulls, Float ); + + EncodeDetail (popUpText, Object); + EncodeDetail (popUpPrefix, Object); + EncodeDetail (popUpSuffix, Object); + EncodeDetail (testAlwaysDisplayPopup, Bool ); + EncodeDetail (maxValue, Float ); + EncodeDetail (minValue, Float ); + EncodeDetail (maxXValue, Float ); + EncodeDetail (minXValue, Float ); + EncodeDetail (variableXAxis, Bool ); + EncodeDetail (numberofXAxisLabels, Integer ); + EncodeDetail (noDataLabel, Bool ); + EncodeDetail (noDataText, Object); + EncodeDetail (staticPaddingValue, Float ); + EncodeDetail (provideCustomView, Bool ); + EncodeDetail (numberOfGapsBetweenLabels, Integer ); + EncodeDetail (baseIndexForXAxis, Integer ); + EncodeDetail (incrementIndexForXAxis, Integer ); + EncodeDetail (provideIncrementPositionsForXAxis, Bool ); + EncodeDetail (numberOfYAxisLabels, Integer ); + EncodeDetail (yAxisPrefix, Object); + EncodeDetail (yAxisSuffix, Object); + EncodeDetail (baseValueForYAxis, Float ); + EncodeDetail (incrementValueForYAxis, Float ); +} + + +- (void)restoreUI { + self.numberOfPointsField.intValue = self.detailViewController.numberOfPoints; + self.widthLine.floatValue = self.myGraph.widthLine; + self.staticPaddingField.floatValue = self.detailViewController.staticPaddingValue; + self.bezierSwitch.on = self.myGraph.enableBezierCurve; + self.interpolateNullValuesSwitch.on = self.myGraph.interpolateNullValues; + self.percentNullSlider.value = self.detailViewController.percentNulls; + + self.xAxisSwitch.on = self.myGraph.enableXAxisLabel; + self.xAxisTopSwitch.on = self.myGraph.positionXAxisTop; + self.numberOfGapsBetweenLabelsField.intValue = self.detailViewController.numberOfGapsBetweenLabels; + self.baseIndexForXAxisField.floatValue = self.detailViewController.baseIndexForXAxis; + self.incrementIndexForXAxisField.intValue = self.detailViewController.incrementIndexForXAxis; + self.arrayOfIndicesForXAxis.on = self.detailViewController.provideIncrementPositionsForXAxis; + self.variableXAxisSwitch.on = self.detailViewController.variableXAxis; + self.numberofXAxisLabelsField.intValue = self.detailViewController.numberofXAxisLabels; + self.maxXValueField.floatValue = self.detailViewController.maxXValue; + self.minXValueField.floatValue = self.detailViewController.minXValue; + + self.yAxisSwitch.on = self.myGraph.enableYAxisLabel; + self.yAxisRightSwitch.on = self.myGraph.positionYAxisRight; + self.minValueField.floatValue = self.detailViewController.minValue; + self.maxValueField.floatValue = self.detailViewController.maxValue; + self.numberofYAxisField.intValue = self.detailViewController.numberOfYAxisLabels; + self.yAxisPrefixField.text = self.detailViewController.yAxisPrefix; + self.yAxisSuffixField.text = self.detailViewController.yAxisSuffix; + self.baseValueForYAxis.floatValue = self.detailViewController.baseValueForYAxis; + self.incrementValueForYAxis.floatValue = self.detailViewController.incrementValueForYAxis; + + self.enableAverageLineSwitch.on = self.myGraph.averageLine.enableAverageLine; + self.averageLineTitleField.text = self.myGraph.averageLine.title; + self.averageLineWidthField.floatValue = self.myGraph.averageLine.width; + + self.widthReferenceLinesField.floatValue = self.myGraph.widthReferenceLines; + self.xRefLinesSwitch.on = self.myGraph.enableReferenceXAxisLines; + self.yRefLinesSwitch.on = self.myGraph.enableReferenceYAxisLines; + self.enableReferenceAxisSwitch.on = self.myGraph.enableReferenceAxisFrame; + [self updateReferenceAxisFrame:self.myGraph.enableReferenceAxisFrame]; + self.leftFrameButton.on = self.myGraph.enableLeftReferenceAxisFrameLine; + self.rightFrameButton.on = self.myGraph.enableRightReferenceAxisFrameLine; + self.topFrameButton.on = self.myGraph.enableTopReferenceAxisFrameLine; + self.bottomFrameButton.on = self.myGraph.enableBottomReferenceAxisFrameLine; + + self.displayDotsSwitch.on = self.myGraph.alwaysDisplayDots; + self.displayDotsOnlySwitch.on = self.myGraph.displayDotsOnly; + self.sizePointField.floatValue = self.myGraph.sizePoint; + self.popupReportSwitch.on = self.myGraph.enablePopUpReport; + self.displayLabelsSwitch.on = self.myGraph.alwaysDisplayPopUpLabels; + self.testDisplayPopupCallBack.on = self.detailViewController.testAlwaysDisplayPopup; + self.labelTextFormat.text = self.detailViewController.popUpText; + self.poupLabelSuffix.text = self.detailViewController.popUpSuffix; + self.popupLabelPrefix.text = self.detailViewController.popUpPrefix; + self.enableCustomViewSwitch.on = self.detailViewController.provideCustomView; + self.enableNoDataLabelSwitch.on = self.detailViewController.noDataLabel; + self.noDataLabelTextField.text = self.detailViewController.noDataText; + + [self updateAnimationGraphStyle]; + self.animationEntranceTime.floatValue = self.myGraph.animationGraphEntranceTime; + self.dotsWhileAnimateSwitch.on = self.myGraph.displayDotsWhileAnimating; + self.touchReportSwitch.on = self.myGraph.enableTouchReport; + self.userScalingSwitch.on = self.myGraph.enableUserScaling; + self.widthTouchInputLineField.floatValue = self.myGraph.widthTouchInputLine; + + self.fontSizeField.floatValue = self.myGraph.labelFont.pointSize; + self.numberFormatField.text = self.myGraph.formatStringForValues; + + + self.colorTopChip.backgroundColor = self.myGraph.colorTop; + self.colorBottomChip.backgroundColor = self.myGraph.colorBottom; + self.gradientTopSwitch.on = self.myGraph.gradientTop != nil; + self.gradientBottomSwitch.on = self.myGraph.gradientBottom != nil; + self.gradientHorizSwitch.on = self.myGraph.gradientLineDirection == BEMLineGradientDirectionHorizontal; + self.gradientLineSwitch.on = self.myGraph.gradientLine != nil; + + self.colorLineChip.backgroundColor = self.myGraph.colorLine; + self.colorPointChip.backgroundColor = self.myGraph.colorPoint; + self.colorXaxisLabelChip.backgroundColor = self.myGraph.colorXaxisLabel; + self.colorBackgroundXaxisChip.backgroundColor = self.myGraph.colorBackgroundXaxis ?: self.myGraph.colorBottom; + self.colorTouchInputLineChip.backgroundColor = self.myGraph.colorTouchInputLine; + self.colorYaxisLabelChip.backgroundColor = self.myGraph.colorYaxisLabel; + self.colorBackgroundYaxisChip.backgroundColor = self.myGraph.colorBackgroundYaxis ?: self.myGraph.colorTop; + self.colorBackgroundPopUpLabelChip.backgroundColor = self.myGraph.colorBackgroundPopUplabel; + + self.alphaTopField.floatValue= self.myGraph.alphaTop; + self.alphaBottomField.floatValue = self.myGraph.alphaBottom; + self.alphaLineField.floatValue = self.myGraph.alphaLine; + self.alphaTouchInputLineField.floatValue = self.myGraph.alphaTouchInputLine; + self.alphaBackgroundXaxisField.floatValue = self.myGraph.alphaBackgroundXaxis; + self.alphaBackgroundYaxisField.floatValue = self.myGraph.alphaBackgroundYaxis; + + +} +- (void)rangePlaceHolders:(id)sender { + //give user a hint on what range is ok for min/max + self.maxValueField.placeholder = [NSString stringWithFormat:@"%0.2f",self.detailViewController.biggestValue ]; + self.minValueField.placeholder = [NSString stringWithFormat:@"%0.2f",self.detailViewController.smallestValue ]; + if (self.variableXAxisSwitch.isOn) { + self.minXValueField.placeholder = [dateFormatter stringFromDate: self.detailViewController.oldestDate ]; + self.maxXValueField.placeholder = [dateFormatter stringFromDate: self.detailViewController.newestDate ]; + } else { + self.minXValueField.placeholder = @"0"; + self.maxXValueField.placeholder = [NSString stringWithFormat:@"%ld",(long)self.detailViewController.numberOfPoints ]; + } + +} +/* properties/methods not implemented: + touchReportFingersRequired, + autoScaleYAxis + Dashpatterns for averageLine, XAxis, Yaxis + Gradient choices + */ + +- (IBAction)numberOfPointsDidChange:(UITextField *)sender { + NSInteger value = sender.intValue; + if (value >= 0 ) { + self.detailViewController.numberOfPoints = value; + } +} + +#pragma mark Main Line +- (IBAction)widthLineDidChange:(UITextField *)sender { + float value = sender.floatValue; + if (value > 0.0f) { + self.myGraph.widthLine = value; + } + [self.myGraph reloadGraph]; +} + +- (IBAction)staticPaddingDidChange:(UITextField *)sender { + self.detailViewController.staticPaddingValue = sender.floatValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableBezier:(UISwitch *)sender { + self.myGraph.enableBezierCurve = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)interpolateNullValues:(UISwitch *)sender { + self.myGraph.interpolateNullValues = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)percentNullSliderChanged:(UISlider *)sender { + self.detailViewController.percentNulls = sender.value; + [self.myGraph reloadGraph]; +} + +#pragma mark Axes and Reference Lines + +- (IBAction)enableXAxisLabel:(UISwitch *)sender { + self.myGraph.enableXAxisLabel = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberOfGapsBetweenLabelDidChange:(UITextField *)sender { + if (sender.intValue >= 0) { + self.detailViewController.numberOfGapsBetweenLabels = sender.intValue; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)baseIndexForXAxisDidChange:(UITextField *)sender { + if (sender.intValue >= 0) { + self.detailViewController.baseIndexForXAxis = sender.intValue; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)incrementIndexForXAxisDidChange:(UITextField *)sender { + if (sender.intValue >= 0) { + self.detailViewController.incrementIndexForXAxis = sender.intValue; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)enableArrayOfIndicesForXAxis:(UISwitch *)sender { + self.detailViewController.provideIncrementPositionsForXAxis = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)variableXAxis:(UISwitch *)sender { + self.detailViewController.variableXAxis = sender.on; + self.maxXValueField.text = @""; + self.minXValueField.text = @""; + self.detailViewController.maxXValue = -1; + self.detailViewController.minXValue = -1; + + [self rangePlaceHolders:self]; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberofXAxisDidChange:(UITextField *)sender { + if (sender.intValue >= 0) { + self.detailViewController.numberofXAxisLabels = sender.intValue; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)minXValueDidChange:(UITextField *)sender { + if (self.variableXAxisSwitch.on) { + //expect date format + NSTimeInterval interval = [self timeIntervalFromString:sender.text]; + if (interval > 0) { + self.detailViewController.minXValue = interval; + } else { + self.detailViewController.minXValue = -1; + } + } else { + self.detailViewController.minXValue = sender.floatValue; + } + [self.myGraph reloadGraph]; +} + +- (NSTimeInterval)timeIntervalFromString:(NSString *)string { + NSDate *date = [dateFormatter dateFromString:string ]; + return (date) ? [date timeIntervalSinceReferenceDate] : 0; + } + +- (IBAction)maxXValueDidChange:(UITextField *)sender { + if (self.variableXAxisSwitch.isOn) { + //expect date format + NSTimeInterval interval = [self timeIntervalFromString:sender.text]; + if (interval > 0) { + self.detailViewController.maxXValue = interval; + } else { + self.detailViewController.maxValue = -1; + } + } else { + self.detailViewController.maxXValue = sender.floatValue; + } + [self.myGraph reloadGraph]; +} + +#pragma mark Axes and Reference Lines + +- (IBAction)enableYAxisLabel:(UISwitch *)sender { + self.myGraph.enableYAxisLabel = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)positionYAxisRight:(UISwitch *)sender { + self.myGraph.positionYAxisRight = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)positionXAxisTop:(UISwitch *)sender { + self.myGraph.positionXAxisTop = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)minValueDidChange:(UITextField *)sender { + self.detailViewController.minValue = sender.floatValue; + [self.myGraph reloadGraph]; + +} + +- (IBAction)maxValueDidChange:(UITextField *)sender { + self.detailViewController.maxValue = sender.floatValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberofYAxisDidChange:(UITextField *)sender { + if (sender.intValue >= 0) { + self.detailViewController.numberOfYAxisLabels = sender.intValue; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)yAxisPrefixDidChange:(UITextField *)sender { + self.detailViewController.yAxisPrefix = sender.text; + [self.myGraph reloadGraph]; +} + +- (IBAction)yAxisSuffixDidChange:(UITextField *)sender { + self.detailViewController.yAxisSuffix = sender.text; + [self.myGraph reloadGraph]; +} +- (IBAction)baseValueForYAxisDidChange:(UITextField *)sender { + self.detailViewController.baseValueForYAxis = sender.floatValue; + [self.myGraph reloadGraph]; + +} +- (IBAction)incrementValueForYAxisDidChange:(UITextField *)sender { + self.detailViewController.incrementValueForYAxis = sender.floatValue; + [self.myGraph reloadGraph]; +} + +#pragma mark Average Line +- (IBAction)enableAverageLine:(UISwitch *)sender { + self.myGraph.averageLine.enableAverageLine = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)averageLineTitleDidChange:(UITextField *)sender { + self.myGraph.averageLine.title = sender.text; + [self.myGraph reloadGraph]; +} + +- (IBAction)averageLineWidthDidChange:(UITextField *)sender { + if (sender.text.floatValue <= 0) { + sender.text = @"1.0"; + } + self.myGraph.averageLine.width = sender.text.doubleValue; + [self.myGraph reloadGraph]; +} + +#pragma mark Reference Lines + +- (IBAction)widthReferenceLines:(UITextField *)sender { + if (sender.text.floatValue <= 0) { + sender.text = @"1.0"; + } + self.myGraph.widthReferenceLines = (CGFloat) sender.text.doubleValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableReferenceXAxisLines:(UISwitch *)sender { + self.myGraph.enableReferenceXAxisLines = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableReferenceYAxisLines:(UISwitch *)sender { + self.myGraph.enableReferenceYAxisLines = sender.on; + [self.myGraph reloadGraph]; +} + +- (void)updateReferenceAxisFrame:(BOOL)newState { + self.myGraph.enableReferenceAxisFrame = newState; + self.frameReferenceAxesCell.alpha = newState ? 1.0 : 0.5 ; + self.frameReferenceAxesCell.userInteractionEnabled = newState; +} + +- (IBAction)enableReferenceAxisFrame:(UISwitch *)sender { + [self updateReferenceAxisFrame:sender.on]; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableLeftReferenceAxisFrameLine:(UIButton *)button { + BOOL newState = button.on; + self.myGraph.enableLeftReferenceAxisFrameLine = newState; + button.on = newState; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableRightReferenceAxisFrameLine:(UIButton *)button { + BOOL newState = button.on; + self.myGraph.enableRightReferenceAxisFrameLine = newState; + button.on = newState; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableTopReferenceAxisFrameLine:(UIButton *)button { + BOOL newState = button.on; + self.myGraph.enableTopReferenceAxisFrameLine = newState; + button.on = newState; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableBottomReferenceAxisFrameLine:(UIButton *)button { + BOOL newState = button.on; + self.myGraph.enableBottomReferenceAxisFrameLine = newState; + button.on = newState; + [self.myGraph reloadGraph]; +} + +#pragma mark Dots & Labels section + +- (IBAction)alwaysDisplayDots:(UISwitch *)sender { + self.myGraph.alwaysDisplayDots = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)displayDotsOnly:(UISwitch *)sender { + self.myGraph.displayDotsOnly = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)sizePointDidChange:(UITextField *)sender { + if (sender.text.floatValue <= 0) { + sender.text = @"1.0"; + } + self.myGraph.sizePoint = (CGFloat) sender.text.floatValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)enablePopUpReport:(UISwitch *)sender { + self.myGraph.enablePopUpReport = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)alwaysDisplayPopUpLabels:(UISwitch *)sender { + self.myGraph.alwaysDisplayPopUpLabels = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableTestDisplayPopups:(UISwitch *)sender { + self.detailViewController.testAlwaysDisplayPopup = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)labelTextDidChange:(UITextField *)sender { + self.detailViewController.popUpText = [self checkUsersFormatString:sender]; + [self.myGraph reloadGraph]; +} + +- (IBAction)labelPrefixDidChange:(UITextField *)sender { + self.detailViewController.popUpPrefix = sender.text; + [self.myGraph reloadGraph]; +} + +- (IBAction)labelSuffixDidChange:(UITextField *)sender { + self.detailViewController.popUpSuffix = sender.text; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableCustomView:(UISwitch *)sender { + self.detailViewController.provideCustomView = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableNoDataLabel:(UISwitch *)sender { + self.detailViewController.noDataLabel = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)noDataLabelTextDidChange:(UITextField *)sender { + self.detailViewController.noDataText = sender.text; + [self.myGraph reloadGraph]; +} + +#pragma mark Animation section +// +//typedef NS_ENUM(NSInteger, BEMLineAnimation) { +// /// The draw animation draws the lines from left to right and bottom to top. +// BEMLineAnimationDraw, +// /// The fade animation fades in the lines from 0% opaque to 100% opaque (based on the \p lineAlpha property). +// BEMLineAnimationFade, +// /// The expand animation expands the lines from a small point to their full width (based on the \p lineWidth property). +// BEMLineAnimationExpand, +// /// No animation is used to display the graph +// BEMLineAnimationNone +//}; +// +- (void)updateAnimationGraphStyle { + NSString * newTitle = @""; + switch (self.myGraph.animationGraphStyle) { + case BEMLineAnimationDraw: + newTitle = @"Draw"; + break; + case BEMLineAnimationFade: + newTitle = @"Fade"; + break; + case BEMLineAnimationExpand: + newTitle = @"Expand"; + break; + case BEMLineAnimationNone: + newTitle = @"None"; + break; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcovered-switch-default" + default: + newTitle = @"N/A"; + break; +#pragma clang diagnostic pop + } + [self.animationGraphStyleButton setTitle:newTitle forState:UIControlStateNormal]; +} + +- (IBAction)animationGraphStyle:(UIButton *)sender { + if (self.myGraph.animationGraphStyle == BEMLineAnimationNone) { + self.myGraph.animationGraphStyle = BEMLineAnimationDraw; + self.myGraph.animationGraphEntranceTime = self.animationEntranceTime.floatValue; + } else { + self.myGraph.animationGraphStyle ++; + } + [self updateAnimationGraphStyle]; + [self.myGraph reloadGraph]; +} + +- (IBAction)animationGraphEntranceTimeDidChange:(UITextField *)sender { + self.myGraph.animationGraphEntranceTime = (CGFloat) sender.floatValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)displayDotsWhileAnimating:(UISwitch *)sender { + self.myGraph.displayDotsWhileAnimating = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableUserScaling:(UISwitch *)sender { + self.myGraph.enableUserScaling = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableTouchReport:(UISwitch *)sender { + self.myGraph.enableTouchReport = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)widthTouchInputLineDidChange:(UITextField *)sender { + if (sender.text.floatValue <= 0) { + sender.text = @"1.0"; + } + self.myGraph.widthTouchInputLine = (CGFloat) sender.text.floatValue; + [self.myGraph reloadGraph]; +} + + +#pragma mark TextFormatting + +- (IBAction)fontFamily:(UIButton *)sender { + // done in IB: [self performSegueWithIdentifier:@"FontPicker" sender:self]; +} + +- (void)updateFont:(NSString *)fontName atSize:(CGFloat)fontSize { + if (!fontName) fontName = self.fontNameButton.titleLabel.text; + if (fontSize <= 0) fontSize = (CGFloat)self.fontSizeField.text.floatValue; + if (fontSize < 1.0) fontSize = 14.0; + UIFont * newFont = nil; + if ([@"System" isEqualToString:fontName]) { + newFont = [UIFont systemFontOfSize:fontSize]; + } else { + newFont = [UIFont fontWithName:fontName size:fontSize]; + } + if (newFont) { + self.myGraph.labelFont = newFont; + self.fontNameButton.titleLabel.font = newFont; + [self.fontNameButton setTitle:fontName forState:UIControlStateNormal]; + } +} + +- (void)fontPickerViewController:(ARFontPickerViewController *)fontPicker didSelectFont:(NSString *)fontName { + [fontPicker dismissViewControllerAnimated:YES completion:nil]; + self.fontNameButton.enabled = NO; + [self.fontNameButton setTitle:fontName forState:UIControlStateNormal]; + self.fontNameButton.enabled = YES; + [self updateFont: fontName atSize:0.0]; + [self.myGraph reloadGraph]; +} + +- (IBAction)fontSizeFieldChanged:(UITextField *)sender { + [self updateFont: nil atSize: self.fontSizeField.text.floatValue]; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberFormatChanged:(UITextField *)sender { + self.myGraph.formatStringForValues = [self checkUsersFormatString:sender]; + [self.myGraph reloadGraph]; +} +- (NSString *)checkUsersFormatString:(UITextField *)sender { + //there are many ways to crash this (more than one format), but this is most obvious + NSString * newFormat = sender.text ?: @""; + if ([newFormat containsString:@"%@"]) { + //prevent crash + NSLog(@"%%@ not allowed in numeric format strings"); + newFormat = [newFormat stringByReplacingOccurrencesOfString:@"%@" withString:@"%%@"]; + sender.text = newFormat; + } + return newFormat; +} + +- (IBAction)alphaTopFieldChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaTop = newAlpha; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)alphaBottomFieldChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaBottom = newAlpha; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)alphaLineFieldChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaLine = newAlpha; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)alphaTouchInputFieldChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaTouchInputLine = newAlpha; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)alphaBackgroundXaxisChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaBackgroundXaxis = newAlpha; + [self.myGraph reloadGraph]; + } +} + +- (IBAction)alphaBackgroundYaxisChanged:(UITextField *)sender { + float newAlpha = sender.floatValue; + if (newAlpha >= 0 && newAlpha <= 1.0) { + self.myGraph.alphaBackgroundYaxis = newAlpha; + [self.myGraph reloadGraph]; + } +} + +#pragma Color section +- (void)didChangeColor:(UIColor *)color { + if (![color isEqual:self.currentColorChip.backgroundColor]) { + self.currentColorChip.backgroundColor = color; + [self.myGraph setValue: color forKey: self.currentColorKey]; + [self.myGraph reloadGraph]; + } + +} +- (void)colorViewController:(MSColorSelectionViewController *)colorViewCntroller didChangeColor:(UIColor *)color { + [self didChangeColor:color]; +} + +- (void)saveColor:(id)sender { + self.myGraph.animationGraphStyle = self.saveAnimationSetting; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)restoreColor:(id)sender { + if (self.saveColorSetting) { + [self didChangeColor:self.saveColorSetting]; + } else { + [self.myGraph setValue: nil forKey: self.currentColorKey]; + + if ([self.currentColorKey isEqualToString:@"colorBackgroundYaxis"]) { + self.currentColorChip.backgroundColor = self.myGraph.colorTop; + } else if ([self.currentColorKey isEqualToString:@"colorBackgroundXaxis"]) { + self.currentColorChip.backgroundColor = self.myGraph.colorBottom; + } + + } + self.myGraph.animationGraphStyle = self.saveAnimationSetting; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)popoverPresentationControllerDidDismissPopover:(UIPopoverPresentationController *)popoverPresentationController { + [self restoreColor:nil]; +} + +- (IBAction)enableGradientTop:(UISwitch *)sender { + if (sender.on) { + CGGradientRef gradient = createGradient(); + self.myGraph.gradientTop = gradient; + CGGradientRelease(gradient); + } else { + self.myGraph.gradientTop = nil; + } + + [self.myGraph reloadGraph]; +} + +- (IBAction)enableGradientBottom:(UISwitch *)sender { + if (sender.on) { + CGGradientRef gradient = createGradient(); + self.myGraph.gradientBottom = gradient; + CGGradientRelease(gradient); + } else { + self.myGraph.gradientBottom = nil; + } + + [self.myGraph reloadGraph]; +} + +- (IBAction)enableGradientLine:(UISwitch *)sender { + if (sender.on) { + CGGradientRef gradient = createGradient(); + self.myGraph.gradientLine = gradient; + CGGradientRelease(gradient); + } else { + self.myGraph.gradientLine = nil; + } + + [self.myGraph reloadGraph]; +} + +- (IBAction)enableGradientHoriz:(UISwitch *)sender { + self.myGraph.gradientLineDirection = sender.on ? BEMLineGradientDirectionVertical : BEMLineGradientDirectionHorizontal; + [self.myGraph reloadGraph]; +} + +#pragma mark - Segues + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + if ([[segue identifier] isEqualToString:@"showDetail"]) { + UINavigationController* navigationController = (UINavigationController*)[segue destinationViewController]; + navigationController.viewControllers = @[self.detailViewController]; + self.detailViewController.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + self.detailViewController.navigationItem.leftItemsSupplementBackButton = YES; + } else if ([[segue identifier] isEqualToString:@"FontPicker"]) { + ARFontPickerViewController * controller = (ARFontPickerViewController*) [segue destinationViewController]; + controller.delegate = self; + } else if ([segue.identifier hasPrefix:@"color"]) { + + //set up color selector + UINavigationController *destNav = segue.destinationViewController; + destNav.popoverPresentationController.delegate = self; +// CGRect cellFrame = [self.view convertRect:((UIView *)sender).bounds fromView:sender]; + destNav.popoverPresentationController.sourceView = ((UIView *)sender) ; + destNav.popoverPresentationController.sourceRect = ((UIView *)sender).bounds ; + destNav.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionAny; + destNav.preferredContentSize = [[destNav visibleViewController].view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; + MSColorSelectionViewController *colorSelectionController = (MSColorSelectionViewController *)destNav.visibleViewController; + colorSelectionController.delegate = self; + + UIBarButtonItem *doneBtn = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Save", ) style:UIBarButtonItemStyleDone target:self action:@selector(saveColor:)]; + colorSelectionController.navigationItem.rightBarButtonItem = doneBtn; + UIBarButtonItem *cancelBtn = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Cancel", ) style:UIBarButtonItemStyleDone target:self action:@selector(restoreColor:)]; + colorSelectionController.navigationItem.leftBarButtonItem = cancelBtn; + + + //remember stuff from sender tableviewCell + if ([sender isKindOfClass:[UITableViewCell class]]) { + NSArray * subViews = [[(UITableViewCell *) sender contentView] subviews]; + for (UIView * subView in subViews) { + if (subView.tag == 12343) { + self.currentColorChip = subView; + break; + } + } + } + self.currentColorKey = segue.identifier; + + NSAssert(self.currentColorKey != nil && self.currentColorChip != nil, @"View Structural problem"); + + UIColor * oldColor = (UIColor *) [self.myGraph valueForKey:self.currentColorKey]; + if (!oldColor) { + //value is not currently set; handle special cases that default to others + if ([self.currentColorKey isEqualToString:@"colorBackgroundYaxis"]) { + oldColor = self.myGraph.colorTop; + self.myGraph.colorBackgroundYaxis = oldColor; + } else if ([self.currentColorKey isEqualToString:@"colorBackgroundXaxis"]) { + oldColor = self.myGraph.colorBottom; + self.myGraph.colorBackgroundXaxis = oldColor; + } else { + oldColor = [UIColor blueColor]; //shouldn't happen + [self didChangeColor:oldColor]; + } + self.currentColorChip.backgroundColor = oldColor; + } + self.saveColorSetting = oldColor; + self.saveAnimationSetting = self.myGraph.animationGraphStyle; + self.myGraph.animationGraphStyle = BEMLineAnimationNone; + + colorSelectionController.color = oldColor; + } + + +} +#pragma mark - Table View + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + //static sections + return [super numberOfSectionsInTableView:tableView]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + //static cells + return [super tableView:tableView numberOfRowsInSection:section]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + //static cells + return [super tableView:tableView cellForRowAtIndexPath:indexPath]; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + // Return NO if you do not want the specified item to be editable. + return NO; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + if (self.splitViewController.isCollapsed) { + [self performSegueWithIdentifier:@"showDetail" sender:self]; + } +} + +#pragma mark TextDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + return YES; +} + +#pragma mark Detail did change +- (void)showDetailTargetDidChange:(id)sender { + if (self.splitViewController.isCollapsed) { + if (!self.navigationItem.rightBarButtonItem) { + UIBarButtonItem *graphBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Graph" style:UIBarButtonItemStylePlain target:self action:@selector(showDetail:)]; + self.navigationItem.rightBarButtonItem = graphBarButton; + } + } else { + self.navigationItem.rightBarButtonItem = nil; + } +} + +-(void) showDetail:(id) sender { + [self performSegueWithIdentifier:@"showDetail" sender:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self.detailViewController removeObserver:self forKeyPath:@"newestDate" ]; + [self.detailViewController removeObserver:self forKeyPath:@"numberOfPoints" ]; + +} +@end + diff --git a/Sample Project/TestBed/NSUserDefaults+Color.h b/Sample Project/TestBed/NSUserDefaults+Color.h new file mode 100644 index 0000000..5088b66 --- /dev/null +++ b/Sample Project/TestBed/NSUserDefaults+Color.h @@ -0,0 +1,16 @@ +// +// NSUserDefaults+NSUserDefaults_Color.h +// SimpleLineChart +// +// Created by Hugh Mackworth on 4/4/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import +@import UIKit; +@interface NSUserDefaults (Color) + +- (UIColor *)colorForKey:(NSString *)colorKey; +- (void)setColor:(UIColor *)color forKey:(NSString *)colorKey; + +@end diff --git a/Sample Project/TestBed/NSUserDefaults+Color.m b/Sample Project/TestBed/NSUserDefaults+Color.m new file mode 100644 index 0000000..887ffb3 --- /dev/null +++ b/Sample Project/TestBed/NSUserDefaults+Color.m @@ -0,0 +1,33 @@ +// +// NSUserDefaults+NSUserDefaults_Color.m +// SimpleLineChart +// +// Created by Hugh Mackworth on 4/4/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import "NSUserDefaults+Color.h" + +@implementation NSUserDefaults (Color) + + + +- (UIColor *)colorForKey:(NSString *)colorKey { + UIColor * color = nil; + NSData * colorData = [self dataForKey:colorKey]; + if (colorData) { + color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData]; + } + return color; +} + +- (void)setColor:(UIColor *)color forKey:(NSString *)colorKey { + NSData * colorData = nil; + if (color) { + colorData = [NSKeyedArchiver archivedDataWithRootObject:color]; + } + [self setObject:colorData forKey:colorKey]; +} + + +@end diff --git a/Sample Project/TestBed/StatsViewController.h b/Sample Project/TestBed/StatsViewController.h new file mode 100644 index 0000000..4481982 --- /dev/null +++ b/Sample Project/TestBed/StatsViewController.h @@ -0,0 +1,33 @@ +// +// StatsViewController.h +// SimpleLineChart +// +// Created by iRare Media on 1/6/14. +// Copyright (c) 2014 Boris Emorine. All rights reserved. +// Copyright (c) 2014 Sam Spencer. +// +@import UIKit; + +@interface StatsViewController : UITableViewController + +@property (weak, nonatomic) IBOutlet UILabel *standardDeviationLabel; +@property (weak, nonatomic) IBOutlet UILabel *averageLabel; +@property (weak, nonatomic) IBOutlet UILabel *medianLabel; +@property (weak, nonatomic) IBOutlet UILabel *modeLabel; +@property (weak, nonatomic) IBOutlet UILabel *minimumLabel; +@property (weak, nonatomic) IBOutlet UILabel *maximumLabel; +@property (weak, nonatomic) IBOutlet UILabel *areaLabel; +@property (weak, nonatomic) IBOutlet UILabel *correlationLabel; +@property (weak, nonatomic) IBOutlet UIImageView *snapshotImageView; + +@property (strong, nonatomic) NSString *standardDeviation; +@property (strong, nonatomic) NSString *average; +@property (strong, nonatomic) NSString *median; +@property (strong, nonatomic) NSString *mode; +@property (strong, nonatomic) NSString *minimum; +@property (strong, nonatomic) NSString *maximum; +@property (strong, nonatomic) NSString *area; +@property (strong, nonatomic) NSString *correlation; +@property (strong, nonatomic) UIImage *snapshotImage; + +@end diff --git a/Sample Project/TestBed/StatsViewController.m b/Sample Project/TestBed/StatsViewController.m new file mode 100644 index 0000000..aa8aa7d --- /dev/null +++ b/Sample Project/TestBed/StatsViewController.m @@ -0,0 +1,79 @@ +// +// StatsViewController.m +// SimpleLineChart +// +// Created by iRare Media on 1/6/14. +// Copyright (c) 2014 Boris Emorine. All rights reserved. +// Copyright (c) 2014 Sam Spencer. +// + +#import "StatsViewController.h" + +@interface StatsViewController () + +@end + +@implementation StatsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view. + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(exit:)]; +} + +- (void)exit:(id)sender { + [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"Cell"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier]; + + if (indexPath.row == 0 && indexPath.section == 0) { + cell.textLabel.text = self.standardDeviation; + cell.detailTextLabel.text = @"Standard Deviation"; + return cell; + } else if (indexPath.row == 1) { + cell.textLabel.text = self.average; + cell.detailTextLabel.text = @"Average"; + return cell; + } else if (indexPath.row == 2) { + cell.textLabel.text = self.median; + cell.detailTextLabel.text = @"Median"; + return cell; + } else if (indexPath.row == 3) { + cell.textLabel.text = self.mode; + cell.detailTextLabel.text = @"Mode"; + return cell; + } else if (indexPath.row == 4) { + cell.textLabel.text = self.maximum; + cell.detailTextLabel.text = @"Maximum Value"; + return cell; + } else if (indexPath.row == 5) { + cell.textLabel.text = self.minimum; + cell.detailTextLabel.text = @"Minimum Value"; + return cell; + } else if (indexPath.row == 6) { + cell.textLabel.text = self.area; + cell.detailTextLabel.text = @"Area under Graph"; + return cell; + } else if (indexPath.row == 7) { + cell.textLabel.text = self.correlation; + cell.detailTextLabel.text = @"Correlation Coefficient"; + return cell; + } else if (indexPath.row == 0 && indexPath.section == 1) { + cell.textLabel.text = @"Rendered Snapshot"; + cell.imageView.image = self.snapshotImage; + return cell; + } else { + NSLog(@"Unknown"); + return cell; + } +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +@end diff --git a/Sample Project/TestBed/UIButton+Switch.h b/Sample Project/TestBed/UIButton+Switch.h new file mode 100644 index 0000000..ec5c4bc --- /dev/null +++ b/Sample Project/TestBed/UIButton+Switch.h @@ -0,0 +1,14 @@ +// +// UIButton+Switch.h +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/14/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import + +@interface UIButton (Switch) +@property (nonatomic) BOOL on; + +@end diff --git a/Sample Project/TestBed/UIButton+Switch.m b/Sample Project/TestBed/UIButton+Switch.m new file mode 100644 index 0000000..baa251d --- /dev/null +++ b/Sample Project/TestBed/UIButton+Switch.m @@ -0,0 +1,24 @@ +// +// UIButton+Switch.m +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/14/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import "UIButton+Switch.h" + +@implementation UIButton (Switch) +static NSString * checkOff = @"☐"; +static NSString * checkOn = @"☒"; + +- (void)setOn: (BOOL) on { + [self setTitle: (on ? checkOn : checkOff) forState:UIControlStateNormal]; +} + +- (BOOL)on { + if (!self.currentTitle) return NO; + return [checkOff isEqualToString: ( NSString * _Nonnull )self.currentTitle ]; +} + +@end diff --git a/Sample Project/TestBed/UITextField+Numbers.h b/Sample Project/TestBed/UITextField+Numbers.h new file mode 100644 index 0000000..8729702 --- /dev/null +++ b/Sample Project/TestBed/UITextField+Numbers.h @@ -0,0 +1,16 @@ +// +// UITextField+Numbers.h +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/14/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import + +//some convenience extensions for setting and reading +@interface UITextField (Numbers) +@property (nonatomic) CGFloat floatValue; +@property (nonatomic) NSInteger intValue; + +@end diff --git a/Sample Project/TestBed/UITextField+Numbers.m b/Sample Project/TestBed/UITextField+Numbers.m new file mode 100644 index 0000000..0aeb9bd --- /dev/null +++ b/Sample Project/TestBed/UITextField+Numbers.m @@ -0,0 +1,48 @@ +// +// UITextField+Numbers.m +// SimpleLineChart +// +// Created by Hugh Mackworth on 5/14/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +#import "UITextField+Numbers.h" + +@implementation UITextField (Numbers) + +- (void)setFloatValue:(CGFloat) num { + if (num < 0.0) { + self.text = @""; + } else if (num >= NSNotFound ) { + self.text = @"oops"; + } else { + self.text = [NSString stringWithFormat:@"%0.1f",num]; + } +} + +- (void)setIntValue:(NSInteger) num { + if (num == NSNotFound || num == -1 ) { + self.text = @""; + } else { + self.text = [NSString stringWithFormat:@"%d",(int)num]; + } +} + +- (CGFloat)floatValue { + if (self.text.length ==0) { + return -1.0; + } else { + return (CGFloat) self.text.floatValue; + } +} + +- (NSInteger)intValue { + if (self.text.length ==0) { + return -1; + } else { + return self.text.integerValue; + } + +} + +@end diff --git a/Sample Project/TestBed/main.m b/Sample Project/TestBed/main.m new file mode 100644 index 0000000..4b8e19f --- /dev/null +++ b/Sample Project/TestBed/main.m @@ -0,0 +1,16 @@ +// +// main.m +// TestBed +// +// Created by Hugh Mackworth on 3/18/17. +// Copyright © 2017 Boris Emorine. All rights reserved. +// + +@import UIKit; +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +}