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..585b19e 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, Double); + RestoreProperty (alpha, Double); + RestoreProperty (width, Double); + 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, Float); + EncodeProperty (alpha, Float); + EncodeProperty (width, Float); + 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/BEMGraphCalculator.m b/Classes/BEMGraphCalculator.m index 0cae401..8c2604e 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,8 +233,8 @@ - (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]]; diff --git a/Classes/BEMLine.h b/Classes/BEMLine.h index 3572cfc..3fe648c 100644 --- a/Classes/BEMLine.h +++ b/Classes/BEMLine.h @@ -43,19 +43,19 @@ typedef NS_ENUM(NSUInteger, BEMLineGradientDirection) { //----- POINTS -----// /// All of the Y-axis values for the points -@property (strong, nonatomic, nonnull) NSArray *arrayOfPoints; +@property (strong, nonatomic, nonnull) NSArray *arrayOfPoints; /// All of the X-Axis coordinates used to draw vertical lines through -@property (strong, nonatomic, nonnull) NSArray *arrayOfVerticalReferenceLinePoints; +@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; /// All of the Y-Axis coordinates used to draw horizontal lines through -@property (strong, nonatomic, nullable) NSArray *arrayOfHorizontalReferenceLinePoints; +@property (strong, nonatomic, nullable) NSArray *arrayOfHorizontalReferenceLinePoints; /// All of the point values -@property (strong, nonatomic, nullable) NSArray *arrayOfValues; +@property (strong, nonatomic, nullable) NSArray *arrayOfValues; /** 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 +77,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 +99,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..a6f5b2d 100644 --- a/Classes/BEMLine.m +++ b/Classes/BEMLine.m @@ -19,7 +19,7 @@ @interface BEMLine() -@property (nonatomic, strong) NSMutableArray *points; +@property (nonatomic, strong) NSMutableArray *points; @end @@ -33,6 +33,7 @@ - (instancetype)initWithFrame:(CGRect)frame { _enableLeftReferenceFrameLine = YES; _enableBottomReferenceFrameLine = YES; _interpolateNullValues = YES; + self.clipsToBounds = YES; } return self; } @@ -57,43 +58,44 @@ - (void)drawRect:(CGRect)rect { referenceFramePath.lineWidth = 0.7f; 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; + CGFloat xValue = [xNumber doubleValue]; 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]; - + NSUInteger index = [self.arrayOfVerticalReferenceLinePoints indexOfObject:xNumber]; + if (index == 0) { // far left reference line + xValue += self.verticalReferenceHorizontalFringeNegation; + } else if (index == [self.arrayOfVerticalReferenceLinePoints count]-1) { // far right reference line + xValue -= self.verticalReferenceHorizontalFringeNegation; + } + } CGPoint initialPoint = CGPointMake(xValue, self.frame.size.height); CGPoint finalPoint = CGPointMake(xValue, 0); @@ -142,26 +144,62 @@ - (void)drawRect:(CGRect)rect { 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]]; + CGFloat value = [self.arrayOfPoints[i] CGFloatValue]; + if (value >= BEMNullGraphValue && self.interpolateNullValues) { + //need to interpolate. For midpoints, just don't add a point + if (i == 0) { + //extrapolate a left edge point from next two actual values + NSUInteger firstPos = 1; //look for first real value + while (firstPos < self.arrayOfPoints.count && [self.arrayOfPoints[firstPos] CGFloatValue] >= BEMNullGraphValue) firstPos++; + if (firstPos >= self.arrayOfPoints.count) break; // all NaNs?? =>don't create any line + + CGFloat firstValue = [self.arrayOfPoints[firstPos] CGFloatValue]; + NSUInteger secondPos = firstPos+1; //look for second real value + while (secondPos < self.arrayOfPoints.count && [self.arrayOfPoints[secondPos] CGFloatValue] >= BEMNullGraphValue) secondPos++; + if (secondPos >= self.arrayOfPoints.count) { + // only one real number + value = firstValue; + } else { + CGFloat delta = firstValue - [self.arrayOfPoints[secondPos] CGFloatValue]; + value = firstValue + firstPos*delta/(secondPos-firstPos); + } + + } else if (i == self.arrayOfPoints.count-1) { + //extrapolate a right edge poit from previous two actual values + NSInteger firstPos = i-1; //look for first real value + while (firstPos >= 0 && [self.arrayOfPoints[firstPos] CGFloatValue] >= BEMNullGraphValue) firstPos--; + if (firstPos < 0 ) continue; // all NaNs?? =>don't create any line; should already be gone + + CGFloat firstValue = [self.arrayOfPoints[firstPos] CGFloatValue]; + NSInteger secondPos = firstPos-1; //look for second real value + while (secondPos >= 0 && [self.arrayOfPoints[secondPos] CGFloatValue] >= BEMNullGraphValue) secondPos--; + if (secondPos < 0) { + // only one real number + value = firstValue; + } else { + CGFloat delta = firstValue - [self.arrayOfPoints[secondPos] CGFloatValue]; + value = firstValue + (self.arrayOfPoints.count - firstPos-1)*delta/(firstPos - secondPos); + } + + } else { + continue; //skip this (middle Null) point, let graphics handle interpolation + } } - } - - 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]; + CGPoint newPoint = CGPointMake(xIndexScale * i, value); + [self.points addObject:[NSValue valueWithCGPoint:newPoint]]; + } + + if (!self.disableMainLine && self.bezierCurveIsEnabled) { + line = [BEMLine quadCurvedPathWithPoints:self.points open:YES]; + fillBottom = [BEMLine quadCurvedPathWithPoints:self.bottomPointsArray open:NO]; + fillTop = [BEMLine quadCurvedPathWithPoints:self.topPointsArray open:NO]; + } else if (!self.disableMainLine && !self.bezierCurveIsEnabled) { + line = [BEMLine linesToPoints:self.points open:YES]; + fillBottom = [BEMLine linesToPoints:self.bottomPointsArray open:NO]; + fillTop = [BEMLine linesToPoints:self.topPointsArray open:NO]; } else { - fillBottom = [BEMLine linesToPoints:self.bottomPointsArray]; - fillTop = [BEMLine linesToPoints:self.topPointsArray]; + fillBottom = [BEMLine linesToPoints:self.bottomPointsArray open:NO]; + fillTop = [BEMLine linesToPoints:self.topPointsArray open:NO]; } //----------------------------// @@ -178,7 +216,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 +224,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,67 +324,71 @@ - (void)drawRect:(CGRect)rect { } } -- (NSArray *)topPointsArray { +- (NSArray *)topPointsArray { CGPoint topPointZero = CGPointMake(0,0); CGPoint topPointFull = CGPointMake(self.frame.size.width, 0); - NSMutableArray *topPoints = [NSMutableArray arrayWithArray:self.points]; + NSMutableArray *topPoints = [NSMutableArray arrayWithArray:self.points]; [topPoints insertObject:[NSValue valueWithCGPoint:topPointZero] atIndex:0]; [topPoints addObject:[NSValue valueWithCGPoint:topPointFull]]; return topPoints; } -- (NSArray *)bottomPointsArray { +- (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]; + NSMutableArray *bottomPoints = [NSMutableArray arrayWithArray:self.points]; [bottomPoints insertObject:[NSValue valueWithCGPoint:bottomPointZero] atIndex:0]; [bottomPoints addObject:[NSValue valueWithCGPoint:bottomPointFull]]; return bottomPoints; } -+ (UIBezierPath *)linesToPoints:(NSArray *)points { ++ (UIBezierPath *)linesToPoints:(NSArray *)points open:(BOOL)open { UIBezierPath *path = [UIBezierPath bezierPath]; NSValue *value = points[0]; CGPoint p1 = [value CGPointValue]; [path moveToPoint:p1]; - for (NSUInteger i = 1; i < points.count; i++) { - value = points[i]; - CGPoint p2 = [value CGPointValue]; - [path addLineToPoint:p2]; + for (NSValue * point in points) { + if (point == value) continue; //already at first point + CGPoint p2 = [point CGPointValue]; + + if (open && (p1.y >= BEMNullGraphValue || p2.y >= BEMNullGraphValue)) { + [path moveToPoint:p2]; + } else { + [path addLineToPoint:p2]; + } + p1 = p2; } return path; } -+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points { ++ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points open:(BOOL)open { UIBezierPath *path = [UIBezierPath bezierPath]; NSValue *value = points[0]; CGPoint p1 = [value CGPointValue]; [path moveToPoint:p1]; - if (points.count == 2) { - value = points[1]; - CGPoint p2 = [value CGPointValue]; - [path addLineToPoint:p2]; - return path; - } - - 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)]; + for (NSValue * point in points) { + if (point == value) continue; //already at first point + CGPoint p2 = [point CGPointValue]; + if (open && (p1.y >= BEMNullGraphValue || p2.y >= BEMNullGraphValue)) { + [path moveToPoint:p2]; + } else { + CGPoint midPoint = midPointForPoints(p1, p2); + [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)]; + [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)]; + } p1 = p2; } return path; } static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) { - return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); + CGFloat avgY = (p1.y + p2.y) / 2.0; + if (isinf(avgY)) avgY = BEMNullGraphValue; + return CGPointMake((p1.x + p2.x) / 2, avgY); } static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) { @@ -403,7 +445,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..9d38dcd 100644 --- a/Classes/BEMSimpleLineGraphView.h +++ b/Classes/BEMSimpleLineGraphView.h @@ -240,7 +240,7 @@ IB_DESIGNABLE @interface BEMSimpleLineGraphView : UIView *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 @@ -473,7 +473,7 @@ IB_DESIGNABLE @interface BEMSimpleLineGraphView : UIView *)incrementPositionsForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph; diff --git a/Classes/BEMSimpleLineGraphView.m b/Classes/BEMSimpleLineGraphView.m index 765e827..2086fb5 100644 --- a/Classes/BEMSimpleLineGraphView.m +++ b/Classes/BEMSimpleLineGraphView.m @@ -8,8 +8,7 @@ // #import "BEMSimpleLineGraphView.h" -#import "BEMGraphCalculator.h" -#import "tgmath.h" +#import "BEMGraphCalculator.h" //just for deprecation warnings; should be removed const CGFloat BEMNullGraphValue = CGFLOAT_MAX; @@ -91,9 +90,8 @@ @interface BEMSimpleLineGraphView () { /// The label displayed when enablePopUpReport is set to YES @property (strong, nonatomic) UILabel *popUpLabel; -/// The label displayed when a custom popUpView is returned by the delegate -@property (strong, nonatomic) UIView *popUpView; - +// Possible custom View displayed instead of popUpLabel +@property (strong, nonatomic) UIView *customPopUpView; #pragma mark calculated properties /// The Y offset necessary to compensate the labels on the X-Axis @@ -108,8 +106,6 @@ @interface BEMSimpleLineGraphView () { /// The smallest value out of all of the data points @property (nonatomic) CGFloat minValue; -// Tracks whether the popUpView is custom or default -@property (nonatomic) BOOL usingCustomPopupView; // Stores the current view size to detect whether a redraw is needed in layoutSubviews @property (nonatomic) CGSize currentViewSize; @@ -117,12 +113,6 @@ @interface BEMSimpleLineGraphView () { /// 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 @@ -130,18 +120,140 @@ @implementation BEMSimpleLineGraphView #pragma mark - Initialization -- (instancetype)initWithFrame:(CGRect)frame { +- (instancetype) initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) [self commonInit]; return self; } -- (instancetype)initWithCoder:(NSCoder *)coder { +- (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 (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); + 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 (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 (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, Float); + 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, 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 (enableTouchReport, Bool); + EncodeProperty (enablePopUpReport, 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 (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 @@ -212,17 +324,6 @@ - (void)commonInit { _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]; - } - - [self drawEntireGraph]; -} - - (void)drawGraph { // Let the delegate know that the graph began layout updates if ([self.delegate respondsToSelector:@selector(lineGraphDidBeginLoading:)]) @@ -256,61 +357,70 @@ - (void)layoutSubviews { [self drawGraph]; } +- (void)clearGraph { + for (UIView * subvView in self.subviews) { + [subvView removeFromSuperview]; + } +} + - (void)layoutNumberOfPoints { // Get the total number of data points from the delegate +#ifndef TARGET_INTERFACE_BUILDER if ([self.dataSource respondsToSelector:@selector(numberOfPointsInLineGraph:)]) { numberOfPoints = [self.dataSource numberOfPointsInLineGraph:self]; + } else { + numberOfPoints = 0; + } +#else + numberOfPoints = 10; +#endif + [self.noDataLabel removeFromSuperview]; + [self.oneDot removeFromSuperview]; - } else numberOfPoints = 0; - - // There are no points to load - if (numberOfPoints == 0) { + if (numberOfPoints == 0) { + // There are no points to load + [self clearGraph]; if (self.delegate && [self.delegate respondsToSelector:@selector(noDataLabelEnableForLineGraph:)] && - ![self.delegate noDataLabelEnableForLineGraph:self]) return; + ![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."); - -#ifndef TARGET_INTERFACE_BUILDER self.noDataLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.viewForFirstBaselineLayout.frame.size.width, self.viewForFirstBaselineLayout.frame.size.height)]; -#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))]; -#endif - self.noDataLabel.backgroundColor = [UIColor clearColor]; self.noDataLabel.textAlignment = NSTextAlignmentCenter; - -#ifndef TARGET_INTERFACE_BUILDER - NSString *noDataText; + NSString *noDataText = nil; 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; + self.noDataLabel.font = self.noDataLabelFont ?: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1]; + self.noDataLabel.textColor = self.noDataLabelColor ?: (self.colorXaxisLabel ?: [UIColor blackColor]); [self.viewForFirstBaselineLayout addSubview:self.noDataLabel]; // Let the delegate know that the graph finished layout updates - if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) + if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) { [self.delegate lineGraphDidFinishLoading:self]; + } } 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."); + [self clearGraph]; 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]; + circleDot.alpha = 1.0f; + + [self.viewForFirstBaselineLayout addSubview:circleDot]; self.oneDot = circleDot; - } else { - // Remove all dots that might have previously been on the graph - [self.noDataLabel removeFromSuperview]; - [self.oneDot removeFromSuperview]; + // Let the delegate know that the graph finished layout updates + if ([self.delegate respondsToSelector:@selector(lineGraphDidFinishLoading:)]) { + [self.delegate lineGraphDidFinishLoading:self]; + } + } } @@ -320,15 +430,14 @@ - (void)layoutTouchReport { // 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.alpha = 0; + self.touchInputLine.backgroundColor = self.colorTouchInputLine; + [self 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; @@ -339,6 +448,7 @@ - (void)layoutTouchReport { self.longPressGesture.minimumPressDuration = 0.1f; [self.panView addGestureRecognizer:self.longPressGesture]; } + [self addSubview:self.panView]; } } @@ -367,9 +477,13 @@ - (void)drawEntireGraph { // Changing the order of the method calls below can result in drawing glitches and even crashes self.averageLine.yValue = NAN; +#ifndef TARGET_INTERFACE_BUILDER self.maxValue = [self getMaximumValue]; self.minValue = [self getMinimumValue]; - +#else + self.minValue = 0.0f; + self.maxValue = 10000.0f; +#endif // 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]; @@ -396,23 +510,27 @@ - (CGFloat)labelWidthForValue:(CGFloat)value { return [labelString sizeWithAttributes:attributes].width; } -- (CGFloat)calculateWidestLabel { +- (CGFloat) calculateWidestLabel { NSDictionary *attributes = @{NSFontAttributeName: self.labelFont}; CGFloat widestNumber; if (self.autoScaleYAxis == YES){ widestNumber = MAX([self labelWidthForValue:self.maxValue], [self labelWidthForValue:self.minValue]); } else { - widestNumber = [self labelWidthForValue:self.frame.size.height] ; + widestNumber = [self labelWidthForValue:self.frame.size.height] ; + } + if (self.averageLine.enableAverageLine) { + return MAX(widestNumber, [self.averageLine.title sizeWithAttributes:attributes].width); + } else { + return widestNumber; } - return MAX(widestNumber, [self.averageLine.title sizeWithAttributes:attributes].width); } - (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; + (((self.frame.size.width - self.YAxisLabelXOffset) / (numberOfPoints - 1)) * index) : + self.frame.size.width/2; if (self.positionYAxisRight == NO) { positionOnXAxis += self.YAxisLabelXOffset; } @@ -421,20 +539,21 @@ - (BEMCircle *)circleDotAtIndex:(NSUInteger)index forValue:(CGFloat)dotValue reu [yAxisValues addObject:@(positionOnYAxis)]; - 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; - } - - BEMCircle *circleDot; CGRect dotFrame = CGRectMake(0, 0, self.sizePoint, self.sizePoint); + BEMCircle *circleDot = nil; if (reuseNumber < self.circleDots.count) { circleDot = self.circleDots[reuseNumber]; circleDot.frame = dotFrame; + [circleDot setNeedsDisplay]; } else { circleDot = [[BEMCircle alloc] initWithFrame:dotFrame]; [self.circleDots addObject:circleDot]; } + if (dotValue >= BEMNullGraphValue) { + // If we're dealing with an null value, don't draw the dot (but leave it in yAxis to interpolate line) + [circleDot removeFromSuperview]; + return nil; + } circleDot.center = CGPointMake(positionOnXAxis, positionOnYAxis); circleDot.tag = (NSInteger) index + DotFirstTag100; @@ -446,14 +565,14 @@ - (BEMCircle *)circleDotAtIndex:(NSUInteger)index forValue:(CGFloat)dotValue reu } - (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; @autoreleasepool { for (NSUInteger index = 0; index < numberOfPoints; index++) { CGFloat dotValue = 0; @@ -462,72 +581,95 @@ - (void)drawDots { 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 { + [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)]; - BEMCircle *circleDot = [self circleDotAtIndex:index forValue:dotValue reuseNumber:circleDotNumber]; - if (circleDot) { - if (!circleDot.superview) { - [self addSubview:circleDot]; - } + BEMCircle * circleDot = [self circleDotAtIndex: index forValue: dotValue reuseNumber: index]; + UILabel * label = nil; + if (index < self.permanentPopups.count) { + label = self.permanentPopups[index]; + } else { + label = [[UILabel alloc] initWithFrame:CGRectZero]; + [self.permanentPopups addObject:label ]; + } - 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 (circleDot) { + [self addSubview:circleDot]; + + if ((self.alwaysDisplayPopUpLabels == YES) && + (![self.delegate respondsToSelector:@selector(lineGraph:alwaysDisplayPopUpAtIndex:)] || + [self.delegate lineGraph:self alwaysDisplayPopUpAtIndex:index])) { + label = [self configureLabel:label forPoint: circleDot ]; + + [self adjustXLocForLabel:label avoidingDot:circleDot.frame]; + + UILabel * leftNeighbor = (index >= 1 && self.permanentPopups[index-1].superview) ? self.permanentPopups[index-1] : nil; + UILabel * secondNeighbor = (index >= 2 && self.permanentPopups[index-2].superview) ? self.permanentPopups[index-2] : nil; + BOOL showLabel = [self adjustYLocForLabel:label + avoidingDot:circleDot.frame + andNeighbors:leftNeighbor.frame + and:secondNeighbor.frame ]; + if (showLabel) { + [self addSubview:label]; + } else { + [label removeFromSuperview]; } + } 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; if (self.animationGraphEntranceTime <= 0) { if (self.displayDotsOnly || self.alwaysDisplayDots ) { - circleDot.alpha = 1.0; - } - } 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]; - } - }]; + circleDot.alpha = 1.0f; } + label.alpha = 1.0f; + } else if (self.displayDotsWhileAnimating) { + [UIView animateWithDuration: self.animationGraphEntranceTime/numberOfPoints delay: index*(self.animationGraphEntranceTime/numberOfPoints) options:UIViewAnimationOptionCurveLinear animations:^{ + circleDot.alpha = 1.0; + label.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]; + } + }]; + } else if (label) { + [UIView animateWithDuration:0.5f delay:self.animationGraphEntranceTime options:UIViewAnimationOptionCurveLinear animations:^{ + label.alpha = 1; + } completion:nil]; } - circleDotNumber++; + + } else { + [label removeFromSuperview]; } } - - for (NSUInteger i = self.circleDots.count -1; i>=circleDotNumber; i--) { - [[self.permanentPopups lastObject] removeFromSuperview]; //no harm if not created + for (NSUInteger i = self.circleDots.count -1; i>=numberOfPoints; i--) { + [[self.permanentPopups lastObject] removeFromSuperview]; //no harm if not showing [self.permanentPopups removeLastObject]; [[self.circleDots lastObject] removeFromSuperview]; [self.circleDots removeLastObject]; } - - // Remove popups - [self.popUpLabel removeFromSuperview]; - [self.popUpView removeFromSuperview]; } } - (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]; } + [self addSubview:self.masterLine]; BEMLine * line = self.masterLine; line.opaque = NO; line.alpha = 1; @@ -560,6 +702,8 @@ - (void)drawLine { line.verticalReferenceHorizontalFringeNegation = xAxisHorizontalFringeNegationValue; line.arrayOfVerticalReferenceLinePoints = self.enableReferenceXAxisLines ? xAxisLabelPoints : nil; line.arrayOfHorizontalReferenceLinePoints = self.enableReferenceYAxisLines ? yAxisLabelPoints : nil; + } else { + line.enableReferenceLines = NO; } line.color = self.colorLine; @@ -569,11 +713,10 @@ - (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; + if (isnan(self.averageLine.yValue)) self.averageLine.yValue = self.getAverageValue; line.averageLineYCoordinate = [self yPositionForDotValue:self.averageLine.yValue]; - line.averageLine = self.averageLine; - } else line.averageLine = self.averageLine; + } + line.averageLine = self.averageLine; line.disableMainLine = self.displayDotsOnly; @@ -596,45 +739,49 @@ - (void)drawXAxis { if (![self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)]) return; [xAxisValues removeAllObjects]; + [xAxisLabelPoints removeAllObjects]; + xAxisHorizontalFringeNegationValue = 0.0; // Draw X-Axis Background Area if (!self.backgroundXAxis) { self.backgroundXAxis = [[UIView alloc] initWithFrame:[self drawableXAxisArea]]; - [self addSubview:self.backgroundXAxis]; } else { self.backgroundXAxis.frame = [self drawableXAxisArea]; } - self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis ?: self.colorBottom; - self.backgroundXAxis.alpha = self.alphaBackgroundXaxis; + [self addSubview:self.backgroundXAxis]; + + if (self.colorBackgroundXaxis) { + self.backgroundXAxis.backgroundColor = self.colorBackgroundXaxis; + self.backgroundXAxis.alpha = self.alphaBackgroundXaxis; + } else { + self.backgroundXAxis.backgroundColor = self.colorBottom; + self.backgroundXAxis.alpha = self.alphaBottom; + } NSArray *axisIndices = nil; if ([self.delegate respondsToSelector:@selector(incrementPositionsForXAxisOnLineGraph:)]) { axisIndices = [self.delegate incrementPositionsForXAxisOnLineGraph:self]; } else { - NSUInteger baseIndex; - NSUInteger increment; + NSUInteger baseIndex = 0; + NSUInteger 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 >= 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; } } + if (increment == 0) increment = 1; NSMutableArray *values = [NSMutableArray array ]; NSUInteger index = baseIndex; while (index < numberOfPoints) { @@ -648,25 +795,26 @@ - (void)drawXAxis { @autoreleasepool { for (NSNumber *indexNum in axisIndices) { NSUInteger index = indexNum.unsignedIntegerValue; + if (index > numberOfPoints) continue; NSString *xAxisLabelText = [self xAxisTextForIndex:index]; UILabel *labelXAxis = [self xAxisLabelWithText:xAxisLabelText atIndex:index reuseNumber: xAxisLabelNumber]; - [xAxisLabelPoints addObject:@(labelXAxis.center.x - (self.positionYAxisRight ? self.YAxisLabelXOffset : 0.0f))]; + [xAxisLabelPoints addObject:@(labelXAxis.center.x - (self.positionYAxisRight ? 0.0f : self.YAxisLabelXOffset))]; - if (!labelXAxis.superview) [self addSubview:labelXAxis]; + [self addSubview:labelXAxis]; [xAxisValues addObject:xAxisLabelText]; xAxisLabelNumber++; } } - for (NSUInteger i = self.xAxisLabels.count -1; i>=xAxisLabelNumber; i--) { + for (NSUInteger i = self.xAxisLabels.count ; i>xAxisLabelNumber; i--) { [[self.xAxisLabels lastObject] removeFromSuperview]; [self.xAxisLabels removeLastObject]; } __block UILabel *prevLabel; - NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:self.xAxisLabels.count]; + NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:self.xAxisLabels.count]; [self.xAxisLabels enumerateObjectsUsingBlock:^(UILabel *label, NSUInteger idx, BOOL *stop) { if (idx == 0) { prevLabel = label; //always show first label @@ -697,7 +845,7 @@ - (NSString *)xAxisTextForIndex:(NSUInteger)index { if ([self.dataSource respondsToSelector:@selector(lineGraph:labelOnXAxisForIndex:)]) { xAxisLabelText = [self.dataSource lineGraph:self labelOnXAxisForIndex:index]; - } else { + } else { xAxisLabelText = @""; } @@ -725,12 +873,12 @@ - (UILabel *)xAxisLabelWithText:(NSString *)text atIndex:(NSUInteger)index reuse // 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; + CGFloat horizontalTranslation = 0; 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 @@ -744,7 +892,7 @@ - (UILabel *)xAxisLabelWithText:(NSString *)text atIndex:(NSUInteger)index reuse return labelXAxis; } -- (NSString *)yAxisTextForValue:(CGFloat)value { +- (NSString *)yAxisTextForValue:(CGFloat) value { NSString *yAxisSuffix = @""; NSString *yAxisPrefix = @""; @@ -752,7 +900,7 @@ - (NSString *)yAxisTextForValue:(CGFloat)value { 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]; @@ -768,11 +916,11 @@ - (UILabel *)yAxisLabelWithText:(NSString *)text atValue:(CGFloat)value reuseNum 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; + xValueForCenterLabelYAxis = self.frame.size.width - xValueForCenterLabelYAxis-2.0f; } UILabel *labelYAxis; - if (reuseNumber == NSIntegerMax) { + if ( reuseNumber == NSIntegerMax) { if (!self.averageLine.label) { self.averageLine.label = [[UILabel alloc] initWithFrame:frameForLabelYAxis]; } @@ -783,6 +931,7 @@ - (UILabel *)yAxisLabelWithText:(NSString *)text atValue:(CGFloat)value reuseNum labelYAxis = [[UILabel alloc] initWithFrame:frameForLabelYAxis]; [self.yAxisLabels addObject:labelYAxis]; } + labelYAxis.frame = frameForLabelYAxis; labelYAxis.text = text; labelYAxis.textAlignment = textAlignmentForLabelYAxis; labelYAxis.font = self.labelFont; @@ -800,6 +949,8 @@ - (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]; } @@ -810,20 +961,25 @@ - (void)drawYAxis { //Make Background for Y Axis CGRect frameForBackgroundYAxis = CGRectMake( (self.positionYAxisRight ? - self.frame.size.width - self.YAxisLabelXOffset - 1: - 1), + self.frame.size.width - self.YAxisLabelXOffset - 1.0f: + 0.0), 0, - self.YAxisLabelXOffset -1, + self.YAxisLabelXOffset, self.frame.size.height); if (!self.backgroundYAxis) { self.backgroundYAxis= [[UIView alloc] initWithFrame:frameForBackgroundYAxis]; - [self addSubview:self.backgroundYAxis]; } else { self.backgroundYAxis.frame = frameForBackgroundYAxis; } - self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis ?: self.colorTop; - self.backgroundYAxis.alpha = self.alphaBackgroundYaxis; + [self addSubview:self.backgroundYAxis]; + if (self.colorBackgroundYaxis) { + self.backgroundYAxis.backgroundColor = self.colorBackgroundYaxis; + self.backgroundYAxis.alpha = self.alphaBackgroundYaxis; + } else { + self.backgroundYAxis.backgroundColor = self.colorTop; + self.backgroundYAxis.alpha = self.alphaTop; + } [yAxisLabelPoints removeAllObjects]; @@ -838,21 +994,18 @@ - (void)drawYAxis { 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; + value = (self.minValue + self.maxValue)/2.0f; increment = 0; //NA } else { - value = minValue; - increment = (maxValue - minValue)/(numberOfLabels-1); + value = self.minValue; + increment = (self.maxValue - self.minValue)/(numberOfLabels-1); if ([self.delegate respondsToSelector:@selector(baseValueForYAxisOnLineGraph:)] && [self.delegate respondsToSelector:@selector(incrementValueForYAxisOnLineGraph:)]) { value = [self.delegate baseValueForYAxisOnLineGraph:self]; increment = [self.delegate incrementValueForYAxisOnLineGraph:self]; - numberOfLabels = (NSUInteger) ((maxValue - value)/increment)+1; + if (increment <= 0) increment = 1; + numberOfLabels = (NSUInteger) ((self.maxValue - value)/increment)+1; if (numberOfLabels > 100) { NSLog(@"[BEMSimpleLineGraph] Increment does not properly lay out Y axis, bailing early"); return; @@ -870,7 +1023,7 @@ - (void)drawYAxis { value = increment/2; } } - NSMutableArray *dotValues = [[NSMutableArray alloc] initWithCapacity:numberOfLabels]; + NSMutableArray *dotValues = [[NSMutableArray alloc] initWithCapacity:numberOfLabels]; for (NSUInteger i = 0; i < numberOfLabels; i++) { [dotValues addObject:@(value)]; value += increment; @@ -884,7 +1037,7 @@ - (void)drawYAxis { atValue:dotValue reuseNumber:yAxisLabelNumber]; - if (!labelYAxis.superview) [self addSubview:labelYAxis]; + [self addSubview:labelYAxis]; yAxisLabelNumber++; } } @@ -896,7 +1049,7 @@ - (void)drawYAxis { // Detect overlapped labels __block UILabel * prevLabel = nil;; - NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:0]; + NSMutableArray *overlapLabels = [NSMutableArray arrayWithCapacity:0]; [self.yAxisLabels enumerateObjectsUsingBlock:^(UILabel *label, NSUInteger idx, BOOL *stop) { @@ -904,17 +1057,17 @@ - (void)drawYAxis { 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)) { + CGRectContainsRect(self.backgroundYAxis.frame, 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)); + // 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)); } } }]; @@ -923,10 +1076,10 @@ - (void)drawYAxis { if (self.averageLine.enableAverageLine && self.averageLine.title.length > 0) { UILabel *averageLabel = [self yAxisLabelWithText:self.averageLine.title - atValue:self.averageLine.yValue - reuseNumber:NSIntegerMax]; + atValue:self.averageLine.yValue + reuseNumber:NSIntegerMax]; - if (!averageLabel.superview) [self addSubview:averageLabel]; + [self addSubview:averageLabel]; //check for overlap; Average wins for (UILabel * label in self.yAxisLabels) { @@ -944,9 +1097,9 @@ - (void)drawYAxis { } /// Area on the graph that doesn't include the axes -- (CGRect)drawableGraphArea { +- (CGRect) drawableGraphArea { // CGRectMake(xAxisXPositionFirstOffset, self.frame.size.height-20, viewWidth/2, 20); - CGFloat xAxisHeight = self.labelFont.pointSize + 8.0f; + CGFloat xAxisHeight = self.enableXAxisLabel ? self.labelFont.pointSize + 8.0f : 0.0f; CGFloat xOrigin = self.positionYAxisRight ? 0 : self.YAxisLabelXOffset; CGFloat viewWidth = self.frame.size.width - self.YAxisLabelXOffset; CGFloat adjustedHeight = self.bounds.size.height - xAxisHeight; @@ -963,225 +1116,94 @@ - (CGRect)drawableXAxisArea { 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]; +- (UILabel *)configureLabel: (UILabel *) oldLabel forPoint: (BEMCircle *)circleDot { - // 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]; - } - - 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; - + + NSUInteger index = (NSUInteger) 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) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" + //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.doubleValue]; #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 ; + CGSize requiredSize = [newPopUpLabel sizeThatFits:CGSizeMake(100.0f, CGFLOAT_MAX)]; + newPopUpLabel.frame = CGRectMake(10, 10, requiredSize.width+10.0f, requiredSize.height+10.0f); + 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 ; if (self.enableYAxisLabel && !self.positionYAxisRight && ((xCenter - halfLabelWidth) <= self.YAxisLabelXOffset) ) { - // When bumping into left Y axis + //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 + //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 + //When over left edge xCenter = halfLabelWidth + 4.0f; } else if (xCenter + halfLabelWidth >= self.frame.size.width) { - // When over right edge + //When over right edge xCenter = self.frame.size.width - halfLabelWidth; } - - // 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 + popUpLabel.center = CGPointMake(xCenter, popUpLabel.center.y); +} + +- (BOOL)adjustYLocForLabel:(UIView *)popUpLabel avoidingDot:(CGRect)dotFrame andNeighbors:(CGRect)leftNeightbor and:(CGRect)secondNeighbor { + //returns YES if it can avoid those neighbors + //note: nil.frame == CGRectZero + //check for bumping into top OR overlap with left neighbors + //default Y is above point + CGFloat halfLabelHeight = popUpLabel.frame.size.height/2.0f; + popUpLabel.center = CGPointMake(popUpLabel.center.x, CGRectGetMinY(dotFrame) - 12.0f - halfLabelHeight ); + if (CGRectGetMinY(popUpLabel.frame) < 2.0f || + (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, leftNeightbor))) || + (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, secondNeighbor)))) { + //if so, 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 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; + (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, leftNeightbor))) || + (!CGRectIsEmpty(CGRectIntersection(popUpLabel.frame, secondNeighbor)))) { + return NO; } } - - if (self.animationGraphEntranceTime <= 0) { - newPopUpLabel.alpha = 1.0f; - } else { - [UIView animateWithDuration:0.5f delay:self.animationGraphEntranceTime options:UIViewAnimationOptionCurveLinear animations:^{ - newPopUpLabel.alpha = 1.0f; - } completion:nil]; - } - - return newPopUpLabel; + return YES; } - (UIImage *)graphSnapshotImage { @@ -1241,9 +1263,13 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.numberOfTouches >= self.touchReportFingersRequired) { CGPoint translation = [self.panGesture velocityInView:self.panView]; return fabs(translation.y) < fabs(translation.x); - } else return NO; + } else { + return NO; + } return YES; - } else return [super gestureRecognizerShouldBegin:gestureRecognizer]; + } else { + return [super gestureRecognizerShouldBegin:gestureRecognizer]; + } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { @@ -1264,55 +1290,83 @@ - (void)handleGestureAction:(UIGestureRecognizer *)recognizer { self.touchInputLine.alpha = self.alphaTouchInputLine; BEMCircle *closestDot = [self closestDotFromTouchInputLine:self.touchInputLine]; - closestDot.alpha = 0.8f; - - if (self.enablePopUpReport == YES && closestDot.tag >= DotFirstTag100 && [closestDot isKindOfClass:[BEMCircle class]] && self.alwaysDisplayPopUpLabels == NO) { - [self labelForPoint:closestDot reuseNumber:NSIntegerMax]; + NSUInteger index = 0; + if (closestDot.tag > DotFirstTag100) { + index = closestDot.tag - DotFirstTag100; + } else { + if (numberOfPoints == 0) return; //something's very wrong } + closestDot.alpha = 0.8f; - 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 (recognizer.state != UIGestureRecognizerStateEnded) { + //ON START OR MOVE + if (self.enablePopUpReport == YES && self.alwaysDisplayPopUpLabels == NO) { + if (!self.customPopUpView && [self.delegate respondsToSelector:@selector(popUpViewForLineGraph:)] ) { + self.customPopUpView = [self.delegate popUpViewForLineGraph:self]; + } + if (self.customPopUpView) { + [self addSubview:self.customPopUpView]; + [self adjustXLocForLabel:self.customPopUpView avoidingDot:closestDot.frame]; + [self adjustYLocForLabel:self.customPopUpView avoidingDot:closestDot.frame andNeighbors:CGRectZero and:CGRectZero]; + 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 addSubview: self.popUpLabel ]; + self.popUpLabel = [self configureLabel:self.popUpLabel forPoint:closestDot]; + [self adjustXLocForLabel:self.popUpLabel avoidingDot:closestDot.frame]; + [self adjustYLocForLabel:self.popUpLabel avoidingDot:closestDot.frame andNeighbors:CGRectZero and:CGRectZero]; + [UIView animateWithDuration:0.2f delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.popUpLabel.alpha = 1.0f; + } completion:nil]; + } } - } - - // ON RELEASE - if (recognizer.state == UIGestureRecognizerStateEnded) { - if ([self.delegate respondsToSelector:@selector(lineGraph:didReleaseTouchFromGraphWithClosestIndex:)]) { - [self.delegate lineGraph:self didReleaseTouchFromGraphWithClosestIndex:(closestDot.tag - DotFirstTag100)]; + if (self.enableTouchReport && [self.delegate respondsToSelector:@selector(lineGraph:didTouchGraphWithClosestIndex:)]) { + [self.delegate lineGraph:self didTouchGraphWithClosestIndex:index]; + } + } else { + // ON RELEASE + if (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) { 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) { + point.alpha = 0; + } + CGFloat distance = (CGFloat)fabs(point.center.x - touchInputLine.center.x) ; + if (distance < currentlyCloser) { + currentlyCloser = distance; + closestDot = point; } } return closestDot; @@ -1322,15 +1376,11 @@ - (CGFloat)getMaximumValue { 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; - + CGFloat dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:i]; if (dotValue >= BEMNullGraphValue) continue; if (dotValue > maxValue) maxValue = dotValue; } @@ -1343,16 +1393,11 @@ - (CGFloat)getMinimumValue { if ([self.delegate respondsToSelector:@selector(minValueForLineGraph:)]) { return [self.delegate minValueForLineGraph:self]; } else { - CGFloat dotValue; CGFloat minValue = INFINITY; @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 dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:i]; if (dotValue >= BEMNullGraphValue) continue; if (dotValue < minValue) minValue = dotValue; } @@ -1360,9 +1405,30 @@ - (CGFloat)getMinimumValue { return minValue; } } +- (CGFloat)getAverageValue { + if ([self.delegate respondsToSelector:@selector(averageValueForLineGraph:)]) { + return [self.delegate averageValueForLineGraph:self]; + } else { + CGFloat sumValue = 0.0f; + int numPoints = 0; + @autoreleasepool { + for (NSUInteger i = 0; i < numberOfPoints; i++) { + CGFloat dotValue = [self.dataSource lineGraph:self valueForPointAtIndex:i]; + if (dotValue >= BEMNullGraphValue) continue; + sumValue += dotValue; + numPoints++; + } + } + if (numPoints > 0) { + return sumValue/numPoints; + } else { + return NAN; + } + } +} - (CGFloat)yPositionForDotValue:(CGFloat)dotValue { - if (dotValue >= BEMNullGraphValue) { + if (isnan(dotValue) || dotValue >= BEMNullGraphValue) { return BEMNullGraphValue; } @@ -1382,7 +1448,7 @@ - (CGFloat)yPositionForDotValue:(CGFloat)dotValue { 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; + positionOnYAxis = topOfChart - percentValue * sizeOfChart + self.XAxisLabelYOffset; } } else { positionOnYAxis = ((self.frame.size.height) - dotValue); @@ -1394,7 +1460,7 @@ - (CGFloat)yPositionForDotValue:(CGFloat)dotValue { #pragma mark - Deprecated Methods -- (NSNumber *)calculatePointValueSum { + - (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..29cdcc1 100644 --- a/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj +++ b/Sample Project/SimpleLineChart.xcodeproj/project.pbxproj @@ -7,6 +7,32 @@ objects = { /* Begin PBXBuildFile section */ + 1E03A0321EC8BED200CA4247 /* UITextField+Numbers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E03A0311EC8BED200CA4247 /* UITextField+Numbers.m */; }; + 1E03A0351EC8BF4C00CA4247 /* UIButton+Switch.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E03A0341EC8BF4C00CA4247 /* UIButton+Switch.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 */; }; 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,12 +70,54 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1E03A0301EC8BED200CA4247 /* UITextField+Numbers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextField+Numbers.h"; sourceTree = ""; }; + 1E03A0311EC8BED200CA4247 /* UITextField+Numbers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITextField+Numbers.m"; sourceTree = ""; }; + 1E03A0331EC8BF4C00CA4247 /* UIButton+Switch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIButton+Switch.h"; sourceTree = ""; }; + 1E03A0341EC8BF4C00CA4247 /* UIButton+Switch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIButton+Switch.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 = ""; }; 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 = ""; }; @@ -58,7 +126,7 @@ 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 = ""; }; + 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 +151,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 1E960A7E1E7DF942000E2BB8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3FD8152186DFD9A00FD8ED3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -106,6 +181,70 @@ /* 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 */, + 1E03A0331EC8BF4C00CA4247 /* UIButton+Switch.h */, + 1E03A0341EC8BF4C00CA4247 /* UIButton+Switch.m */, + 1E03A0301EC8BED200CA4247 /* UITextField+Numbers.h */, + 1E03A0311EC8BED200CA4247 /* UITextField+Numbers.m */, + 1E960A9E1E7DF9BB000E2BB8 /* StatsViewController.h */, + 1E960A9F1E7DF9BC000E2BB8 /* StatsViewController.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 +254,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 +271,7 @@ C3B90A55187D15F7003E407D /* Classes */, C3FD815E186DFD9A00FD8ED3 /* SimpleLineChart */, C3FD817D186DFD9A00FD8ED3 /* SimpleLineChartTests */, + 1E960A821E7DF942000E2BB8 /* TestBed */, C3FD8157186DFD9A00FD8ED3 /* Frameworks */, C3FD8156186DFD9A00FD8ED3 /* Products */, ); @@ -142,6 +282,7 @@ children = ( C3FD8155186DFD9A00FD8ED3 /* SimpleLineChart.app */, C3FD8176186DFD9A00FD8ED3 /* SimpleLineChartTests.xctest */, + 1E960A811E7DF942000E2BB8 /* TestBed.app */, ); name = Products; sourceTree = ""; @@ -208,6 +349,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 +410,14 @@ LastUpgradeCheck = 0800; ORGANIZATIONNAME = "Boris Emorine"; TargetAttributes = { + 1E960A801E7DF942000E2BB8 = { + CreatedOnToolsVersion = 8.2.1; + DevelopmentTeam = E8XXXD4S77; + ProvisioningStyle = Automatic; + }; + C3FD8154186DFD9A00FD8ED3 = { + DevelopmentTeam = E8XXXD4S77; + }; C3FD8175186DFD9A00FD8ED3 = { TestTargetID = C3FD8154186DFD9A00FD8ED3; }; @@ -272,11 +438,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 +478,36 @@ /* 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 */, + 1E960AA61E7E097C000E2BB8 /* BEMLine.m in Sources */, + 1E960AA71E7E097C000E2BB8 /* BEMAverageLine.m in Sources */, + 1E960AA81E7E0990000E2BB8 /* BEMGraphCalculator.m in Sources */, + 1E960AA01E7DF9BC000E2BB8 /* ARFontPickerViewController.m in Sources */, + 1E960B171E7F9C80000E2BB8 /* MSHSBView.m in Sources */, + 1E03A0351EC8BF4C00CA4247 /* UIButton+Switch.m in Sources */, + 1E960B121E7F9C80000E2BB8 /* MSColorComponentView.m in Sources */, + 1E960A881E7DF942000E2BB8 /* AppDelegate.m in Sources */, + 1E960AA31E7DF9BC000E2BB8 /* StatsViewController.m in Sources */, + 1E960AA21E7DF9BC000E2BB8 /* MasterViewController.m in Sources */, + 1E03A0321EC8BED200CA4247 /* UITextField+Numbers.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 +544,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 +595,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 = { @@ -420,6 +682,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; @@ -466,6 +733,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 +760,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 +829,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/StatsViewController.m b/Sample Project/SimpleLineChart/StatsViewController.m index 5796b48..3623d76 100644 --- a/Sample Project/SimpleLineChart/StatsViewController.m +++ b/Sample Project/SimpleLineChart/StatsViewController.m @@ -17,7 +17,6 @@ @implementation StatsViewController - (void)viewDidLoad { [super viewDidLoad]; - // Do any additional setup after loading the view. } - (void)viewWillAppear:(BOOL)animated { 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..42d406f 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; diff --git a/Sample Project/SimpleLineChartTests/CustomizationTests.m b/Sample Project/SimpleLineChartTests/CustomizationTests.m index 4aad044..ce1fc9b 100644 --- a/Sample Project/SimpleLineChartTests/CustomizationTests.m +++ b/Sample Project/SimpleLineChartTests/CustomizationTests.m @@ -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; @@ -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..be477d9 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; @@ -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,14 @@ - (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"); 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 +114,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 +141,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 +152,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..0d2c40a --- /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 [[UIFont familyNames] count]; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSString *familyName = [self fontFamilyForSection:section]; + return [[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..2ebc00f --- /dev/null +++ b/Sample Project/TestBed/AppDelegate.m @@ -0,0 +1,30 @@ +// +// 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 { + return YES; +} + +- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder { + return YES; +} + +- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder { + return YES; +} + +@end 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..4d2f2a0 --- /dev/null +++ b/Sample Project/TestBed/Base.lproj/Main.storyboard @@ -0,0 +1,3096 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sample Project/TestBed/DetailViewController.h b/Sample Project/TestBed/DetailViewController.h new file mode 100644 index 0000000..6f561d7 --- /dev/null +++ b/Sample Project/TestBed/DetailViewController.h @@ -0,0 +1,43 @@ +// +// 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; + +//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) BOOL noDataLabel; +@property (strong, nonatomic) NSString *noDataText; +@property (assign, nonatomic) CGFloat staticPaddingValue; +@property (assign, nonatomic) BOOL provideCustomView; +@property (assign, nonatomic) NSUInteger numberOfGapsBetweenLabels; +@property (assign, nonatomic) NSUInteger baseIndexForXAxis; +@property (assign, nonatomic) NSUInteger incrementIndexForXAxis; +@property (assign, nonatomic) BOOL provideIncrementPositionsForXAxis; +@property (assign, nonatomic) NSUInteger 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..d4c6a50 --- /dev/null +++ b/Sample Project/TestBed/DetailViewController.m @@ -0,0 +1,367 @@ +// +// 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 (weak, nonatomic) IBOutlet UIStepper *graphObjectIncrement; +@property (nonatomic) NSInteger previousStepperValue; + +@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) IBOutlet UIView * customView; +@property (weak, nonatomic) IBOutlet UILabel * customViewLabel; +@end + +@implementation DetailViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + self.navigationItem.leftItemsSupplementBackButton = YES; + + self.maxValue = -1.0; + self.minValue = -1.0; + self.staticPaddingValue = -1.0; + self.numberOfGapsBetweenLabels = NSNotFound; + self.baseIndexForXAxis = NSNotFound; + self.incrementIndexForXAxis = NSNotFound; + self.numberOfYAxisLabels = NSNotFound; + self.baseValueForYAxis = -1.0; + self.incrementValueForYAxis = -1.0; + // Do any additional setup after loading the view. + + + [self hydrateDatasets]; + + [self updateLabelsBelowGraph:self.myGraph]; +} + +#pragma mark Data management +- (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]; + + self.totalNumber = 0; + NSDate *baseDate = [NSDate date]; + BOOL showNullValue = YES; + self.graphObjectIncrement.value = 9; + self.previousStepperValue = self.graphObjectIncrement.value; + + // Add objects to the array based on the stepper value + for (int 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 + } else { + [self.arrayOfDates addObject:[self dateForGraphAfterDate:self.arrayOfDates[i-1]]]; // Dates for the X-Axis of the graph + } + if (showNullValue && (i % 4 == 0)) { + self.arrayOfValues[i] = @(BEMNullGraphValue); + } else { + self.totalNumber = self.totalNumber + [[self.arrayOfValues objectAtIndex:i] intValue]; // All of the values added together + } + + } +} + +- (NSDate *)dateForGraphAfterDate:(NSDate *)date { + NSTimeInterval secondsInTwentyFourHours = 24 * 60 * 60; + NSDate *newDate = [date dateByAddingTimeInterval:secondsInTwentyFourHours]; + return newDate; +} + +- (NSString *)labelForDateAtIndex:(NSInteger)index { + NSDate *date = self.arrayOfDates[index]; + NSDateFormatter *df = [[NSDateFormatter alloc] init]; + df.dateFormat = @"MM/dd"; + NSString *label = [df stringFromDate:date]; + return label; +} + +#pragma mark - Graph Actions + +// Refresh the line graph using the specified properties +- (IBAction)refresh:(id)sender { + [self hydrateDatasets]; + [self.myGraph reloadGraph]; +} + +- (float)getRandomFloat { + float i1 = (float)(arc4random() % 1000000) / 100 ; + return i1; +} + +- (IBAction)addOrRemovePointFromGraph:(id)sender { + if (self.graphObjectIncrement.value > self.previousStepperValue) { + [self addPointToGraph]; + } else if (self.graphObjectIncrement.value < self.previousStepperValue) { + [self removePointFromGraph]; + } + self.previousStepperValue = self.graphObjectIncrement.value; +} + +- (void) addPointToGraph { + // Add point + NSNumber * newValue ; + if (self.arrayOfValues.count % 4 == 0) { + newValue = @(BEMNullGraphValue); + } else { + newValue = @([self getRandomFloat]); + } + [self.arrayOfValues addObject:newValue]; + NSDate *lastDate = self.arrayOfDates.count > 0 ? [self.arrayOfDates lastObject]: [NSDate date]; + NSDate *newDate = [self dateForGraphAfterDate:lastDate]; + [self.arrayOfDates addObject:newDate]; + [self.myGraph reloadGraph]; +} + +- (void) removePointFromGraph { + if (self.arrayOfValues.count > 0) { + // Remove point + [self.arrayOfValues removeObjectAtIndex:0]; + [self.arrayOfDates removeObjectAtIndex:0]; + [self.myGraph reloadGraph]; + } +} +- (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.snapshotImage = [self.myGraph graphSnapshotImage]; + } +} + + +#pragma mark - SimpleLineGraph Data Source + +- (NSUInteger)numberOfPointsInLineGraph:(BEMSimpleLineGraphView *)graph { + return [self.arrayOfValues count]; +} + +- (CGFloat)lineGraph:(BEMSimpleLineGraphView *)graph valueForPointAtIndex:(NSUInteger)index { + return [[self.arrayOfValues objectAtIndex:index] doubleValue]; +} + +#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(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 != NSNotFound; + } else if (aSelector == @selector(baseIndexForXAxisOnLineGraph:)) { + return self.baseIndexForXAxis != NSNotFound; + } else if (aSelector == @selector(incrementIndexForXAxisOnLineGraph:)) { + return self.incrementIndexForXAxis != NSNotFound; + } else if (aSelector == @selector(incrementPositionsForXAxisOnLineGraph:)) { + return self.provideIncrementPositionsForXAxis; + } else if (aSelector == @selector(numberOfYAxisLabelsOnLineGraph:)) { + return self.numberOfYAxisLabels != NSNotFound; + } 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]; + } +} + + +- (NSString *)lineGraph:(BEMSimpleLineGraphView *)graph labelOnXAxisForIndex:(NSUInteger)index { + NSString *label = [self labelForDateAtIndex:index]; + return [label stringByReplacingOccurrencesOfString:@" " withString:@"\n"]; +} + +- (NSString *)popUpSuffixForlineGraph:(BEMSimpleLineGraphView *)graph { + return self.popUpSuffix; +} + +- (NSString *)popUpPrefixForlineGraph:(BEMSimpleLineGraphView *)graph { + return self.popUpPrefix; +} + +- (NSString *)popUpTextForlineGraph:(BEMSimpleLineGraphView *)graph atIndex:(NSUInteger)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:(NSUInteger)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:(NSUInteger)index { + NSAssert (popupView == self.customView, @"View problem"); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-nonliteral" + if (!self.myGraph.formatStringForValues.length) return; + self.customViewLabel.text = [NSString stringWithFormat:self.myGraph.formatStringForValues, [self lineGraph:graph valueForPointAtIndex:index] ]; +#pragma pop +} + +//----- X AXIS -----// + +- (NSUInteger)numberOfGapsBetweenLabelsOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.numberOfGapsBetweenLabels; +} + +- (NSUInteger)baseIndexForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.baseIndexForXAxis; +} + +- (NSUInteger)incrementIndexForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + return self.incrementIndexForXAxis; +} + +- (NSArray *)incrementPositionsForXAxisOnLineGraph:(BEMSimpleLineGraphView *)graph { + NSMutableArray * positions = [NSMutableArray array]; + NSUInteger index = 1; + while (index < self.arrayOfValues.count) { + if (arc4random() % 4 == 0) [positions addObject:@(index)]; + index +=1; + } + return positions; +} + +//----- Y AXIS -----// + +- (NSUInteger)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:(NSUInteger)index { + self.labelValues.text = [self formatNumber:[self.arrayOfValues objectAtIndex:index]]; + 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]; + }]; +} + +- (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: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..d052473 --- /dev/null +++ b/Sample Project/TestBed/Info.plist @@ -0,0 +1,45 @@ + + + + + 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 + + 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..0528776 --- /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: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..54262b1 --- /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 NSUInteger 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: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:(NSUInteger)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:(NSUInteger)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: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..c0cfabb --- /dev/null +++ b/Sample Project/TestBed/MasterViewController.m @@ -0,0 +1,871 @@ +// +// 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 "UITextField+Numbers.h" +#import "UIButton+Switch.h" + +@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 (nonatomic) BOOL hasRestoredUI; + +@property (strong, nonatomic) IBOutlet BEMSimpleLineGraphView *myGraph; + +@property (strong, nonatomic) NSDictionary *methodList; + +@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 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 *yAxisSwitch; +@property (strong, nonatomic) IBOutlet UISwitch *yAxisRightSwitch; +@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 *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 + +static NSString * enableTouchReport = @"enableTouchReport"; +static NSString * lineChartPrefix = @"lineChart"; + +CGGradientRef createGradient () { + 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; +} + + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + self.title = @"Options"; + self.hasRestoredUI = NO; + self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showDetailTargetDidChange:) name:UIViewControllerShowDetailTargetDidChangeNotification object:nil]; + [self showDetailTargetDidChange:self]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + if (!self.hasRestoredUI) [self restoreUI]; +} + +- (void)decodeRestorableStateWithCoder:(NSCoder *)coder { + [super decodeRestorableStateWithCoder:coder]; + [self restoreUI]; //kludge for VWA not getting called during restore +} + +- (void)restoreUI { + [self.detailViewController loadViewIfNeeded]; + self.myGraph = self.detailViewController.myGraph; + self.hasRestoredUI = YES; + + 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.xAxisSwitch.on = self.myGraph.enableXAxisLabel; + self.numberOfGapsBetweenLabelsField.intValue = self.detailViewController.numberOfGapsBetweenLabels; + self.baseIndexForXAxisField.floatValue = self.detailViewController.baseValueForYAxis; + self.incrementIndexForXAxisField.intValue = self.detailViewController.incrementIndexForXAxis; + self.arrayOfIndicesForXAxis.on = self.detailViewController.provideIncrementPositionsForXAxis; + + 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.incrementIndexForXAxisField.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.widthTouchInputLineField.floatValue = self.myGraph.widthTouchInputLine; + + self.fontNameButton.titleLabel.text = self.myGraph.labelFont.fontName; + 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; + +} + +/* properties/methods not implemented: + touchReportFingersRequired, + autoScaleYAxis + + Dashpatterns for averageLine, XAxis, Yaxis + + Gradient choices + */ + + +#pragma mark Main Line +- (IBAction)widthLineDidChange:(UITextField *)sender { + float value = sender.text.floatValue; + if (value > 0.0f) { + self.myGraph.widthLine = sender.text.doubleValue; + } + [self.myGraph reloadGraph]; +} + +- (IBAction)staticPaddingDidChange:(UITextField *)sender { + self.detailViewController.staticPaddingValue = sender.text.doubleValue; + [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]; +} + +#pragma mark Axes and Reference Lines +- (NSUInteger)getValue:(NSString *) text { + return (text.length > 0 && text.integerValue >= 0) ? text.integerValue : NSNotFound; +} + +- (IBAction)enableXAxisLabel:(UISwitch *)sender { + self.myGraph.enableXAxisLabel = sender.on; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberOfGapsBetweenLabelDidChange:(UITextField *)sender { + self.detailViewController.numberOfGapsBetweenLabels = [self getValue:sender.text]; + [self.myGraph reloadGraph]; +} + +- (IBAction)baseIndexForXAxisDidChange:(UITextField *)sender { + self.detailViewController.baseIndexForXAxis = [self getValue:sender.text]; + [self.myGraph reloadGraph]; +} +- (IBAction)incrementIndexForXAxisDidChange:(UITextField *)sender { + self.detailViewController.incrementIndexForXAxis = [self getValue:sender.text]; + [self.myGraph reloadGraph]; +} + +- (IBAction)enableArrayOfIndicesForXAxis:(UISwitch *)sender { + self.detailViewController.provideIncrementPositionsForXAxis = sender.on; + [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)minValueDidChange:(UITextField *)sender { + self.detailViewController.minValue = sender.text.doubleValue; + [self.myGraph reloadGraph]; + +} + +- (IBAction)maxValueDidChange:(UITextField *)sender { + self.detailViewController.maxValue = sender.text.doubleValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)numberofYAxisDidChange:(UITextField *)sender { + self.detailViewController.numberOfYAxisLabels = [self getValue:sender.text]; + [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.text.doubleValue; + [self.myGraph reloadGraph]; + +} +- (IBAction)incrementValueForYAxisDidChange:(UITextField *)sender { + self.detailViewController.incrementValueForYAxis = sender.text.doubleValue; + [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; + } else { + self.myGraph.animationGraphStyle ++; + } + [self updateAnimationGraphStyle]; + [self.myGraph reloadGraph]; +} + +- (IBAction)animationGraphEntranceTimeDidChange:(UITextField *)sender { + self.myGraph.animationGraphEntranceTime = (CGFloat) sender.text.floatValue; + [self.myGraph reloadGraph]; +} + +- (IBAction)displayDotsWhileAnimating:(UISwitch *)sender { + self.myGraph.displayDotsWhileAnimating = 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 { + if (!fontName) fontName = self.fontNameButton.titleLabel.text; + CGFloat 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.myGraph reloadGraph]; + } +} + +- (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]; +} + +- (IBAction)fontSizeFieldChanged:(UITextField *)sender { + [self updateFont:nil]; +} + +- (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 name:UIViewControllerShowDetailTargetDidChangeNotification object:nil]; +} +@end + diff --git a/Sample Project/TestBed/StatsViewController.h b/Sample Project/TestBed/StatsViewController.h new file mode 100644 index 0000000..2615045 --- /dev/null +++ b/Sample Project/TestBed/StatsViewController.h @@ -0,0 +1,29 @@ +// +// 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 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) UIImage *snapshotImage; + +@end diff --git a/Sample Project/TestBed/StatsViewController.m b/Sample Project/TestBed/StatsViewController.m new file mode 100644 index 0000000..bb90e14 --- /dev/null +++ b/Sample Project/TestBed/StatsViewController.m @@ -0,0 +1,82 @@ +// +// 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]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.standardDeviationLabel.text = self.standardDeviation; + self.averageLabel.text = self.average; + self.medianLabel.text = self.median; + self.modeLabel.text = self.mode; + self.maximumLabel.text = self.maximum; + self.minimumLabel.text = self.minimum; + self.snapshotImageView.image = self.snapshotImage; +} + +- (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 == 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..b372fe4 --- /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) NSUInteger intValue; + +@end diff --git a/Sample Project/TestBed/UITextField+Numbers.m b/Sample Project/TestBed/UITextField+Numbers.m new file mode 100644 index 0000000..8afd1f6 --- /dev/null +++ b/Sample Project/TestBed/UITextField+Numbers.m @@ -0,0 +1,50 @@ +// +// 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 = @"oopsf"; + } else { + self.text = [NSString stringWithFormat:@"%0.1f",num]; + } +} + +- (void)setIntValue:(NSUInteger) num { + if (num == NSNotFound ) { + self.text = @""; + } else if (num == (NSUInteger)-1 ) { + self.text = @"oops"; + }else { + self.text = [NSString stringWithFormat:@"%d",(int)num]; + } +} + +- (CGFloat)floatValue { + if (self.text.length ==0) { + return -1.0; + } else { + return (CGFloat) self.text.floatValue; + } +} + +- (NSUInteger)intValue { + if (self.text.length ==0) { + return NSNotFound; + } else { + return (NSUInteger) 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])); + } +}