Draw a lot of objects with NSBezierPath - ios

Hi i need draw many complex objects, like in image
i draw square and point in it with this code:
CGFloat mmForSqure = self.frame.size.height/6;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(originX, originY, mmForSqure, mmForSqure)];
CGFloat originPoint = mmForSqure/4;
for (int i = 1; i<4; i++) {
for (int j = 1; j<4; j++) {
CGPoint pathPoint = CGPointMake(i*originPoint+originX, j*originPoint+originY);
[path moveToPoint:pathPoint];
[path addLineToPoint:CGPointMake(i*originPoint+originX+1, j*originPoint+originY+1)];
}
}
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.path = path.CGPath;
shapeLayer.fillColor = [UIColor whiteColor].CGColor;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.lineWidth = 1.0;
[self.layer addSublayer:shapeLayer];
and other squares with two loops
CGFloat sizeOfSquared = self.frame.size.height/6;
for (int i = 0; i<=self.frame.size.width/sizeOfSquared; i++) {
for (int j=0; j!=6; j++) {
}
}
But this is slow UI and take much resources, some one have any idea how optimize resources consumption and UI ??

If u don't need any operations on individual squares, u can try making the lines first(all vertical and 5 horizontal) and then add the dots in a loop using self.frame.size.height/6 divided by 4 as the distance between the points and skip after every 3 points. Just a suggestion. Try it if it works. :)

Related

UIBezierPath Draw On Top of Previous Path?

I am trying to customize EChart, a library for bar graphs on Github for my own purposes. In this file: https://github.com/zhuhuihuihui/EChart/blob/master/EChart/EColumn.m
I am attempting to modify the setGrade: method so that instead of redrawing the entire bar from the bottom of the graph up, it instead will just draw upon the top of the bar to the next desired Y position in the graph. I included a GIF below to show the problem I am having.
This is the code that is drawing this bar (along with a CABasicAnimation below it):
-(void)setGrade:(float)grade {
_grade = grade;
UIBezierPath *progressline = [UIBezierPath bezierPath];
[progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
[progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];
_chartLine.path = progressline.CGPath;
_chartLine.strokeEnd = 1.0;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[_chartLine addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
This code is called from EColumnChart.m (https://github.com/zhuhuihuihui/EChart/blob/master/EChart/EColumnChart.m)
This is the relevant part, the first if just initializes a bar the first time, and the second I customized to just update the value, calling the setGrade: method
if (nil == eColumn) {
eColumn = [[EColumn alloc] initWithFrame:CGRectMake(widthOfTheColumnShouldBe * 0.5 + (i * widthOfTheColumnShouldBe * 1.5), 0, widthOfTheColumnShouldBe, self.frame.size.height)];
eColumn.shouldAnimate = NO;
eColumn.backgroundColor = [UIColor clearColor];
eColumn.grade = eColumnDataModel.value / _fullValueOfTheGraph;
eColumn.eColumnDataModel = eColumnDataModel;
[eColumn setDelegate:self];
[self addSubview:eColumn];
} else {
eColumn.shouldAnimate = YES;
eColumn.grade = eColumnDataModel.value / _fullValueOfTheGraph;
}
[_eColumns setObject:eColumn forKey:[NSNumber numberWithInteger:currentIndex ]];
_fullValueOfTheGraph has its own algorithm to determine how far up the graph the bar should be drawn. The eColumnDataModel.value is just an integer I give it which is the Y value of the bar column.
I am extremely unfamiliar with bezier paths. Is the trick to make the bezier path an ivar and somehow keep track of the top location?
Update:
I have tried to use this code in the beginning part of the setGrade: method however it just creates the little sliver difference between the last update and the current one. I added a prevGrade ivar which is the last grade that was used to update the bar. It is close but it is as if I need to combine the bezier paths in order to get the full bar shape. Any ideas?
UIBezierPath *_progressline = [UIBezierPath bezierPath];
if (_prevGrade != 0.0f) {
if (grade < _prevGrade) { //Bar graph going down
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 + _prevGrade) * self.frame.size.height)];
} else { //Bar graph going up
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 - _prevGrade) * self.frame.size.height)];
}
} else {
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
}
[_progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];
I think that what you need is animate path for CAShapeLayer instead of strokeEnd. I had been testing the code from the repo that you share and I make some changes in order to animate _chartLine.
this is the code of setGrade: method
-(void)setGrade:(float)grade
{
_grade = grade;
if(_chartLine.path == nil)
{
UIBezierPath *_progressline = [UIBezierPath bezierPath];
if (_prevGrade != 0.0f) {
if (grade < _prevGrade) { //Bar graph going down
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 + _prevGrade) * self.frame.size.height)];
} else { //Bar graph going up
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 - _prevGrade) * self.frame.size.height)];
}
} else {
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
}
[_progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];
_chartLine.path = _progressline.CGPath;
_chartLine.strokeEnd = 1.0;
}else
{
CAKeyframeAnimation *morph = [CAKeyframeAnimation animationWithKeyPath:#"path"];
morph.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
morph.values = [self getPathValuesForAnimation:_chartLine startGrade:_prevGrade neededGrade:_grade];
morph.duration = 1;
morph.removedOnCompletion = YES;
morph.fillMode = kCAFillModeForwards;
morph.delegate = self;
[_chartLine addAnimation:morph forKey:#"pathAnimation"];
_chartLine.path = (__bridge CGPathRef _Nullable)((id)[morph.values lastObject]);
}
_prevGrade = grade;
}
//this method return values for path animation
-(NSArray*)getPathValuesForAnimation:(CAShapeLayer*)layer startGrade:(float)currentGrade neededGrade:(float)neededGrade
{
NSMutableArray * values = [NSMutableArray array];
if(_prevGrade != 0.0f)
[values addObject:(id)layer.path];
UIBezierPath * finalPath = [UIBezierPath bezierPath];
[finalPath moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
[finalPath addLineToPoint:CGPointMake(self.frame.size.width/2.0,(1 - neededGrade)*self.frame.size.height)];
[values addObject:(id)[finalPath CGPath]];
return values;
}
I hope this helps you, for me works great, regards
This is how it works for me, (My gif exporter is not good, so all glitch that you see is problem of my gif exporter)
This is with _chartLine.lineCap = kCALineCapButt;
And this is with _chartLine.lineCap = kCALineCapRound;

Unable to remove CAShapeLayer

I have a total of 21 lines drawn across the screen for a measurement tool. The lines are drawn as a layer on UIViewController.view. I am attempting to remove the lines as follows. The NSLog statement confirms that I am getting all 21 CAShapeLayers that I am looking for.
CAShapeLayer* layer = [[CAShapeLayer alloc]init];
for (UIView *subview in viewsToRemove){
if([subview isKindOfClass:[CAShapeLayer class]]){
count++;
NSLog(#"%d: %#", count, subview);
[layerArray addObject:layer];
}
}
for(CAShapeLayer *l in layerArray){
[l removeFromSuperlayer];
}
Any help getting these lines removed would be greatly appreciated.
I don't think it's necessary but if you want to see the code to draw the lines here it is:
for(int i = 0; i < numberOfColumns; i++){
CAShapeLayer *lineShape = nil;
CGMutablePathRef linePath = nil;
linePath = CGPathCreateMutable();
lineShape = [CAShapeLayer layer];
if(i == 0 || i == 20 || i == 10 || i == 3 || i == 17)
lineShape.lineWidth = 4.0f;
else
lineShape.lineWidth = 2.0f;
lineShape.lineCap = kCALineCapRound;
if( i == 0 || i == 20)
lineShape.strokeColor = [[UIColor whiteColor]CGColor];
else if(i == 3 || i == 17)
lineShape.strokeColor = [[UIColor redColor]CGColor];
else if (i == 10)
lineShape.strokeColor = [[UIColor blackColor]CGColor];
else
lineShape.strokeColor = [[UIColor grayColor]CGColor];
x += xIncrement; y = 5;
int toY = screenHeight - self.toolBar.frame.size.height - 10;
CGPathMoveToPoint(linePath, NULL, x, y);
CGPathAddLineToPoint(linePath, NULL, x, toY);
lineShape.path = linePath;
CGPathRelease(linePath);
[self.view.layer addSublayer:lineShape];
Your code makes no sense:
CAShapeLayer* layer = [[CAShapeLayer alloc]init];
for (UIView *subview in viewsToRemove){
if([subview isKindOfClass:[CAShapeLayer class]]){
count++;
NSLog(#"%d: %#", count, subview);
[layerArray addObject:layer];
}
}
for(CAShapeLayer *l in layerArray){
[l removeFromSuperlayer];
}
You are creating a completely new CAShapeLayer and then adding that same CAShapeLayer (namely your layer object) over and over to the layerArray. It is never in the interface, it never has a superlayer, and so removing it from its superlayer does nothing.
Moreover, this line is pointless:
if([subview isKindOfClass:[CAShapeLayer class]]){
A subview will never be a CAShapeLayer. A subview is a UIView. It is not a layer of any kind (though it has a layer).
What you want to is look for the CAShapeLayers that are already in the interface. Of course, an easy way to do this would have been to keep a reference to each CAShapeLayer at the time you created it and put it into the interface in the first place:
[self.view.layer addSublayer:lineShape];
// now store a reference to this layer in an array that you can use later!

iOS Clear UIView and redraw drawRect:

I want my custom view to clear everything (subviews, sublayers, drawings, etc.) in it and call drawRect: again when pressing a button, so it will be like getting a clean slate and painting on it again.
Right now, I've tried using setNeedsDisplay and it just draws over the whole thing without cleaning the previous work.
A snippet of code in my drawRect:
if (_displayYAxisAverageLine) {
// Check if y axis data are numbers
for (id yValue in yAxisValues) {
if (![yValue isKindOfClass:[NSNumber class]]) {
return;
}
}
NSNumber *average = [yAxisValues valueForKeyPath:#"#avg.self"];
CGFloat scale = [self scaleForYValue:average];
CGFloat avgLineY = roundf(graphHeight * scale)+_graphInset;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, avgLineY)];
[path addLineToPoint:CGPointMake(contentWidth, avgLineY)];
CAShapeLayer *avgLine = [CAShapeLayer layer];
avgLine.path = path.CGPath;
avgLine.fillColor = [UIColor clearColor].CGColor;
avgLine.strokeColor = _borderColor.CGColor;
avgLine.lineWidth = 1.f;
avgLine.lineDashPattern = #[#2, #2];
[self.contentView.layer addSublayer:avgLine];
}

skshapenode adding two nodes?

I'm not sure whether there's an issue with the implementation or the way I'm using it.
However, it's presenting over 2,400 nodes when it should be ~ 1,250
-(void)drawWeb {
//get distance of 50 across
int distanceMargin = _background.frame.size.width/50;
NSLog(#"%i", distanceMargin);
__block int xCounter = distanceMargin;
__block int yCounter = 0;
NSArray *alphabet = [[NSArray alloc] initWithObjects:#"A",#"B",#"C",#"D",#"E",#"F",#"G",#"H",#"I",#"J",#"K",#"L",#"M",#"N",#"O",#"P",#"Q",#"R",#"S",#"T",#"U",#"V",#"W",#"X",#"Y",#"Z", nil];
for (int i = 0; i < 25; i++) {
for (int j = 0; j < 50; j++) {
webPoint *shape = [webPoint shapeNodeWithCircleOfRadius:1];
shape.position = CGPointMake(xCounter, _background.frame.size.height - yCounter);
shape.fillColor = [UIColor blackColor];
shape.alpha = 1;
shape.webPoint = [NSString stringWithFormat:#"%#%i", alphabet[i], j];
shape.positionX = xCounter;
shape.positionY = yCounter;
shape.name = #"webPoint";
[_background addChild:shape];
xCounter = xCounter + distanceMargin;
}
xCounter = distanceMargin;
yCounter = yCounter + distanceMargin;
}
}
By default when creating SKShapeNode, strokeColor is already set and it requires 1 node + 1 draw call. In your example you are setting fillColor too, which requires additional node and additional draw pass.
SKShapeNode is not performant solution in many cases (it should be used sparingly), and in your case, if you enable debugging labels you will probably see that there are a lot of draw calls required for scene rendering. Debugging labels can be enabled in view controller (showsDrawCount = YES)
Draw calls are directly affecting on performance in SpriteKit applications and you should try to keep that number as low as possible. One way would be using SKSpriteNode and texture atlases.

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!

Resources