iOS - Cant change view origin - ios

I am plowing through the Big Nerd Ranch Guide 4th Edition, but in the chapter dealing with Views and View Hierarchy I am experiencing some problems with view origins.
I have added a custom subview to my view controller and overwritten the drawRect to draw a bunch of circles in the middle of the screen, however, the origin of the circles won't change and they all get stuck up in the left corner, and I have no idea why. I have literally just copied the code from the book, where it seems to work just fine.
This is my drawRect:
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
//Figure out the center of the bounds rectangle
CGPoint center;
center.x = bounds.origin.x + bounds.size.width/2.0;
center.y = bounds.origin.y + bounds.size.height/2.0;
//The largest circle will circumscribe the view
float maxRadius = hypotf(bounds.size.width, bounds.size.height)/2.0;
UIBezierPath *path = [[UIBezierPath alloc]init];
//Loops and create multiple circles with different radii
for (float currentRadius = maxRadius; currentRadius > 0 ; currentRadius -= 20) {
[path moveToPoint:CGPointMake(center.x +currentRadius, center.y)];
[path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI*2.0 clockwise:YES];
}
path.lineWidth = 10;
[self.circleColor setStroke];
[path stroke];
}
And here is the outcome. A bunch of circles NOT in the center of the screen...

I would do a few things.
Find out where your view's origin is by creating another view, smaller, bright blue or anything, and say centerView.center = biggerView.center. Of course, hook it up.
Depending on your results, move your frames/views appropriately.
Is autolayout working? Is it defined right? Try turning it off for this one.
Try using self.center.x and y instead of making another center.x/y.
Actually do this first. Log or breakpoint bounds and make sure it's not 0 for width. You can print it with NSStringFromCGRect()

Related

Confusing lag situation - UIBezierPath re-draw going too slaw

I've got a "SLATE2" tablet that allows me to write on a tablet with a special pen and interact with my own app. I'm having some trouble though, and I don't think it's a problem with hardware.
- (void)touchesBegan: (CGPoint)point
{
[path moveToPoint:point];
}
- (void)touchesMoved: (CGPoint)point
{
[path addLineToPoint: point]; // (4)
[Newpath appendPath: path];
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[self setNeedsDisplay];
}
- (void)touchesEnded: (CGPoint)point
{
pathTwo = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPathWithCGPath:Newpath.CGPath];
pathTwo = [pathTwo fitInto: self.bounds];
[path removeAllPoints];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] setStroke];
[pathTwo stroke];
return;
}
The drawing to the UIView screen is far too slow. I'm looking at the console and the events being raised by the actual tablet are at almost lightspeed. If there is a better way to draw on this UIView faster please show me.
I've dscovered the issue. fitInto is called so frequently and pathTwo is re-drawn every time. I need fitInto to be called everytime theres a new part of the pad that's drawn on to scale how much is shown.
I tried turning off anti-aliasing but that didn't work.
fitInto works like so...
imagine the screen of the iphone
and if you were to draw on a tablet, a square tablet, at the bottom, say a circle, if you were to draw a circle, it would just zoom in on the iphone screen as its the only drawing on the tablet. if you draw at the top of the tablet, the circle at the bottom is now shown to scale of the entire tablet because of the two opposing ends. i don't think this function could be made any faster... but let's try!
func fit(into:CGRect) -> Self {
let bounds = self.cgPath.boundingBox
let sw = into.size.width/bounds.width
let sh = into.size.height/bounds.height
let factor = min (5, min(sw, max(sh, 0.0)))
return scale(x: factor, y: factor, into: into)
}
and here is scale
func scale(x:CGFloat, y:CGFloat, into: CGRect? = nil) -> Self{
var transform = CGAffineTransform(scaleX: x, y: y)
if into != nil {
transform = transform.concatenating(CGAffineTransform(translationX: into!.midX - self.cgPath.boundingBox.midX, y: into!.midY - self.cgPath.boundingBox.midY))
}
let _ = applyCentered(transform: transform)
return self
}
Have you checked how many points are actually in your path? I don't think you want to draw your CGPath pixel by pixel. Instead you should only add a segment when the distance exceeds a certain threshold. You can also simplify the path by drawing curves through the points instead of drawing line segments. See this primer on bezier curves.
EDIT: The Ray Wenderlich sample app looks like its actually doing the same thing, adding points in TouchedMoved. However in touchesEnded they rasterize the curve into a bitmap. I'm not so sure that that will help your case though because it looks like you are deleting all of the points in touchesEnded. I would still look at their code and see if you have any other points that could be a bottle neck.
What effect are you after? Are you trying to let the user trace lines and curves with the pen, or dots if they tap?
The simple fact is that iOS is not fast enough to track the user's pen/finger strokes point-by-point. Instead, you want to collect a series of points and create a UIBezierPath that connects those points with line segments. That will give you something pretty close to what you want. However, if the user traces fast, that will lag behind as well.

Attempting to mask a circle around an image not working

I have an image that I am attempting to mask a circle around so the image appears round. This somewhat works but the circle comes to a point on the top and bottom.
profileImageView.layer.cornerRadius = profileImageView.frame.size.width/2;
profileImageView.layer.masksToBounds = YES;
Should this code be drawing a perfect circle? It seems to draw a circle in one place but in two other places, its not working correctly.
I have had the best results masking the image view with a CAShapeLayer:
CGFloat radius = self.profileImageView.frame.size.width / 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 0;
self.profileImageView.layer.mask = layer;
Should this code be drawing a perfect circle?
Not necessarily. After all, the width and the height of this layer might not be the same. And even if they are, dividing by 2 might not give you a radius that fits perfectly into an integral number of points as they are mapped to pixels on the screen.
It really would be better, if what you want is a mask that's a circle, to give this layer an actual mask that is an actual circle. Misusing the corner radius as you are doing is just lazy (and, as you've discovered, it's error-prone).

UIBezierPath & CAShapeLayer initial animation jump

I built this for my company: https://github.com/busycm/BZYStrokeTimer and during the course of building, I noticed an interesting "bug" that I can't seem to mitigate when using UIBezierPath. Right when the animation starts, the path jumps a certain number of pixels forward (or backwards depending if it's counterclockwise) instead of starting up with a smooth, incremental animation. And what I found that's really interesting is how much the path jumps forward is actually the value of the line width for the CAShaperLayer.
So for example, if my bezier path starts off at CGRectGetMidX(self.bounds) and the line with is 35, the animation actually starts from CGRectGetMidX(self.bounds)+35 and the larger the line width, the more noticeable the jump is. Is there any way to get rid of that so that path will smoothly animate out from the start point?
Here's a picture of the first frame. This is what it looks like immediately after the animation starts.
Then when I resume the animation and pause again, the distance moved is about 1/100th of the distance you see in the picture.
Here's my bezier path code:
- (UIBezierPath *)generatePathWithXInset:(CGFloat)dx withYInset:(CGFloat)dy clockWise:(BOOL)clockwise{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, dy/2)];
[path closePath];
return clockwise ? path : [path bezierPathByReversingPath];
}
Here's the animation code:
CABasicAnimation *wind = [self generateAnimationWithDuration:self.duration == 0 ? kDefaultDuration : self.duration fromValue:#(self.shapeLayer.strokeStart) toValue:#(self.shapeLayer.strokeEnd) withKeypath:keypath withFillMode:kCAFillModeForwards];
wind.timingFunction = [CAMediaTimingFunction functionWithName:self.timingFunction];
wind.removedOnCompletion = NO;
self.shapeLayer.path = [self generatePathWithXInset:self.lineWidth withYInset:self.lineWidth clockWise:self.clockwise].CGPath;
[self.shapeLayer addAnimation:wind forKey:#"strokeEndAnimation"];
And here's how I construct the CAShapeLayer.
- (CAShapeLayer *)shapeLayer {
return !_shapeLayer ? _shapeLayer = ({
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = kDefaultLineWidth;
layer.fillColor = UIColor.clearColor.CGColor;
layer.strokeColor = [UIColor blackColor].CGColor;
layer.lineCap = kCALineCapSquare;
layer.frame = self.bounds;
layer.strokeStart = 0;
layer.strokeEnd = 1;
layer;
}) : _shapeLayer;
}
I think what's happening here is that, in this frame of the animation, you are drawing a line that consists of a single point. Since the line has a thickness associated with it, and the line cap type is kCALineCapSquare, that'll get rendered as a square with height and width equal to the line width.
You can think of it as if you are drawing a line with a square marker, and you are going to drag the midpoint of the marker so that it goes through every point in the curve you specified. For the first point in the line, it's as if the marker touches down at that point, leaving a square behind.
Here's a visual representation the different line cap types that will hopefully make it more intuitive. You should probably change the line cap style to kCALineCapButt.
Sidenote:
After you make that change, in this line of code
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
you probably don't have to offset the x coordinate by dx/2 anymore.

iOS graphics appearing from upper left corner on custom control

I have been making a circular control and i am doing fine, except that the graphics appears from upper left corner when i do the render first time.
The whole control is subclassed UIControl, with custom CALayer which does rendering of a circle.
This is the code that renders the circle:
- (void) drawInContext:(CGContextRef)ctx{
id modelLayer = [self modelLayer];
CGFloat radius = [[modelLayer valueForKey:DXCircularControlLayerPropertyNameRadius] floatValue];
CGFloat lineWidth = [[modelLayer valueForKey:DXCircularControlLayerPropertyNameLineWidth] floatValue];
//NSLog(#"%s, angle: %6.2f, radius: %6.2f, angle_m: %6.2f, radius_m: %6.2f", __func__, self.circleAngle, self.circleRadius, [[modelLayer valueForKey:#"circleAngle"] floatValue], [[modelLayer valueForKey:#"circleRadius"] floatValue]);
// draw circle arc up to target angle
CGRect frame = self.frame;
CGContextRef context = ctx;
CGContextSetShouldAntialias(context, YES);
CGContextSetAllowsAntialiasing(context, YES);
// draw thin circle
//CGContextSetLineWidth(context, <#CGFloat width#>)
// draw selection circle
CGContextSetLineWidth(context, lineWidth);
CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
CGContextAddArc(context, frame.size.width / 2.0f, frame.size.height / 2.0f, radius, 0.0f, self.circleAngle, 0);
CGContextStrokePath(context);
CGContextSetShouldAntialias(context, NO);
}
Here is the video of a problem.
If you watch carefully, you'll notice that rendering of circle somehow doesnt start centered. It skews itself from the upper left corner.
This only happens when doing the animation for the first time.
I know this can happen if one mistakes begin and end of animation commit blocks, but i dont use them here.
Just in case here is the link to the bitbucket repo of this control:
There is nothing wrong with the drawing method - you messed up a little bit setting the layer's frame ;-)
You are setting the frame for the circularControlLayer within your - (void) setAngle:(CGFloat)angle method. That means the frame is set for the first time when you animate the angle property - so the frame will be animated too. Set the frame within the - (void) commonInitDXCircularControlView method.
If you are creating custom layers, have a look at the [UIView layerClass] method. Using it will save you from trouble with bounds/frame management.

Circular, 3 color gradient, animating progress view

I have been searching the web for a library\solution how to achieve this.
I need to make a circular progress bar.
The progress bar should be built from up to 3 colors, depending of the progress value.
I have found several circular progress bars but I'm having trouble modifying them to the required result.
It's supposed to look sort of like the following image, only with a value label in the centre.
Circular gradient
When setting the progress, it should animate to that point.
Each color is up to a certain progress and then it should change with gradient to the next one.
Any ideas on how to achieve this?
I use a progress HUD from github here. It has an annular progress display.
I used the drawing code from this and had a play making a few adjustments. The following code will draw you an annular ring with each part of the ring using a color based on the progress. Code assumes the progress is in self.progress.
This code draws an annular ring in steps of 0.05 progress. So that gives you 20 color changes possible. Reduce this to show more and increase to show less. This code changes the ring color from red at 0.0 progress to green at 1.0 progress as a demonstration. Just change the code which sets the color to an algorithm of your choosing, table lookup etc...
It works when I do the changes inside the HUD linked to above. Just put this function in the view you want to draw the annular ring in.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat lineWidth = 5.f;
CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *processPath = [UIBezierPath bezierPath];
processPath.lineCapStyle = kCGLineCapRound;
processPath.lineWidth = lineWidth;
endAngle = (self.progress * 2 * (float)M_PI) + startAngle;
CGFloat tmpStartAngle=startAngle;
CGFloat shownProgressStep=0.05; // The size of progress steps to take when rendering progress colors.
for (CGFloat shownProgress=0.0;shownProgress <= self.progress ;shownProgress+=shownProgressStep){
endAngle=(shownProgress * 2 *(float)M_PI) + startAngle;
CGFloat rval=shownProgress;
CGFloat gval=(1.0-shownProgress);
UIColor *progressColor=[UIColor colorWithRed:rval green:gval blue:0.0 alpha:1.0];
[progressColor set];
[processPath addArcWithCenter:center radius:radius startAngle:tmpStartAngle endAngle:endAngle clockwise:YES];
[processPath stroke];
[processPath removeAllPoints];
tmpStartAngle=endAngle;
}
}
Most probably you will have to go all the way down to CoreGraphics. You can find some instructions on how to that here:
Draw segments from a circle or donut

Resources