Difference drawing on CALayer and UIView - ios

I have one component that has an UIView subclass and a custom CAlayer into it.
In the UIView there is a circle that is drawn with CoreGraphics, and this is the code:
CGRect b = self.bounds;
int strokeSize = 2;
CGRect arcBounds = CGRectMake(b.origin.x+1, b.origin.y+1, b.size.width-2, b.size.height-2);
CGContextSaveGState(ctx); {
CGContextSetLineWidth(ctx, strokeSize);
CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor);
CGContextStrokeEllipseInRect(ctx, arcBounds);
} CGContextRestoreGState(ctx);
when I draw that circle in the drawRect method inside the UIView it works perfect and the circle is drawn smooth and looks great.
The problem appears when I draw another circle just over this one, but the second one is drawn in the CALayer, actually in the drawInContext method of my custom CALayer. Using just the same code the circle doesn't looks good, and have some "pixellation" on the borders.
Any clues on what can be happening? Thanks in advance.

This is due to the contentsScale property. When you have a custom CALayer the default value of this property is 1.0.
The default value of this property is 1.0. For layers attached to a
view, the view changes the scale factor automatically to a value that
is appropriate for the current screen. For layers you create and
manage yourself, you must set the value of this property yourself
based on the resolution of the screen and the content you are
providing. Core Animation uses the value you specify as a cue to
determine how to render your content. Source.
If you have a retina device and you draw with the contentsScale set to 1.0, it will result in that pixelated look you described. In order to fix this you should set the layer's contentsScale to the one of the screen.
[self.layer setContentsScale:[[UIScreen mainScreen] scale]];
This issue does not happen when you draw the circle in the drawRect method of your UIView since there the default contentsScaleFactor is already the one of the screen.
For views that implement a custom drawRect: method and are associated
with a window, the default value for this property is the scale factor
associated with the screen currently displaying the view. Source.

Related

Resizing a UIView but drawing done by drawRect also changing size

I am testing a UIView using a UISlider as in the example images below:
I have a custom UIView with a yellow background that draws the gray square, the drawRect method is like so:
-(void)drawRect:(CGRect)rect{
NSLog(#"Draw rect called");
UIBezierPath* squarePath = [UIBezierPath bezierPathWithRect: CGRectMake(10, 10, 100, 100)];
[UIColor.grayColor setFill];
[squarePath fill];
}
And the method for my slide changing value:
- (IBAction)changeValue:(id)sender {
CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformIdentity, self.slider.value, self.slider.value);
self.tableView.transform = transform;
[self.tableView setNeedsDisplay];
}
I dont understand why the square is getting larger. I've noticed that drawRect is called every time the slider is moved. If this happens then why is the square size changing? Shouldn't it remain the same size and just the frame grow with the square in the top left corner?
My second question is, how would I change the code so just the frame grows and the drawing size stays the same? I ask this because actually I want the drawing size to change dynamically using my own code in drawRect.
Any pointers would be really appreciated! thanks!
The reason why the size of the square changes is because you've transformed it. Transformations don't just affect the frame of a view; they will affect the content. The square is getting drawn into its context at its constant size (100x100) and then the transform is stretching it before it gets rendered.
The reason why it's not expanding to the right and down is because by default the anchor point of a transform is the center of the bounds. Thus it'll scale from the center outwards. From the documentation:
The origin of the transform is the value of the center property ...
Transformations aren't intended to be used to simply scale the width and height of your frame. The frame property is for that. Simply store the view's frame in a variable, change its width and height, then set it back. In your drawRect: code you can check the dimensions of the rectangle that's given to you and make your square's width/height a percentage of that.

iOS drawing in CALayers drawInContext is either pixelated or blurry on retina

I have a custom UIView that draws something in an overwritten
- (void)drawRect:(CGRect)rect
this works fine and gives sharp results on retina screens.
However, now I would like to make the properties on which the drawing is based animatable. Creating animatable properties seems to be possible only on the CALayer, so instead of doing the drawing in UIView, I create a custom CALayer subclass and do the drawing inside
- (void) drawInContext:(CGContextRef)ctx
I use pretty much the exact same drawing code in this function as I used in the drawRect function of the custom UIView.
The result looks the same - however, it's not retina resolution but pixelated (large square pixels)
if I put
self.contentsScale = [UIScreen mainScreen].scale;
To the beginning of my drawInContext implementation, then instead of a pixelated result, I get a blurry result (as if the rendering is still performed in non-retina resolution and then upscaled to retina resolution).
what's the correct way to render sharp retina paths in CALayers drawInContext ?
here are some screenshots (the blue line is part of the custom drawing in question. the yellow part is just an image)
Drawn inside custom UIView's drawRect:
Drawn inside custom CALayer's drawInContext:
Drawin inside custom CALayer's drawInContext, with setting self.contentScale first:
For completeness, here's (a stripped down version of the) drawing code:
//if drawing inside custom UIView sublcass:
- (void)drawRect:(CGRect)rect
{
CGContextRef currenctContext = UIGraphicsGetCurrentContext();
[[UIColor blackColor] set];
CGContextSetLineWidth(currenctContext, _lineWidth);
CGContextSetLineJoin(currenctContext,kCGLineJoinRound);
CGContextMoveToPoint(currenctContext,x1, y1);
CGContextAddLineToPoint(currenctContext,x2, y2);
CGContextStrokePath(currenctContext);
}
//if drawing inside custom CALayer subclass:
- (void) drawInContext:(CGContextRef)ctx {
{
//self.contentsScale = [UIScreen mainScreen].scale;
CGContextRef currenctContext = ctx;
CGContextSetStrokeColorWithColor(currenctContext, [UIColor blackColor].CGColor);
CGContextSetLineWidth(currenctContext, _lineWidth);
CGContextSetLineJoin(currenctContext,kCGLineJoinRound);
CGContextMoveToPoint(currenctContext,x1, y1);
CGContextAddLineToPoint(currenctContext,x2, y2);
CGContextStrokePath(currenctContext);
}
To restate what I want to achieve: I want to achieve the same crisp retina rendering as in the UIView approach, but when rendering in CALayer
The issue is most likely to be contentScale here; be aware that if you're assigning this to a custom view by overriding its layerClass function, the layer's content scale may be reset. There may be some other instances in which this also happens. To be safe, set the content scale only after the layer has been added to a view.
Try assigning the main screen's scale to your custom layer during your custom view's init method. In Swift 3 that looks like this:
layer.contentsScale = UIScreen.mainScreen().scale
Or, in Swift 4:
layer.contentsScale = UIScreen.main.scale
Are you using shouldRasterize = YES in the layer? Try drawing in the CALayer subclass but set the rasterizationScale to the screen's scale.
After adding layer to its superlayer. set shouldRasterize to YES , set contentsScale and resterizatioinScale to screen scale:
[self.progressView.layer addSublayer:self.progressLayer];
self.progressLayer.shouldRasterize = YES;
self.progressLayer.contentsScale = kScreenScale;
self.progressLayer.rasterizationScale = kScreenScale;
CABasicAnimation *animate = [CABasicAnimation animationWithKeyPath:#"progress"];// progress is a customized property of progressLayer
animate.duration = 1.5;
animate.beginTime = 0;
animate.fromValue = #0;
animate.toValue = #1;
animate.fillMode = kCAFillModeForwards;
animate.removedOnCompletion = NO;
animate.repeatCount = HUGE_VALF;
[self.progressLayer addAnimation:animate forKey:#"progressAnimation"];

How to improve performance in rendering image?

I am drawing image on a custom UIView. On resizing the view, the drawing performance goes down and it starts lagging.
My image drawing code is below:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, width, height)];
CGContextAddPath(context, bpath.CGPath);
CGContextClip(context);
CGContextDrawImage(context, [self bounds], image.CGImage);
}
Is this approach correct?
You would be better using Instruments to find where the bottleneck is than asking on here.
However, what you will probably find is that every time the frame changes slightly the entire view will be redrawn.
If you're just using the drawRect to clip the view into an oval (I guess there's an image behind it or something) then you would be better off using a CAShapeLayer.
Create a CAShapeLayer and give it a CGPath then add it as a clipping layer to the view.layer.
Then you can change the path on the CAShapeLayer and it will update. You'll find (I think) that it performs much better too.
If your height and width are the same, you could just use a UIImageView instead of needing a custom view, and get the circular clipping by setting properties on the image view's layer. That approach draws nice and quickly.
Just set up a UIImageView (called "image" in my example) and then have your view controller do this once:
image.layer.cornerRadius = image.size.width / 2.0;
image.layer.masksToBounds = YES;

Drawing outside a UIView

I have a UIView where I would like to draw a Circle that extends past the frame of the UIView,
I have set the masksToBounds to NO - expecting that I can draw past outside the bounds of the UIView by 5 pixels on the right and bottom.
I expect the oval to not get clipped but it does get clipped and does not draw outside the bounds?
- (void)drawRect:(CGRect)rect
{
int width = self.bounds.size.width;
int height = self.bounds.size.height;
self.layer.masksToBounds = NO;
//// Rounded Rectangle Drawing
//// Oval Drawing
UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, width+5, height+5)];
[[UIColor magentaColor] setFill];
[ovalPath fill];
[[UIColor blackColor] setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
From http://developer.apple.com/library/ios/#documentation/general/conceptual/Devpedia-CocoaApp/DrawingModel.html
UIView and NSView automatically configure the drawing environment of a
view before its drawRect: method is invoked. (In the AppKit framework,
configuring the drawing environment is called locking focus.) As part
of this configuration, the view class creates a graphics context for
the current drawing environment.
This graphics context is a Quartz object (CGContext) that contains
information the drawing system requires, such as the colors to apply,
the drawing mode (stroke or fill), line width and style information,
font information, and compositing options. (In the AppKit, an object
of the NSGraphicsContext class wraps a CGContext object.) A graphics
context object is associated with a window, bitmap, PDF file, or other
output device and maintains information about the current state of the
drawing environment for that entity. A view draws using a graphics
context associated with the view’s window. For a view, the graphics
context sets the default clipping region to coincide with the view’s
bounds and puts the default drawing origin at the origin of a view’s
boundaries.
Once the clipping region is set, you can only make it smaller. So, what you're trying to do isn't possible in a UIView drawRect:.
I'm not certain this will fix your problem, but it's something to look into. You're setting self.layer.masksToBounds = NO every single time you enter drawRect. You should try setting it inside the init method just once instead, A) because it's unnecessary to do it multiple times and B) because maybe there's a problem with setting it after drawRect has already been called--who knows.

Fill In UIView With Multiple Colors iOS

I'd like to fill in a UIView Background with multiple colors. I want to use it as a status bar of sorts, so if 1/2 the necessary steps are completed, the UIView Background will be 1/2 green and 1/2 red. When the user completes more steps (say 2/3), more of the UIView Background turns green (2/3 in this case).
I'm guessing I need to override
-(void) drawREct: (CGRect) rect
I imagine I would get the UIView, figure out how big it is, and divide it into 2 rectangles and then fill in those rectangles.
Another option would be to add 2 UIViews programmatically, but I'm a fan of IB.
Is it possible to divy up a UIView like I want?
Thanks
This is not an IB solution, but you can subclass UIView and override the drawRect method to add a custom gradient. This will allow you to put any number of colors you like and have them transition hard or smoothly. There are many nice tutorials online that should different ways to do this (some more elaborate, some quite simple).
Another option that is fairly simple, is to override drawRect, make the background red, then fill a rectangle that takes up the bottom half of the view. It doesn't allow for fancy or smooth transitions between colors, but it's very easy to implement. For instance, something along these lines should work:
-(void)drawRect:(CGRect)rect {
CGRect upperRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height * percentDone);
CGRect lowerRect = CGRectMake(rect.origin.x, rect.origin.y + (rect.size.height * percentDone), rect.size.width, rect.size.height *(1-percentDone));
[[UIColor redColor] set];
UIRectFill(upperRect);
[[UIColor greenColor] set];
UIRectFill(lowerRect);
}
Here percentDone is a float (declare, property(nonatomic), synthesize) that you can tie to the user's steps. Then just update the view when the user does something by
splitView.percentDone = .5;
[splitView setNeedsDisplay];
You can smooth this out with animations as well.
An easy way would be to set the background color for your background view to, say, red. Then add another view to that background view and call that the indicator view. Size and position the indicator view to cover the background, and set its background color to green. Connect the indicator view to an outlet in your view controller, and have the view controller adjust its width (or height) as necessary to correspond with the progress of the task at hand.
Another way would be as #PengOne suggests: create a custom view, give it a 'progress' property, and override -drawRect: to draw the contents appropriately. If you're going this route, there's nothing to stop you from getting a little creative. Instead of just filling two rectangles, make the boundary between the two colors a little more interesting. You could add ripples or bubbles that, with an appropriate sound effect, might look like a container filling with liquid. Or you could do a Qix-like animation that slowly fills the screen... ;-)

Resources