Changing a circle's size with UIPinchGestureRecognizer - objective-c

I'm drawing a simple circle in the center of the screen:
int radius = 100;
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
self.circle.fillColor = [UIColor clearColor].CGColor;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
[self.view.layer addSublayer:self.circle];
}
Using the pinch gesture, I allow the user to increase/decrease the radius of the shape:
- (void)scale:(UIPinchGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
if (gestureRecognizer.scale < lastScale) {
--radius;
}
else if (gestureRecognizer.scale > lastScale) {
++radius;
}
// Center the shape in self.view
self.circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius, CGRectGetMidY(self.view.frame)-radius);
self.circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
}
lastScale = gestureRecognizer.scale;
}
However, the circle doesn't stay dead center. Instead, it bounces around the middle and doesn't settle until the gesture finishes.
Does anyone know why this is happening and if so, how I can prevent it?

There are a few problems in your code. As #tc. said, you're not setting the shape layer's frame (or bounds). The default layer size is CGSizeZero, which is why you're having to offset the layer's position by the radius every time you change the radius.
Also, the position and path properties of a shape layer are animatable. So by default, when you change them, Core Animation will animate them to their new values. The path animation is contributing to your unwanted behavior.
Also, you should set the layer's position or frame based on self.view.bounds, not self.view.frame, because the layer's position/frame is the coordinate system of self.view, not the coordinate system of self.view.superview. This will matter if self.view is the top-level view and you support interface autorotation.
I would suggest revising how you're implementing this. Make radius a CGFloat property, and make setting the property update the layer's bounds and path:
#interface ViewController ()
#property (nonatomic, strong) CAShapeLayer *circle;
#property (nonatomic) CGFloat radius;
#end
#implementation ViewController
- (void)setRadius:(CGFloat)radius {
_radius = radius;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
}
If you really want to force the radius to be an integer, I suggest internally tracking it as a float anyway, because the user interaction is smoother if it's a float. Just round it in a temporary variable before creating the CGRect for the bounds and path:
CGFloat intRadius = roundf(radius);
self.circle.bounds = CGRectMake(0, 0, 2 * intRadius, 2 * intRadius);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
In addCircle, just set the radius property and let that setter take care of setting the layer's bounds and path. Also defer setting the layer's position until the system's layout phase. That way, you'll reposition the circle in the center again after an interface rotation.
- (void)viewDidLoad {
[super viewDidLoad];
[self addCircle];
}
- (void)addCircle {
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 5;
self.radius = 100;
[self.view.layer addSublayer:self.circle];
[self.view setNeedsLayout];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.circle.position = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
}
Finally, to handle a pinch gesture, just set the new radius to the old radius times the gesture's scale. The radius setter will take care of updating the layer's path and bounds. Then reset the gesture's scale to 1. This is simpler than tracking the gesture's prior scale. Also, use CATransaction to disable animation of the path property.
- (IBAction)pinchGestureWasRecognized:(UIPinchGestureRecognizer *)recognizer {
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
self.radius *= recognizer.scale;
recognizer.scale = 1;
} [CATransaction commit];
}

Related

Circular UIView with multiple coloured border working like a pie chart

I am trying to set a circular avatar of a player of a game with a piechart representation on the avatar's circular border.
Player 1 -
Wins 25%
Lost 70%
Drawn 5%
cell.selectedPhoto.frame = CGRectMake(cell.selectedPhoto.frame.origin.x, cell.selectedPhoto.frame.origin.y, 75, 75);
cell.selectedPhoto.clipsToBounds = YES;
cell.selectedPhoto.layer.cornerRadius = 75/2.0f;
cell.selectedPhoto.layer.borderColor=[UIColor orangeColor].CGColor;
cell.selectedPhoto.layer.borderWidth=2.5f;
cell.selectedBadge.layer.cornerRadius = 15;
I have the UIImageView as a circle already with a single border colour.
At first guess perhaps I will need to clear the border of my UIImageView and have instead a UIView sitting behind my UIImageView that is a standard piechart, but is there a smarter way of doing this?
Thank you in advance.
I would recommend you create a custom UIView subclass for this, that manages various CALayer objects to create this effect. I was going to set about doing this in Core Graphics, but if you ever want to add some nice animations to this, you'll want to stick with Core Animation.
So let's first define our interface.
/// Provides a simple interface for creating an avatar icon, with a pie-chart style border.
#interface AvatarView : UIView
/// The avatar image, to be displayed in the center.
#property (nonatomic) UIImage* avatarImage;
/// An array of float values to define the values of each portion of the border.
#property (nonatomic) NSArray* borderValues;
/// An array of UIColors to define the colors of the border portions.
#property (nonatomic) NSArray* borderColors;
/// The width of the outer border.
#property (nonatomic) CGFloat borderWidth;
/// Animates the border values from their current values to a new set of values.
-(void) animateToBorderValues:(NSArray*)borderValues duration:(CGFloat)duration;
#end
Here we can set the avatar image, border width, and provide an array of colors and values. Next, lets work on implementing this. First we'll want to define some variables that we'll want to keep track of.
#implementation AvatarView {
CALayer* avatarImageLayer; // the avatar image layer
NSMutableArray* borderLayers; // the array containing the portion border layers
UIBezierPath* borderLayerPath; // the path used to stroke the border layers
CGFloat radius; // the radius of the view
}
Next, lets setup our avatarImageLayer, as well as a couple other variables in the initWithFrame method:
-(instancetype) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
radius = frame.size.width*0.5;
// create border layer array
borderLayers = [NSMutableArray array];
// create avatar image layer
avatarImageLayer = [CALayer layer];
avatarImageLayer.frame = frame;
avatarImageLayer.contentsScale = [UIScreen mainScreen].nativeScale; // scales the layer to the screen scale
[self.layer addSublayer:avatarImageLayer];
}
return self;
}
Next let's define our method that will populate the border layers when the borderValues property updates, allowing the view to have a dynamic number of border layers.
-(void) populateBorderLayers {
while (borderLayers.count > _borderValues.count) { // remove layers if the number of border layers got reduced
[(CAShapeLayer*)[borderLayers lastObject] removeFromSuperlayer];
[borderLayers removeLastObject];
}
NSUInteger colorCount = _borderColors.count;
NSUInteger borderLayerCount = borderLayers.count;
while (borderLayerCount < _borderValues.count) { // add layers if the number of border layers got increased
CAShapeLayer* borderLayer = [CAShapeLayer layer];
borderLayer.path = borderLayerPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = _borderWidth;
borderLayer.strokeColor = (borderLayerCount < colorCount)? ((UIColor*)_borderColors[borderLayerCount]).CGColor : [UIColor clearColor].CGColor;
if (borderLayerCount != 0) { // set pre-animation border stroke positions.
CAShapeLayer* previousLayer = borderLayers[borderLayerCount-1];
borderLayer.strokeStart = previousLayer.strokeEnd;
borderLayer.strokeEnd = previousLayer.strokeEnd;
} else borderLayer.strokeEnd = 0.0; // default value for first layer.
[self.layer insertSublayer:borderLayer atIndex:0]; // not strictly necessary, should work fine with `addSublayer`, but nice to have to ensure the layers don't unexpectedly overlap.
[borderLayers addObject:borderLayer];
borderLayerCount++;
}
}
Next, we want to make a method that can update the layer's stroke start and end values when borderValues gets updated. This could be merged into previous method, but if you want to setup animation you'll want to keep it separate.
-(void) updateBorderStrokeValues {
NSUInteger i = 0;
CGFloat cumulativeValue = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeStart = cumulativeValue;
cumulativeValue += [_borderValues[i] floatValue];
s.strokeEnd = cumulativeValue;
i++;
}
}
Next, we just need to override the setters in order to update certain aspects of the border and avatar image when the values change:
-(void) setAvatarImage:(UIImage *)avatarImage {
_avatarImage = avatarImage;
avatarImageLayer.contents = (id)avatarImage.CGImage; // update contents if image changed
}
-(void) setBorderWidth:(CGFloat)borderWidth {
_borderWidth = borderWidth;
CGFloat halfBorderWidth = borderWidth*0.5; // we're gonna use this a bunch, so might as well pre-calculate
// set the new border layer path
borderLayerPath = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius, radius} radius:radius-halfBorderWidth startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
for (CAShapeLayer* s in borderLayers) { // apply the new border layer path
s.path = borderLayerPath.CGPath;
s.lineWidth = borderWidth;
}
// update avatar masking
CAShapeLayer* s = [CAShapeLayer layer];
avatarImageLayer.frame = CGRectMake(halfBorderWidth, halfBorderWidth, self.frame.size.width-borderWidth, self.frame.size.height-borderWidth); // update avatar image frame
s.path = [UIBezierPath bezierPathWithArcCenter:(CGPoint){radius-halfBorderWidth, radius-halfBorderWidth} radius:radius-borderWidth startAngle:0 endAngle:M_PI*2.0 clockwise:YES].CGPath;
avatarImageLayer.mask = s;
}
-(void) setBorderColors:(NSArray *)borderColors {
_borderColors = borderColors;
NSUInteger i = 0;
for (CAShapeLayer* s in borderLayers) {
s.strokeColor = ((UIColor*)borderColors[i]).CGColor;
i++;
}
}
-(void) setBorderValues:(NSArray *)borderValues {
_borderValues = borderValues;
[self populateBorderLayers];
[self updateBorderStrokeValues];
}
Finally, we can even take one step further by animating the layers! Let's just add a single of method that can handle this for us.
-(void) animateToBorderValues:(NSArray *)borderValues duration:(CGFloat)duration {
_borderValues = borderValues; // update border values
[self populateBorderLayers]; // do a 'soft' layer update, making sure that the correct number of layers are generated pre-animation. Pre-sets stroke positions to a pre-animation state.
// define stroke animation
CABasicAnimation* strokeAnim = [CABasicAnimation animation];
strokeAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
strokeAnim.duration = duration;
CGFloat cumulativeValue = 0;
for (int i = 0; i < borderLayers.count; i++) {
cumulativeValue += [borderValues[i] floatValue];
CAShapeLayer* s = borderLayers[i];
if (i != 0) [s addAnimation:strokeAnim forKey:#"startStrokeAnim"];
// define stroke end animation
strokeAnim.keyPath = #"strokeEnd";
strokeAnim.fromValue = #(s.strokeEnd);
strokeAnim.toValue = #(cumulativeValue);
[s addAnimation:strokeAnim forKey:#"endStrokeAnim"];
strokeAnim.keyPath = #"strokeStart"; // re-use the previous animation, as the values are the same (in the next iteration).
}
// update presentation layer values
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self updateBorderStrokeValues]; // sets stroke positions.
[CATransaction commit];
}
And that's it! Here's an example of the usage:
AvatarView* v = [[AvatarView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
v.avatarImage = [UIImage imageNamed:#"photo.png"];
v.borderWidth = 10;
v.borderColors = #[[UIColor colorWithRed:122.0/255.0 green:108.0/255.0 blue:255.0/255.0 alpha:1],
[UIColor colorWithRed:100.0/255.0 green:241.0/255.0 blue:183.0/255.0 alpha:1],
[UIColor colorWithRed:0 green:222.0/255.0 blue:255.0/255.0 alpha:1]];
// because the border values default to 0, you can add this without even setting the border values initially!
[v animateToBorderValues:#[#(0.4), #(0.35), #(0.25)] duration:2];
Results
Full project: https://github.com/hamishknight/Pie-Chart-Avatar
Actually you can directly create your own layer from CALayer. here is a sample Animation layer from my own project.
AnimationLayer.h
#import <QuartzCore/QuartzCore.h>
#interface AnimationLayer : CALayer
#property (nonatomic,assign ) float percent;
#property (nonatomic, strong) NSArray *percentValues;
#property (nonatomic, strong) NSArray *percentColours;
#end
percentValues are your values for which part is gotten.
it should be #[#(35),#(75),#(100)] for win ratio:%35, loose:%40 and draw:%25.
percentColors are UIColor objects for win, loose and draw.
in `AnimationLayer.m`
#import "AnimationLayer.h"
#import <UIKit/UIKit.h>
#implementation AnimationLayer
#dynamic percent,percentValues,percentColours;
+ (BOOL)needsDisplayForKey:(NSString *)key{
if([key isEqualToString:#"percent"]){
return YES;
}else
return [super needsDisplayForKey:key];
}
- (void)drawInContext:(CGContextRef)ctx
{
CGFloat arcStep = (M_PI *2) / 100 * (1.0-self.percent); // M_PI*2 is equivalent of full cirle
BOOL clockwise = NO;
CGFloat x = CGRectGetWidth(self.bounds) / 2; // circle's center
CGFloat y = CGRectGetHeight(self.bounds) / 2; // circle's center
CGFloat radius = MIN(x, y);
UIGraphicsPushContext(ctx);
// draw colorful circle
CGContextSetLineWidth(ctx, 12);//12 is the width of circle.
CGFloat toDraw = (1-self.percent)*100.0f;
for (CGFloat i = 0; i < toDraw; i++)
{
UIColor *c;
for (int j = 0; j<[self.percentValues count]; j++)
{
if (i <= [self.percentValues[j] intValue]) {
c = self.percentColours[j];
break;
}
}
CGContextSetStrokeColorWithColor(ctx, c.CGColor);
CGFloat startAngle = i * arcStep;
CGFloat endAngle = startAngle + arcStep+0.02;
CGContextAddArc(ctx, x, y, radius-6, startAngle, endAngle, clockwise);//set the radius as radius-(half of your line width.)
CGContextStrokePath(ctx);
}
UIGraphicsPopContext();
}
#end
and in some place where you will use this effect, you should call this like
+(void)addAnimationLayerToView:(UIView *)imageOfPlayer withColors:(NSArray *)colors andValues:(NSArray *)values
{
AnimationLayer *animLayer = [AnimationLayer layer];
animLayer.frame = imageOfPlayer.bounds;
animLayer.percentColours = colors;
animLayer.percentValues = values;
[imageOfPlayer.layer insertSublayer:animLayer atIndex:0];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percent"];
[animation setFromValue:#1];
[animation setToValue:#0];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setDuration:6];
[animLayer addAnimation:animation forKey:#"imageAnimation"];
}

Method to resize CALayer frame on window resize?

I draw a series of images to various CALayer sublayers, then add those sublayers to a superlayer:
- (void)renderImagesFromArray:(NSArray *)array {
CALayer *superLayer = [CALayer layer];
for (id object in array) {
CALayer* subLayer = [CALayer layer];
// Disregard...
NSURL *path = [NSURL fileURLWithPathComponents:#[NSHomeDirectory(), #"Desktop", object]];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:path];
[self positionImage:image layer:subLayer];
subLayer.contents = image;
subLayer.hidden = YES;
[superLayer addSublayer:subLayer];
}
[self.view setLayer:superLayer];
[self.view setWantsLayer:YES];
// Show top layer
CALayer *top = superLayer.sublayers[0];
top.hidden = NO;
}
I then call [self positionImage: layer:] to stretch the CALayer to it's maximum bounds (essentially using the algorithm for the CSS cover property), and position it in the center of the window:
- (void)positionImage:(NSImage *)image layer:(CALayer *)layer{
float imageWidth = image.size.width;
float imageHeight = image.size.height;
float frameWidth = self.view.frame.size.width;
float frameHeight = self.view.frame.size.height;
float aspectRatioFrame = frameWidth/frameHeight;
float aspectRatioImage = imageWidth/imageHeight;
float computedImageWidth;
float computedImageHeight;
float verticalSpace;
float horizontalSpace;
if (aspectRatioImage <= aspectRatioFrame){
computedImageWidth = frameHeight * aspectRatioImage;
computedImageHeight = frameHeight;
verticalSpace = 0;
horizontalSpace = (frameWidth - computedImageWidth)/2;
} else {
computedImageWidth = frameWidth;
computedImageHeight = frameWidth / aspectRatioImage;
horizontalSpace = 0;
verticalSpace = (frameHeight - computedImageHeight)/2;
}
[CATransaction flush];
[CATransaction begin];
CATransaction.disableActions = YES;
layer.frame = CGRectMake(horizontalSpace, verticalSpace, computedImageWidth, computedImageHeight);
[CATransaction commit];
}
This all works fine, except when the window gets resized. I solved this (in a very ugly way) by subclassing NSView, then implementing the only method that was actually called when the window resized, viewWillDraw::
- (void)viewWillDraw{
[super viewWillDraw];
[self redraw];
}
- (void)redraw{
AppDelegate *appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
CALayer *superLayer = self.layer;
NSArray *sublayers = superLayer.sublayers;
NSImage *image;
CALayer *current;
for (CALayer *view in sublayers){
if (!view.isHidden){
current = view;
image = view.contents;
}
}
[appDelegate positionImage:image layer:current];
}
So... what's the right way to do this? viewWillDraw: get's called too many times which means I have to do unnecessary and redundant calculations, and I can't use viewWillStartLiveResize: because I need to constantly keep the image in its correct position. What am I overlooking?
Peter Hosey was right; my original method was clunky, and I shouldn't have been overriding setNeedsDisplayInRect:. I first made sure that I was using an auto layout in my app, then implemented the following:
subLayer.layoutManager = [CAConstraintLayoutManager layoutManager];
subLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
subLayer.contentsGravity = kCAGravityResizeAspect;
Basically, I set the sublayer's autoResizingMask to stretch both horizontally and vertically, and then set contentsGravity to preserve the aspect ratio.
That last variable I found by chance, but it's worth noting that you can only use a few contentsGravity constants if, like in my case, you're setting an NSImage as the layer's contents:
That method creates an image that is suited for use as the contents of a layer and that is supports all of the layer’s gravity modes. By contrast, the NSImage class supports only the kCAGravityResize, kCAGravityResizeAspect, and kCAGravityResizeAspectFill modes.
Always fun when a complicated solution can be simplified to 3 lines of code.

Antialiasing on CAShapeLayers

I have a custom view for a loading indicator which is comprised of a number of CAShapeLayers which I perform various animations on to represent various states. In the init method I create a number of shape layers and add them as sublayers to the view's layer. The issue I'm having is the shapes that I'm drawing have very rough edges and don't seem to be antialiased. I've tried a number of things from similar answers on here but I can't seem to antialias the shapes. I've never had this problem before then drawing directly in drawRect.
Is there anything I'm missing or is there a better way to accomplish what I'm trying to do?
Update: Here's a comparison of how the shapes are being drawn:
On the left I'm drawing in layoutSubviews using this code:
CGFloat lineWidth = 2.5f;
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame)/2);
CGFloat startAngle = - ((float)M_PI / 2);
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *trackPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.trackLayer.path = trackPath.CGPath;
self.trackLayer.lineWidth = lineWidth;
On the right I'm calling a separate method from awakeFromNib:
-(void)awakeFromNib
{
....
self.trackLayer = [self trackShape]
[self.layer addSublayer:self.trackLayer];
}
-(CAShapeLayer*)trackShape
{
float start_angle = 2*M_PI*-M_PI_2;
float end_angle = 2*M_PI*1-M_PI_2;
float minSize = MIN(self.frame.size.width, self.frame.size.height);
float radius = (minSize-_strokeWidth)/2 -4;
CGPoint center = CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:start_angle
endAngle:end_angle
clockwise:YES].CGPath;
circle.strokeColor = self.trackColor.CGColor;
circle.fillColor = nil;
circle.lineWidth = (_strokeWidth == -1.0) ? minSize * _strokeWidthRatio
: _strokeWidth;
circle.rasterizationScale = [[UIScreen mainScreen] scale];
return circle;
}
The picture you provided is exactly what happened to me. I got the chunky circle. In my case, the code was inside the drawRect method. The problem was that I was adding a CAShapeLayer, as a subLayer of my view, every time drawRect was called.
So to anyone who stumbles upon something similar, keep track of the CAShapeLayer you are adding or clear everything before drawing again in the drawRect.

Set size of UIView based on content size

Is it possible to draw something on a UIView, then, after the drawing is completed, resize the view to be the same size as the thing that was previously drawn?
For example, if I draw a circle on a UIView, I would like to crop the UIView to the dimensions of the circle that I just drew on the view.
UPDATE
I am looking into using a CAShapeLayer as a possible solution. Does anyone know how to convert a UIBezierPath to a CAShapeLayer then set the position of the CAShapeLayer?
I have tried:
shapeLayer.path = bezierPath.CGPath;
shapeLayer.position = CGPointMake(0, 0);
but this does not work.
Yes you can do that. Have a look at this example that will answer both your questions.
First of all you need to add a UIView called myView and attach it to an IBOutlet ivar.
Define this global values for demonstration purposes:
#define POSITION CGPointMake(50.0,50.0)
#define SIZE CGSizeMake(100.0,100.0)
Declare two methods, one that will draw a shape in myView and another one that will resize the view to adapt it to the drawn shape:
-(void) circle
{
CAShapeLayer *layerData = [CAShapeLayer layer];
layerData.fillColor = [UIColor greenColor].CGColor;
UIBezierPath * dataPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, SIZE.width, SIZE.height)];
layerData.path = dataPath.CGPath;
layerData.position = CGPointMake(POSITION.x, POSITION.y);
[myView.layer addSublayer:layerData];
}
-(void) resize
{
((CAShapeLayer *)[myView.layer.sublayers objectAtIndex:0]).position = CGPointMake(0.0, 0.0);
myView.frame = CGRectMake(POSITION.x + myView.frame.origin.x , POSITION.y + myView.frame.origin.y, SIZE.width, SIZE.height);
}
Finally, in viewWillAppear: call both methods:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self circle];
[self resize];
}
You can run the same code calling only circle and calling both methods. In both cases the drawn circle will be at the same exact position but in the second case myView has been resized to have the same size as the drawn shape.
Hope it helps.

Scaling MKMapView Annotations relative to the zoom level

The Problem
I'm trying to create a visual radius circle around a annonation, that remains at a fixed size in real terms. Eg. So If i set the radius to 100m, as you zoom out of the Map view the radius circle gets progressively smaller.
I've been able to achieve the scaling, however the radius rect/circle seems to "Jitter" away from the Pin Placemark as the user manipulates the view.
I'm lead to believe this is much easier to achieve on the forthcoming iPhone OS 4, however my application needs to support 3.0.
The Manifestation
Here is a video of the behaviour.
The Implementation
The annotations are added to the Mapview in the usual fashion, and i've used the delegate method on my UIViewController Subclass (MapViewController) to see when the region changes.
-(void)mapView:(MKMapView *)pMapView regionDidChangeAnimated:(BOOL)animated{
//Get the map view
MKCoordinateRegion region;
CGRect rect;
//Scale the annotations
for( id<MKAnnotation> annotation in [[self mapView] annotations] ){
if( [annotation isKindOfClass: [Location class]] && [annotation conformsToProtocol:#protocol(MKAnnotation)] ){
//Approximately 200 m radius
region.span.latitudeDelta = 0.002f;
region.span.longitudeDelta = 0.002f;
region.center = [annotation coordinate];
rect = [[self mapView] convertRegion:region toRectToView: self.mapView];
if( [[[self mapView] viewForAnnotation: annotation] respondsToSelector:#selector(setRadiusFrame:)] ){
[[[self mapView] viewForAnnotation: annotation] setRadiusFrame:rect];
}
}
}
The Annotation object (LocationAnnotationView)is a subclass of the MKAnnotationView and it's setRadiusFrame looks like this
-(void) setRadiusFrame:(CGRect) rect{
CGPoint centerPoint;
//Invert
centerPoint.x = (rect.size.width/2) * -1;
centerPoint.y = 0 + 55 + ((rect.size.height/2) * -1);
rect.origin = centerPoint;
[self.radiusView setFrame:rect];
}
And finally the radiusView object is a subclass of a UIView, that overrides the drawRect method to draw the translucent circles. setFrame is also over ridden in this UIView subclass, but it only serves to call [UIView setNeedsDisplay] in addition to [UIView setFrame:] to ensure that the view is redrawn after the frame has been updated.
The radiusView object's (CircleView) drawRect method looks like this
-(void) drawRect:(CGRect)rect{
//NSLog(#"[CircleView drawRect]");
[self setBackgroundColor:[UIColor clearColor]];
//Declarations
CGContextRef context;
CGMutablePathRef path;
//Assignments
context = UIGraphicsGetCurrentContext();
path = CGPathCreateMutable();
//Alter the rect so the circle isn't cliped
//Calculate the biggest size circle
if( rect.size.height > rect.size.width ){
rect.size.height = rect.size.width;
}
else if( rect.size.height < rect.size.width ){
rect.size.width = rect.size.height;
}
rect.size.height -= 4;
rect.size.width -= 4;
rect.origin.x += 2;
rect.origin.y += 2;
//Create paths
CGPathAddEllipseInRect(path, NULL, rect );
//Create colors
[[self areaColor] setFill];
CGContextAddPath( context, path);
CGContextFillPath( context );
[[self borderColor] setStroke];
CGContextSetLineWidth( context, 2.0f );
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextAddPath(context, path );
CGContextStrokePath( context );
CGPathRelease( path );
//CGContextRestoreGState( context );
}
Thanks for bearing with me, any help is appreciated.
Jonathan
First, what's foo used in the first function? And I'm assuming radiusView's parent is the annotation view, right?
The "Jittering"
Also, the center point of radiusView should coincide with that of the annotationView. This should solve your problem:
-(void) setRadiusFrame:(CGRect)rect{
rect.origin.x -= 0.5*(self.frame.size.width - rect.size.width);
rect.origin.y -= 0.5*(self.frame.size.height - rect.size.height);
[self.radiusView setFrame:rect]
}
Unnecessary method
You could set the frame directly on the radiusView and avoid the above calculation:
UIView * radiusView = [[[self mapView] viewForAnnotation: annotation] radiusView];
rect = [[self mapView] convertRegion:foo toRectToView: radiusView.superView];
[radiusView setFrame:rect];
When drawing the ellipse, don't use the rect passed to drawRect:, it doesn't have to be the same as the frame. It's safer to directly use self.frame
Unnecessary view
Now I gave the above points if you need to use the above hierarchy, but I don't see why don't you just draw your ellipses directly in the LocationAnnotationView? It's there for this purpose after all. This is how you do this:
when scaling, change the annotationView's rect directly:
rect = [[self mapView] convertRegion:foo toRectToView: self.mapView];
[[[self mapView] viewForAnnotation: annotation] setFrame:rect];
Move the implementation of drawRect: to LocationAnnotationView.
This is easier to implement, and should address your problem as the center point of the annotation view moves with your pin and you shouldn't see this problem.
Fixes
There are two other issues in the code:
Set longitudeDelta like this:
region.span.longitudeDelta = 0.002*cos(region.center.latitude*pi/180.0);
as the longitude delta converted to meters changes with the latitude. Alternatively, you could only set latitude delta, then modify the rect so it becomes rectangular (width==height).
in drawRect:, don't use the passed rect; instead use self.frame. There's no guarantee that these are the same, and rect could have any value.
Let me know if these work ;-)