Wrong positioning of subviews - objective-c

I want to arrange several custom NSViews one after the other.
But when I run the App, views are drawn with different (doubled) frame origin values, than are values set in code.
Here is simplified code with 2 views:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
float height1 = 40.0;
float height2 = 65.0;
float width = [[window contentView] frame].size.width;
NSRect r1 = NSMakeRect(0, 0, width, height1);
NSRect r2 = NSMakeRect(0, height1, width, height2);
MyView *c1 = [[MyView alloc] initWithFrame:r1];
MyView *c2 = [[MyView alloc] initWithFrame:r2];
[[window contentView] addSubview:c1];
[[window contentView] addSubview:c2];
}
code for MyView basically consists only from drawRect:
- (void)drawRect:(NSRect)dirtyRect {
// Drawing code here.
NSRect cellFrame = [self frame];
// frame Y coordinates at superview
float superY = [self convertPoint:[self frame].origin
toView:[self superview]].y;
NSLog(#"superY:%f selfY:%f", superY, cellFrame.origin.y);
// top, bottom border and diagonal line of [self frame]
NSBezierPath* borderLine = [NSBezierPath bezierPath];
NSPoint pt1 = NSMakePoint(cellFrame.origin.x,
cellFrame.origin.y);
NSPoint pt2 = NSMakePoint(cellFrame.origin.x + cellFrame.size.width,
cellFrame.origin.y);
NSPoint pt3 = NSMakePoint(cellFrame.origin.x,
cellFrame.origin.y + cellFrame.size.height);
NSPoint pt4 = NSMakePoint(cellFrame.origin.x + cellFrame.size.width,
cellFrame.origin.y + cellFrame.size.height);
[borderLine moveToPoint:pt1];
[borderLine lineToPoint:pt2];
[borderLine lineToPoint:pt3];
[borderLine lineToPoint:pt4];
[[NSColor redColor] setStroke];
[borderLine setLineWidth:01];
[borderLine stroke];
}
and here is the result (as you can see - 'y' coordinate of second view is doubled and for some reason, this view is only partly drawn):
result with console

You are mixing up the concept of a view's frame and bounds rectangles. "Bounds" refer's to a view's dimensions in its own coordinate system, i.e. the origin will be zero and the size will be the view's width and height.
"Frame" refers to a view's dimensions in it's parent view's coordinate system, i.e. the origin will be wherever the view is positioned in its superview, and the width and height will be the same as the bounds rectangle's.
So for the logging in your example code, you are calling "convertPoint" unnecesarily and incorrectly, because you can get the view's actual origin simply by calling "[self frame].origin"
When doing drawing, you need to call "[self bounds]" to get the rectangle in which to draw. In your code you are calling "[self frame]" which gives you a rectangle in your superview's coordinate system (frame), but that won't work because the drawing routines draw in the view's own (bounds) coordinate system (i.e. with origin at {0, 0})
An exception to this would be if a view fills the entire content of its superview, in which case you could call either [self bounds] or [self frame], since both would return the same rectangle.
I got your code to work by changing
NSRect cellFrame = [self frame];
to
NSRect cellFrame = [self bounds];
Also, the easiest way to log an NSRect is
NSLog(#"%#", NSStringFromRect([self frame])); for example.
Hope that helps.

Related

Rotation around different anchorPoint and position makes image jump to new position first

I'm doing a rotation animation on a view and want it to rotate around the view's center X and bottom Y. I change the anchorPoint and position of the layer and run the animation. Here's my code:
- (void)viewDidLoad {
[super viewDidLoad];
_imageView = [UIImageView newAutoLayoutView];
_imageView.image = [PCImage imageNamed:#"Umbrella"];
[self.view addSubview:_imageView];
[_imageView autoAlignAxisToSuperviewAxis:ALAxisVertical];
[_imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CGPoint newPosition = CGPointMake(CGRectGetMidX(_imageView.frame), CGRectGetMaxY(_imageView.frame));
NSLog(#"frame %#, new position %#", NSStringFromCGRect(_imageView.frame), NSStringFromCGPoint(newPosition));
_imageView.layer.anchorPoint = CGPointMake(.5, 1.0);
_imageView.layer.position = newPosition;
[UIView animateKeyframesWithDuration:2.0 delay:2.0 options:UIViewKeyframeAnimationOptionCalculationModeLinear | UIViewKeyframeAnimationOptionAutoreverse | UIViewKeyframeAnimationOptionRepeat animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:.1 animations:^{
_imageView.transform = CGAffineTransformMakeRotation(M_PI / 64);
}];
} completion:nil];
}
edit
The rotation is working, but the view 'jumps' upward to a new position first, where the view's bottom is now where the view's center Y was when first laid out. I thought changing the anchorPoint and updating the position would prevent the jumping. The view is pinned to the superview's bottom edge, and center X to the superview's center X in autolayout, if that might matter. Any ideas?
edit2
I've read other good posts on this like the following but I must be missing something..
Scale UIView with the top center as the anchor point?
I ended up changing the view to not use autolayout after reading this post:
Adjust anchor point of CALayer when autolayout is used
Looks like transforms and autolayout aren't designed to work well together.
_imageView = [UIImageView new];
_imageView.image = [PCImage imageNamed:#"Umbrella"];
_imageView.frame = CGRectMake(0, kScreenHeight - _imageView.image.size.height, _imageView.image.size.width, _imageView.image.size.height);
[self.view addSubview:_imageView];
At some point hoping to experiment with other ideas in that post.

Why does adding a top border to a subview does not resize with auto layout/device rotation?

(using Xcode 6 and iOS 8)
How can I create a top border in a sub UIView that sizes correctly when the view changes due to auto layout or device rotation?
I have code draws a line as the top border but it uses the size of what I have in Interface Builder. It does not size correctly after auto layout resizes the view nor after device rotation.
In the UIViewController, I tried [self.view layoutSubviews], [self.view layoutIfNeeded], [self.myCustomView layoutIfNeeded], and so on, in following method because when this method is called, the view has been resized by auto layout. But doesn't change the size of the line.
UIViewController:
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.myCustomView layoutIfNeeded];
}
Hierarchy is:
UIViewController -> ViewController's View -> my custom UIView
Tried 2 different methods to draw the border which do draw the lines in my-custom-uiview. Neither one is recalled after the view is resized by auto layout or device rotation. This really shouldn't be that hard!
myCustomUIView:
First method:
// in initWithCoder, initWithFrame, awakeFromNib methods
CALayer *border = [CALayer layer];
border.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width, 2.0f);
border.backgroundColor = [UIColor blackColor].CGColor;
[self.contentView.layer addSublayer:border];
Second method:
- (void) drawRect:(CGRect)rect {
UIBezierPath *line = [UIBezierPath bezierPath];
[line moveToPoint:CGPointMake(0, 0)];
[line addLineToPoint:CGPointMake(self.bounds.size.width, 0)];
[line setLineWidth:2.0f];
[[UIColor blackColor] setStroke];
[line stroke];
}

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.

UIView frame, bounds and center

I would like to know how to use these properties in the right manner.
As I understand, frame can be used from the container of the view I am creating.
It sets the view position relative to the container view. It also sets the size of that view.
Also center can be used from the container of the view I'm creating. This property changes the position of the view relative to its container.
Finally, bounds is relative to the view itself. It changes the drawable area for the view.
Can you give more info about the relationship between frame and bounds? What about the clipsToBounds and masksToBounds properties?
Since the question I asked has been seen many times I will provide a detailed answer of it. Feel free to modify it if you want to add more correct content.
First a recap on the question: frame, bounds and center and theirs relationships.
Frame A view's frame (CGRect) is the position of its rectangle in the superview's coordinate system. By default it starts at the top left.
Bounds A view's bounds (CGRect) expresses a view rectangle in its own coordinate system.
Center A center is a CGPoint expressed in terms of the superview's coordinate system and it determines the position of the exact center point of the view.
Taken from UIView + position these are the relationships (they don't work in code since they are informal equations) among the previous properties:
frame.origin = center - (bounds.size / 2.0)
center = frame.origin + (bounds.size / 2.0)
frame.size = bounds.size
NOTE: These relationships do not apply if views are rotated. For further info, I will suggest you take a look at the following image taken from The Kitchen Drawer based on Stanford CS193p course. Credits goes to #Rhubarb.
Using the frame allows you to reposition and/or resize a view within its superview. Usually can be used from a superview, for example, when you create a specific subview. For example:
// view1 will be positioned at x = 30, y = 20 starting the top left corner of [self view]
// [self view] could be the view managed by a UIViewController
UIView* view1 = [[UIView alloc] initWithFrame:CGRectMake(30.0f, 20.0f, 400.0f, 400.0f)];
view1.backgroundColor = [UIColor redColor];
[[self view] addSubview:view1];
When you need the coordinates to drawing inside a view you usually refer to bounds. A typical example could be to draw within a view a subview as an inset of the first. Drawing the subview requires to know the bounds of the superview. For example:
UIView* view1 = [[UIView alloc] initWithFrame:CGRectMake(50.0f, 50.0f, 400.0f, 400.0f)];
view1.backgroundColor = [UIColor redColor];
UIView* view2 = [[UIView alloc] initWithFrame:CGRectInset(view1.bounds, 20.0f, 20.0f)];
view2.backgroundColor = [UIColor yellowColor];
[view1 addSubview:view2];
Different behaviours happen when you change the bounds of a view.
For example, if you change the bounds size, the frame changes (and vice versa). The change happens around the center of the view. Use the code below and see what happens:
NSLog(#"Old Frame %#", NSStringFromCGRect(view2.frame));
NSLog(#"Old Center %#", NSStringFromCGPoint(view2.center));
CGRect frame = view2.bounds;
frame.size.height += 20.0f;
frame.size.width += 20.0f;
view2.bounds = frame;
NSLog(#"New Frame %#", NSStringFromCGRect(view2.frame));
NSLog(#"New Center %#", NSStringFromCGPoint(view2.center));
Furthermore, if you change bounds origin you change the origin of its internal coordinate system. By default the origin is at (0.0, 0.0) (top left corner). For example, if you change the origin for view1 you can see (comment the previous code if you want) that now the top left corner for view2 touches the view1 one. The motivation is quite simple. You say to view1 that its top left corner now is at the position (20.0, 20.0) but since view2's frame origin starts from (20.0, 20.0), they will coincide.
CGRect frame = view1.bounds;
frame.origin.x += 20.0f;
frame.origin.y += 20.0f;
view1.bounds = frame;
The origin represents the view's position within its superview but describes the position of the bounds center.
Finally, bounds and origin are not related concepts. Both allow to derive the frame of a view (See previous equations).
View1's case study
Here is what happens when using the following snippet.
UIView* view1 = [[UIView alloc] initWithFrame:CGRectMake(30.0f, 20.0f, 400.0f, 400.0f)];
view1.backgroundColor = [UIColor redColor];
[[self view] addSubview:view1];
NSLog(#"view1's frame is: %#", NSStringFromCGRect([view1 frame]));
NSLog(#"view1's bounds is: %#", NSStringFromCGRect([view1 bounds]));
NSLog(#"view1's center is: %#", NSStringFromCGPoint([view1 center]));
The relative image.
This instead what happens if I change [self view] bounds like the following.
// previous code here...
CGRect rect = [[self view] bounds];
rect.origin.x += 30.0f;
rect.origin.y += 20.0f;
[[self view] setBounds:rect];
The relative image.
Here you say to [self view] that its top left corner now is at the position (30.0, 20.0) but since view1's frame origin starts from (30.0, 20.0), they will coincide.
Additional references (to update with other references if you want)
UIView Geometry
UIView Frames and Bounds
About clipsToBounds (source Apple doc)
Setting this value to YES causes subviews to be clipped to the bounds
of the receiver. If set to NO, subviews whose frames extend beyond the
visible bounds of the receiver are not clipped. The default value is
NO.
In other words, if a view's frame is (0, 0, 100, 100) and its subview is (90, 90, 30, 30), you will see only a part of that subview. The latter won't exceed the bounds of the parent view.
masksToBounds is equivalent to clipsToBounds. Instead to a UIView, this property is applied to a CALayer. Under the hood, clipsToBounds calls masksToBounds. For further references take a look to How is the relation between UIView's clipsToBounds and CALayer's masksToBounds?.
This question already has a good answer, but I want to supplement it with some more pictures. My full answer is here.
To help me remember frame, I think of a picture frame on a wall. Just like a picture can be moved anywhere on the wall, the coordinate system of a view's frame is the superview. (wall=superview, frame=view)
To help me remember bounds, I think of the bounds of a basketball court. The basketball is somewhere within the court just like the coordinate system of the view's bounds is within the view itself. (court=view, basketball/players=content inside the view)
Like the frame, view.center is also in the coordinates of the superview.
Frame vs Bounds - Example 1
The yellow rectangle represents the view's frame. The green rectangle represents the view's bounds. The red dot in both images represents the origin of the frame or bounds within their coordinate systems.
Frame
origin = (0, 0)
width = 80
height = 130
Bounds
origin = (0, 0)
width = 80
height = 130
Example 2
Frame
origin = (40, 60) // That is, x=40 and y=60
width = 80
height = 130
Bounds
origin = (0, 0)
width = 80
height = 130
Example 3
Frame
origin = (20, 52) // These are just rough estimates.
width = 118
height = 187
Bounds
origin = (0, 0)
width = 80
height = 130
Example 4
This is the same as example 2, except this time the whole content of the view is shown as it would look like if it weren't clipped to the bounds of the view.
Frame
origin = (40, 60)
width = 80
height = 130
Bounds
origin = (0, 0)
width = 80
height = 130
Example 5
Frame
origin = (40, 60)
width = 80
height = 130
Bounds
origin = (280, 70)
width = 80
height = 130
Again, see here for my answer with more details.
I found this image most helpful for understanding frame, bounds, etc.
Also please note that frame.size != bounds.size when the image is rotated.
I think if you think it from the point of CALayer, everything is more clear.
Frame is not really a distinct property of the view or layer at all, it is a virtual property, computed from the bounds, position(UIView's center), and transform.
So basically how the layer/view layouts is really decided by these three property(and anchorPoint), and either of these three property won't change any other property, like changing transform doesn't change bounds.
There are very good answers with detailed explanation to this post. I just would like to refer that there is another explanation with visual representation for the meaning of Frame, Bounds, Center, Transform, Bounds Origin in WWDC 2011 video Understanding UIKit Rendering starting from #4:22 till 20:10
After reading the above answers, here adding my interpretations.
Suppose browsing online, web browser is your frame which decides where and how big to show webpage. Scroller of browser is your bounds.origin that decides which part of webpage will be shown. bounds.origin is hard to understand. The best way to learn is creating Single View Application, trying modify these parameters and see how subviews change.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100.0f, 200.0f, 200.0f, 400.0f)];
[view1 setBackgroundColor:[UIColor redColor]];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectInset(view1.bounds, 20.0f, 20.0f)];
[view2 setBackgroundColor:[UIColor yellowColor]];
[view1 addSubview:view2];
[[self view] addSubview:view1];
NSLog(#"Old view1 frame %#, bounds %#, center %#", NSStringFromCGRect(view1.frame), NSStringFromCGRect(view1.bounds), NSStringFromCGPoint(view1.center));
NSLog(#"Old view2 frame %#, bounds %#, center %#", NSStringFromCGRect(view2.frame), NSStringFromCGRect(view2.bounds), NSStringFromCGPoint(view2.center));
// Modify this part.
CGRect bounds = view1.bounds;
bounds.origin.x += 10.0f;
bounds.origin.y += 10.0f;
// incase you need width, height
//bounds.size.height += 20.0f;
//bounds.size.width += 20.0f;
view1.bounds = bounds;
NSLog(#"New view1 frame %#, bounds %#, center %#", NSStringFromCGRect(view1.frame), NSStringFromCGRect(view1.bounds), NSStringFromCGPoint(view1.center));
NSLog(#"New view2 frame %#, bounds %#, center %#", NSStringFromCGRect(view2.frame), NSStringFromCGRect(view2.bounds), NSStringFromCGPoint(view2.center));

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 ;-)