Custom Graphics on UICollectionViewCell - ios

I want to add a grid on each UICollectionViewCell in my UICollectionView. Here is what I tried to do:
- (void) drawRect:(CGRect)rect{
int width = rect.size.width;
int height = rect.size.height;
int i = 0;
[[UIColor blackColor] setStroke];
UIBezierPath* drawingPath = [UIBezierPath bezierPath];
for( i = 0 ; i <= width ; i += width/3) {
[drawingPath moveToPoint:CGPointMake(i, 0)];
[drawingPath addLineToPoint:CGPointMake(i, height)];
NSLog(#"width:%d, height:%d", height, i);
}
for( i = 0 ; i <= height ; i += height/3) {
[drawingPath moveToPoint:CGPointMake(0,i)];
[drawingPath addLineToPoint:CGPointMake(width, i)];
}
[drawingPath stroke];
}
However, I end up with something that looks like this:
The other thing is that I want to color in certain squares in the grid. Is there any simple way of doing that without creating additional subviews? Thanks for all the help!

Related

Unable to scale shapes to match the dimensions of UIView

I have created a custom view that draws shapes of varying sides. The view is added to main view as a subview as given below. The shapes are of different dimensions.
My source code is given below
-(instancetype) initWithSides:(NSUInteger) sides andFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setBackgroundColor:[UIColor clearColor]];
self.sides = sides;
self.radius = CGRectGetWidth(self.frame) * 1.5 / sides);
}
return self;
}
-(void) drawRect:(CGRect) frame {
// Sides is passed as constructor argument. 4 sides means a quadrilateral etc.
// Get CGPAthRef instance
CGFloat angle = DEGREES_TO_RADIANS(360 / ((CGFloat) self.sides));
int count = 0;
CGFloat xCoord = CGRectGetMidX(self.frame);
CGFloat yCoord = CGRectGetMidY(self.frame);
while (count < self.sides) {
CGFloat xPosition =
xCoord + self.radius * cos(angle * ((CGFloat) count));
CGFloat yPosition =
yCoord + self.radius * sin(angle * ((CGFloat) count));
[self.points addObject:[NSValue valueWithCGPoint:CGPointMake(xPosition, yPosition)]];
count ++;
}
NSValue* v = [self.points firstObject];
CGPoint first = v.CGPointValue;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, first.x, first.y);
for (int ix = 1; ix < [self.points count]; ix++) {
NSValue* pValue = [self.points objectAtIndex:ix];
CGPoint p = pValue.CGPointValue;
CGPathAddLineToPoint(path, nil, p.x, p.y);
}
CGPathCloseSubpath(path);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path);
[self colourView:context withPath:path];
}
-(void) colourView:(CGContextRef) context withPath:(CGPathRef) ref {
NSUInteger num = arc4random_uniform(8) + 1;
UIColor* color = nil;
switch (num) {
case 1:
color = [UIColor redColor];
break;
case 2:
color = [UIColor greenColor];
break;
case 3:
color = [UIColor yellowColor];
break;
case 4:
color = [UIColor blueColor];
break;
case 5:
color = [UIColor orangeColor];
break;
case 6:
color = [UIColor brownColor];
break;
case 7:
color = [UIColor purpleColor];
break;
case 8:
color = [UIColor blackColor];
break;
default:
break;
}
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillPath(context);
}
This constitutes one single shape. This is how I am drawing the rest of the views.
-(void) initDrawView {
NSUInteger width = CGRectGetWidth(self.view.bounds) / 8;
NSUInteger height = (CGRectGetHeight(self.view.frame))/ 8;
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleSingleTap:)];
for (int i = 0 ; i < 8; i++) {
CGFloat yCoord = i * height + i * 15;
for (int j = 0; j < 8; j ++) {
int side = 3 + arc4random() % 8;
CGFloat xCoord = j * width;
SimpleRegularPolygonView* view =
[[SimpleRegularPolygonView alloc] initWithSides:side andFrame:CGRectMake(xCoord, yCoord, width, height)];
[view sizeToFit];
view.viewEffectsDelegate = self;
[view setTag: (8 * i + j)];
[self.view addGestureRecognizer:singleFingerTap];
[self.view addSubview:view];
}
}
}
1) I don't know how to make them have the same dimensions. How can I do that? (First image)
2) the images don't scale up to the size of the UIView (Second image).
Your current code makes the radius inversely proportional to the number of sides:
self.radius = CGRectGetWidth(self.frame) * 1.5 / sides;
so the more sides, the smaller the image. A quick fix would be to just make the radius half the frame width:
self.radius = CGRectGetWidth(self.frame) /2;
This will mean shapes with an even number of sides fill the frame width. But those with an odd number of sides will appear to have space to the left. If you want to adjust for that you will need more detailed calculations for the width, and you will also need to move the "centre" of the shape. For an odd number sides, the radius would need to be:
self.radius = CGRectGetWidth(self.frame) /(1 + cos(angle / 2));
and xCoord would need to be:
CGFloat xCoord = CGRectGetMinX(self.frame) + self.radius * cos(angle/2);

Core Graphics, odd behaviour with CGContextSetShadowWithColor blur height

I've been adding shadows to my shapes using CGContextSetShadowWithColor. I'm trying to use the same blur offset height.
However the height of the shadow I'm seeing on different shapes is different. I've no idea why this occurring.
Here's some typical code.
CGSize offset = CGSizeMake(0.1, self.l_shadHeight);
CGContextSetShadowWithColor(context, offset, mauveBlurRadius, mauve.CGColor);
EDIT - FURTHER CODE
+ (NSInteger)setShadowHeight
{
NSInteger retVal = 10;
if (IS_IPHONE_6P)
{
retVal = 7;
}
else if (IS_IPHONE_6)
{
retVal = 7;
}
else if (IS_IPHONE_5 || TARGET_INTERFACE_BUILDER)
{
retVal = 6;
}
else if (IS_IPHONE_4_AND_OLDER || IS_IPHONE)
{
retVal = 6;
}
else if (isiPadPro)
{
retVal = 16;
}
else if (IS_IPAD)
{
retVal = 11;
}
return retVal;
}
//
-(void)setSizingClassValues
{
self.l_shadHeight = [DrawingConstants setShadowHeight];
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
[self setSizingClassValues];
//// General Declarations
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
//// Shadow Declarations
//UIColor* circleShadow = shadowColor2;
UIColor* circleShadow = [shadowColor2 colorWithAlphaComponent: [DrawingConstants SHADOW_ALPHA]];
CGSize circleShadowOffset = CGSizeMake(0.1, self.l_shadHeight);
//CGFloat circleShadowBlurRadius = 5;
CGFloat circleShadowBlurRadius = [DrawingConstants setShadowBlurRadius];
//
//// Frames
CGRect frame = self.bounds;
CGFloat xPadding = 18;
CGFloat YPadding = 19;
CGFloat wPadding = 39;
CGFloat hPadding = 54;
if (isiPadPro || IS_IPAD)
{
if (self.size == sizeSmallerPerformance)
{
//so we don't have to rework autolayout on other screens!
CGFloat fac = 2;
xPadding = xPadding / fac;
YPadding = YPadding / fac;
wPadding = wPadding / fac;
hPadding = hPadding / fac;
}
}
//// Subframes
CGRect group = CGRectMake(CGRectGetMinX(frame) + xPadding,
CGRectGetMinY(frame) + YPadding,
CGRectGetWidth(frame) - wPadding,
CGRectGetHeight(frame) - hPadding);
//
//// Abstracted Attributes
CGFloat circleSurroundStrokeWidth = self.l_borderWidth;
CGFloat tongueLeftStrokeWidth = self.l_strokeWidth;;
CGFloat tongueStrokeWidth = self.l_strokeWidth;
//// Group
{
//// CircleSurround Drawing
UIBezierPath* circleSurroundPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(CGRectGetMinX(group) + floor(CGRectGetWidth(group) * 0.00164) + 0.5, CGRectGetMinY(group) + floor(CGRectGetHeight(group) * 0.00161) + 0.5, floor(CGRectGetWidth(group) * 0.99836) - floor(CGRectGetWidth(group) * 0.00164), floor(CGRectGetHeight(group) * 0.99839) - floor(CGRectGetHeight(group) * 0.00161))];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, circleShadowOffset, circleShadowBlurRadius, circleShadow.CGColor);
[circleFillColour setFill];
[circleSurroundPath fill];
CGContextRestoreGState(context);
[whiteColour setStroke];
circleSurroundPath.lineWidth = circleSurroundStrokeWidth;
[circleSurroundPath stroke];
}
//// Cleanup
CGGradientRelease(innerFaceGradient);
CGColorSpaceRelease(colorSpace);
}
#end
I've found this issue, I had my shadow on the wrong thing. Instead I added it to my outermost group.
Thanks #kurtrevis

How to resize all subviews/sublayers when superview is resized?

This question relates to the project: https://github.com/FindForm/FFAngularPointilism
Currently, all the behavior is dictated by the dimensions of the original UIImage. Despite resizing, shrinking, and rotating, the drawing of the "double triangle squares" remains the same. The triangular blur is unchanged regardless of the bounds of the UIImageView to which it is applied.
In my UIImageView subclass, I initialize with a UIImage and call the following in the designated initializer:
- (void)loadMatrix
{
num = 12;
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
CGFloat theBiggestSideLength = MAX(width, height);
_array = [NSMutableArray array];
_array2 = [NSMutableArray array];
for(int i = 0; i < theBiggestSideLength; i += num)
{
[self.array addObject:[NSMutableArray array]];
[self.array2 addObject:[NSMutableArray array]];
}
for(int i = 0; i < self.array.count; i++)
{
for(int j = 0; j < self.array.count; j++)
{
[self.array[i] addObject:[self getRGBAsFromImage:self.image
atX:j * 2 * num
andY:i * 2 * num]];
int xIndex = ((j * 2 * num) - (num / 2.0));
int yIndex = ((i * 2 * num) + (num / 2.0));
xIndex %= (int)width * 2;
yIndex %= (int)width * 2;
NSLog(#"%d", xIndex);
[self.array2[i] addObject:[self getRGBAsFromImage:self.image
atX:xIndex
andY:yIndex]];
}
}
_imageGrayscaleView = [[UIImageView alloc] initWithImage:[self convertToGreyscale:self.image]];
_finalbwimage = self.imageGrayscaleView.image;
_imageGrayscaleView.frame = self.bounds;
[self insertSubview:_imageGrayscaleView atIndex:0];
_viewMask = [[UIView alloc] initWithFrame:self.frame];
_viewMask.center = CGPointMake(_viewMask.center.x,
_viewMask.center.y + _viewMask.frame.size.height / 2.0f);
_shapeLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectZero;
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
_shapeLayer.path = path;
CGPathRelease(path);
self.imageGrayscaleView.layer.mask = _shapeLayer;
}
To draw these tiles, I add a timer and call the following method each interval. Num and row are updating as expected.:
- (void)drawTile
{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(pixel * num, row * num, num, num)];
view.backgroundColor = [self.array[row] objectAtIndex:pixel];
[self addSubview:view];
[self.ksubviews addObject:view];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(view.frame.origin.x , view.frame.origin.y + (view.frame.size.height))];
[path addLineToPoint:CGPointMake(view.frame.origin.x , view.frame.origin.y)];
[path addLineToPoint:CGPointMake(view.frame.origin.x + view.frame.size.width, view.frame.origin.y + view.frame.size.height)];
[path closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [[UIColor blackColor] CGColor];
shapeLayer.fillColor = [((UIColor *)[self.array2[row] objectAtIndex:pixel]) CGColor];
shapeLayer.lineWidth = 0;
[self.layer addSublayer:shapeLayer];
[self.ksublayers addObject:shapeLayer];
}
The following is the correct behavior:
When this image is resized via the nib, or its bounds set programmatically, the animatable triangular squares are unchanged.
You make all the subviews changes on the method Draw Rect inside your class, and every time you make a change, to the superview, you call the method setNeedDisplay.
I didn't understand you question very well, but i hope this helps!

Is there a more efficient way to draw a chess board than this?

Here's how I'm drawing a chess board in my app. Is there a more efficient or sensible way to do this apart from using a static image?
- (void)drawRect:(CGRect)rect {
int verticalOffset = 40;
int horizontalOffset = 0;
int squareSize = 40;
NSString *nextSquareColour = #"light";
CGContextRef context = UIGraphicsGetCurrentContext();
for (int i = 1; i <= 64; i++) {
// define square position
CGRect square = {horizontalOffset, verticalOffset, squareSize, squareSize};
// set square to be light or dark in colour
if ([nextSquareColour isEqualToString:#"dark"]) {
// dark square
CGContextSetRGBFillColor(context, 0.05, 0.05, 0.05, 0.80);
nextSquareColour = #"light";
} else {
// light square
CGContextSetRGBFillColor(context, 0.95, 0.95, 0.95, 0.80);
nextSquareColour = #"dark";
}
CGContextSetStrokeColorWithColor(context, [UIColor
clearColor].CGColor);
CGContextFillRect(context, square);
CGContextStrokeRect(context, square);
// set up colour and position of next square
horizontalOffset = horizontalOffset + squareSize;
if (i % 8 == 0) {
verticalOffset = verticalOffset + squareSize;
horizontalOffset = 0;
nextSquareColour = #"dark";
}
if (i % 16 == 0) {
nextSquareColour = #"light";
}
} // end for-loop
}
You shouldn't have performance issues unless you cause your view to redraw too many times in a limited time interval. From the look of it (a chess board) that shouldn't happen in your case.
One thing you could avoid is drawing both square colors. You could for instance set the dark color as the background and draw only the light colored squares.
Here is a little class that does it: (Colors, sizes and starting position are settable)
#interface ChessView : UIView
#property (nonatomic, strong) UIColor *lightSquareColor; // default is 0.95f 0.95f 0.95f 0.8f
#property (nonatomic, strong) UIColor *darkSquareColor; // default is 0.05f 0.05f 0.05f 0.8f
#property (nonatomic) CGFloat squareSize; // default is 40.0f
#property (nonatomic) CGPoint boardOrigin; // default is {0.0f, 0.0f}
#end
#implementation ChessView
#synthesize lightSquareColor = _lightSquareColor;
#synthesize darkSquareColor = _darkSquareColor;
#pragma mark - UIView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self baseInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self baseInit];
}
return self;
}
- (void)baseInit
{
self.squareSize = 40.0f;
self.boardOrigin = CGPointZero;
}
- (void)drawRect:(CGRect)rect
{
CGFloat verticalOffset = self.boardOrigin.y;
CGFloat horizontalOffset = self.boardOrigin.x;
CGFloat squareSize = self.squareSize;
CGContextRef context = UIGraphicsGetCurrentContext();
// draw background with dark color
[self.darkSquareColor setFill];
CGContextFillRect(context, CGRectMake(horizontalOffset, verticalOffset, squareSize * 8.0f, squareSize * 8.0f));
// Create a path and add light squares to it
CGMutablePathRef path = CGPathCreateMutable();
for (int i = 1; i <= 32; i++) {
CGRect square = {horizontalOffset, verticalOffset, squareSize, squareSize};
CGPathAddRect(path, NULL, square);
horizontalOffset = horizontalOffset + 2.0f * squareSize;
if (i % 4 == 0) {
verticalOffset = verticalOffset + self.squareSize;
horizontalOffset = i % 8 == 0 ? 0.0f : squareSize;
}
}
[self.lightSquareColor setFill];
CGContextAddPath(context, path);
CGPathRelease(path);
CGContextFillPath(context);
}
#pragma mark - Colors
- (UIColor *)lightSquareColor
{
if (!_lightSquareColor) {
_lightSquareColor = [UIColor colorWithRed:0.95f
green:0.95f
blue:0.95f
alpha:0.8f];
}
return _lightSquareColor;
}
- (UIColor *)darkSquareColor
{
if (!_darkSquareColor) {
_darkSquareColor = [UIColor colorWithRed:0.05f
green:0.05f
blue:0.05f
alpha:0.8f];
}
return _darkSquareColor;
}
- (void)setLightSquareColor:(UIColor *)lightSquareColor
{
if (![_lightSquareColor isEqual:lightSquareColor]) {
_lightSquareColor = lightSquareColor;
[self setNeedsDisplay];
}
}
- (void)setDarkSquareColor:(UIColor *)darkSquareColor
{
if (![_darkSquareColor isEqual:darkSquareColor]) {
_darkSquareColor = darkSquareColor;
[self setNeedsDisplay];
}
}
#pragma mark - Metrics
- (void)setBoardOrigin:(CGPoint)boardOrigin
{
if (!CGPointEqualToPoint(_boardOrigin, boardOrigin)) {
_boardOrigin = boardOrigin;
[self setNeedsDisplay];
}
}
- (void)setSquareSize:(CGFloat)squareSize
{
if (_squareSize != squareSize) {
_squareSize = squareSize;
[self setNeedsDisplay];
}
}
#end
Apart from UI, you are comparing string for so many times. You can avoid it by below way:
for(int row = 0; row < 8; row++)
{
for(int column = 0; column < 8; column++)
{
CGRect square = {horizontalOffset + (column * squareSize),
verticalOffset + (row * squareSize),
squareSize,
squareSize};
if((row + column) % 2 == 0)
CGContextSetRGBFillColor(context, 0.05, 0.05, 0.05, 0.80);
else
CGContextSetRGBFillColor(context, 0.95, 0.95, 0.95, 0.80);
CGContextSetStrokeColorWithColor(context, [UIColor
clearColor].CGColor);
CGContextFillRect(context, square);
CGContextStrokeRect(context, square);
}
}
If you want just square cells filled with solid color you can use UIView or CALayer for each cell. Set appropriate background for each view (layer) and add them to the superview (superlayer). This will consume less memory than drawing the board.
Other option is to use CAReplicatorLayer. But I don't see any benefit here over the first option.
You can use line dashes, to simplify the whole thing. But i didn't check that is more performant.
Something like :
- (void)drawRect:(CGRect)rect {
int verticalOffset = 40;
int horizontalOffset = 0;
int squareSize = 40;
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat dashes[2] = { squareSize, squareSize};
CGRect boardRect = {horizontalOffset, verticalOffset, squareSize * 8, squareSize * 8};
CGFloat halfSquareSize = squareSize * .5;
// Fill board with light color
CGContextSetRGBFillColor(context, 0.95, 0.95, 0.95, 0.80);
CGContextFillRect(context, boardRect);
// Draw dark squares only by using line dashes
CGContextSetRGBFillColor(context, 0.05, 0.05, 0.05, 0.80);
CGContextSetLineWidth(context, squareSize);
for (int i = 0; i < 8; i++)
{
if ((i & 0x1) == 0)
{
CGContextSetLineDash(context, squareSize, dashes, 2);
}
else
{
CGContextSetLineDash(context, 0, dashes, 2);
}
CGContextMoveToPoint(context, horizontalOffset, verticalOffset + i * squareSize + halfSquareSize);
CGContextAddLineToPoint(context, horizontalOffset + 8 * squareSize, verticalOffset + i * squareSize + halfSquareSize);
CGContextDrawPath(context, kCGPathFillStroke);
}
}
Why not just have a predawn image that contains the whole board

Horizantal slider with tick marks on iOS

Can i realise a horizontal slider with tick marks on iOS? There is not such option like in MacOS.
I actually wrote a tutorial on how to do this on my company's website:
http://fragmentlabs.com/blog/making-talking2trees-part-3-77
The quick answer for this is a custom method I wrote, and which is explained in the article above:
-(UIView*)tickMarksViewForSlider:(UISlider*)slider View:(UIView *)view
{
// set up vars
int ticksDivider = (slider.maximumValue > 10) ? 10 : 1;
int ticks = (int) slider.maximumValue / ticksDivider;
int sliderWidth = 364;
float offsetOffset = (ticks < 10) ? 1.7 : 1.1;
offsetOffset = (ticks > 10) ? 0 : offsetOffset;
float offset = sliderWidth / ticks - offsetOffset;
float xPos = 0;
// initialize view to return
view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y+1,
slider.frame.size.width, slider.frame.size.height);
view.backgroundColor = [UIColor clearColor];
// make a UIImageView with tick for each tick in the slider
for (int i=0; i < ticks; i++)
{
if (i == 0) {
xPos += offset+5.25;
}
else
{
UIView *tick = [[UIView alloc] initWithFrame:CGRectMake(xPos, 3, 2, 16)];
tick.backgroundColor = [UIColor colorWithWhite:0.7 alpha:1];
tick.layer.shadowColor = [[UIColor whiteColor] CGColor];
tick.layer.shadowOffset = CGSizeMake(0.0f, 1.0f);
tick.layer.shadowOpacity = 1.0f;
tick.layer.shadowRadius = 0.0f;
[view insertSubview:tick belowSubview:slider];
xPos += offset - 0.4;
}
}
// return the view
return view;
}
You'd basically apply this method to a view that sits behind your UISlider, and it creates the tick marks at the appropriate intervals. You can change how many ticks are visible by modifying the ticksDivider variable. The above example creates one per slider value unless the slider's maximumValue property is greater than 10 (my sliders had values of 5, 40 and 120), in which case I divided the value by 10.
You have to play with the offsetOffset value and the float value after xPos += offset+... to account for the width of your thumb image and slider cap insets (so the thumb button appears to be centered over each).
From #CarlGoldsmith - You'd have to program that yourself; UISliders on iOS don't have that functionality.
While ctlockey's solution certainly works, I wanted something simpler but also that allowed the thumb to snap to whichever tick mark it is closest to when the user releases it.
My solution is below. It is a subclass of UISlider that only overrides two methods. It is bare bones but would be a foundation for a more complex version.
-(void)endTrackingWithTouch:(UITouch *)touch
withEvent:(UIEvent *)event
{
[super endTrackingWithTouch:touch withEvent:event];
if (self.value != floorf(self.value))
{
CGFloat roundedFloat = (float)roundf(self.value);
[self setValue:roundedFloat animated:YES];
}
}
-(void)drawRect:(CGRect)rect
{
CGFloat thumbOffset = self.currentThumbImage.size.width / 2.0f;
NSInteger numberOfTicksToDraw = roundf(self.maximumValue - self.minimumValue) + 1;
CGFloat distMinTickToMax = self.frame.size.width - (2 * thumbOffset);
CGFloat distBetweenTicks = distMinTickToMax / ((float)numberOfTicksToDraw - 1);
CGFloat xTickMarkPosition = thumbOffset; //will change as tick marks are drawn across slider
CGFloat yTickMarkStartingPosition = self.frame.size.height / 2; //will not change
CGFloat yTickMarkEndingPosition = self.frame.size.height; //will not change
UIBezierPath *tickPath = [UIBezierPath bezierPath];
for (int i = 0; i < numberOfTicksToDraw; i++)
{
[tickPath moveToPoint:CGPointMake(xTickMarkPosition, yTickMarkStartingPosition)];
[tickPath addLineToPoint:CGPointMake(xTickMarkPosition, yTickMarkEndingPosition)];
xTickMarkPosition += distBetweenTicks;
}
[tickPath stroke];
}

Resources