I need to calculate the visible CGRect of a UIView subview, in the coordinates of the original view. I've got it working if the scale is 1, but if one of the superviews or the view itself is scaled (pinch), the visible CGRect origin is offset slightly.
This works when the scale of the views is 1 or the view is a subview of the root view:
// return the part of the passed view that is visible
// TODO: figure out why result origin is wrong for scaled subviews
//
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view frame
}
When a subview is scaled, the size of the resultant view is correct, but the origin is offset by a small amount. It appears that the convertRect does not calculate the origin properly for scaled subviews.
I tried adjusting the origin relative to the X/Y transform scale but I could not get the calculation correct. Perhaps someone can help?
To save time, here is a complete test ViewController.m, where a box with an X is drawn on the visible part of the views - just create a reset button in the Main.storyboard and connect it to the reset method:
//
// ViewController.m
// VisibleViewDemo
//
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
CG_INLINE void drawLine(UIView *view,CGPoint point1,CGPoint point2, UIColor *color, NSString *layerName) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:point1];
[path addLineToPoint:point2];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineWidth = 2.0;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.name = layerName;
[view.layer addSublayer:shapeLayer];
}
CG_INLINE void removeShapeLayers(UIView *view,NSString *layerName) {
if (view.layer.sublayers.count > 0) {
for (CALayer *layer in [view.layer.sublayers copy]) {
if ([layer.name isEqualToString:layerName]) {
[layer removeFromSuperlayer];
}
}
}
}
CG_INLINE void drawXBox(UIView *view, CGRect rect,UIColor *color) {
NSString *layerName = #"xbox";
removeShapeLayers(view, layerName);
CGPoint topLeft = CGPointMake(rect.origin.x,rect.origin.y);
CGPoint topRight = CGPointMake(rect.origin.x + rect.size.width,rect.origin.y);
CGPoint bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
CGPoint bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
drawLine(view,topLeft,topRight,color,layerName);
drawLine(view,topRight,bottomRight,color,layerName);
drawLine(view,topLeft,bottomLeft,color,layerName);
drawLine(view,bottomLeft,bottomRight,color,layerName);
drawLine(view,topLeft,bottomRight,color,layerName);
drawLine(view,topRight,bottomLeft,color,layerName);
}
#interface ViewController ()
#end
#implementation ViewController
UIView *view1;
UIView *view2;
UIView *view3;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2;
CGFloat height = [UIScreen mainScreen].bounds.size.height / 4;
view1 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2, width, height)];
view1.backgroundColor = UIColor.yellowColor;
[self.view addSubview:view1];
[self addGestures:view1];
view2 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2 + height + 16, width, height)];
view2.backgroundColor = UIColor.greenColor;
[self.view addSubview:view2];
[self addGestures:view2];
view3 = [[UIView alloc] initWithFrame:CGRectMake(10, 10, width / 2, height / 2)];
view3.backgroundColor = [UIColor.blueColor colorWithAlphaComponent:0.5];
[view1 addSubview:view3]; // this one will behave differently
[self addGestures:view3];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self checkOnScreen:view1];
[self checkOnScreen:view2];
[self checkOnScreen:view3];
}
- (IBAction)reset:(id)sender {
view1.transform = CGAffineTransformIdentity;
view2.transform = CGAffineTransformIdentity;
view3.transform = CGAffineTransformIdentity;
[self.view setNeedsLayout];
}
- (void)addGestures:(UIView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[view addGestureRecognizer:panGestureRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePinch:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view
}
- (void)checkOnScreen:(UIView *)view {
CGRect visibleRect = [self getVisibleRect:view];
if (CGRectEqualToRect(visibleRect, CGRectNull)) {
visibleRect = CGRectZero;
}
drawXBox(view,visibleRect,UIColor.blackColor);
}
//
// Pinch (resize) an image on the ViewController View
//
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.transform = CGAffineTransformTranslate(initialTransform,translation.x,translation.y);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
#end
So you need to know the real visible frame of a view that is somehow derived from bounds+center+transform and calculate everything else from that, instead of the ordinary frame value. This means you'll also have to recreate convertRect:fromView: to be based on that. I always sidestepped the problem by using transform only for short animations where such calculations are not necessary. Thinking about coding such a -getVisibleRect: method makes me want to run away screaming ;)
What is a frame?
The frame property is derived from center and bounds.
Example:
center is (60,50)
bounds is (0,0,100,100)
=> frame is (10,0,100,100)
Now you change the frame to (10,20,100,100). Because the size of the view did not change, this results only in a change to the center. The new center is now (60,70).
How about transform?
Say you now transform the view, by scaling it to 50%.
=> the view has now half the size than before, while still keeping the same center. It looks like the new frame is (35,45,50,50). However the real result is:
center is still (60,50): this is expected
bounds is still (0,0,100,100): this should be expected too
frame is still (10,20,100,100): this is somewhat counterintuitive
frame is a calculated property, and it doesn't care at all about the current transform. This means that the value of the frame is meaningless whenever transform is not the identity transform. This is even documented behaviour. Apple calls the value of frame to be "undefined" in this case.
Consequences
This has the additional consequences that methods such as convertRect:fromView: do not work properly when there are non-standard transforms involved. This is because all these methods rely on either frame or bounds of views, and they break as soon as there are transforms involved.
What can be done?
Say you have three views:
view1 (no transform)
view2 (scale transform 50%)
view3 (no transform)
and you want to know the coordinates of view3 from the point of view of view1.
From the point of view of view2, view3 has frame view3.frame. Easy.
From the point of view of view1, view2 has not frame view2.frame, but the visible frame is a rectangle with size view2.bounds/2 and center view2.center.
To get this right you need some basic linear algebra (with matrix multiplications). (And don't forget the anchorPoint..)
I hope it helps..
What can be done for real?
In your question you said that there is an offset. Maybe you can just calculate the error now? The error should be something like 0.5 * (1-scale) * (bounds.size) . If you can calculate the error, you can subtract it and call it a day :)
Thanks to #Michael for putting in so much effort in his answer. It didn't solve the problem but it made me think some more and try some other things.
And voila, I tried something that I'm certain I had done before, but this time I started with my latest code. It turns out a simple solution did the trick. The builtin UIView convertRect:fromView and convertRect:toView worked as expected when used together.
I apologize to anyone that has spent time on this. I'm humbled in my foolishness and how much time I have spent on this. I must have made a mistake somewhere when I tried this before because it didn't work. But this works very well now:
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect rootRect = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect rootVisible = CGRectIntersection(vc.view.bounds, rootRect);
// convert the rect back to the initial view's coordinate system
CGRect visible = [view convertRect:rootVisible fromView:vc.view];
return visible; // may be same as the original view frame
}
If someone uses the Viewcontroller.m from my question, just replace the getVisibleRect method with this one and it will work very nicely.
NOTE: I tried rotating the view and the visible rect is rotated too because I displayed it on the view itself. I guess I could reverse whatever the view rotation is on the shape layers, but that's for another day!
I am trying to create a subclass of UIView that will show a list of fixed sized UIImages similar to how a UILabel displays letters.If all the images won't fit on one line, the images are arranged on multiple lines.
How can I achieve this using autolayouts so that if I put this view in a UIStackView the images will be listed correctly?
Here is a sample if I did it using fixed position :
- (void) layoutSubviews{
[super layoutSubviews];
CGRect bounds = self.bounds;
CGRect imageRect = CGRectMake(0, 0, 30, 30);
for (UIImageView* imageView in self.imageViews){
imageView.frame = imageRect;
imageRect.origin.x = CGRectGetMaxX(imageRect);
if (CGRectGetMaxX(imageRect) > CGRectGetMaxX(bounds)){
imageRect.origin.x = 0.0;
imageRect.origin.y = CGRectGetMaxY(imageRect);
}
}
}
Update:
Here is a sample project to show the issue.
https://github.com/datinc/DATDemoImageListView
Here the link for ImageListView
https://github.com/datinc/DATDemoImageListView/blob/master/DATDemoImageListView/DATDemoImageListView.m
You should overwrite intrinsicContentSize returning the preferred content size of your view.
The problem is that in intrinsicContentSize the view has not it's final bounds. You can use an internal height constraint, and overwrite updateConstraints:
- (void)updateConstraints {
CGFloat theWidth = CGRectGetWidth(self.bounds);
NSUInteger theCount = [self.subviews count];
CGFloat theRows = ceilf(theCount / floorf(theWidth / 30.0));
self.heightConstraint.constant = 30.0 * theRows;
[super updateConstraints];
}
In viewDidAppear: and on layout changes (e. g. rotation) you have to call setNeedsUpdateConstraints to get a proper initial layout of the image views.
I custom a uitableviewcell with nib, and want to custom the highlighted style. When the cell highlighted, I want to reduce the size of the subview of the cell and keep the position of the imageview in the cell not changing. So, it looks like the frame of a imageview zoomed out, but the image itself stay there not changing its position.
However, this code snippet doesn't work as I wanted.Could anybody help me to figure out where I am wrong. Any help will be appreciated!
tips: the imageview is a subview of self.frameView and frameView is a subview of frameBgView.
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
if (highlighted)
{
self.frameView.backgroundColor = UIColorFromRGBA(0, 0, 0, 0.1);
CGRect rect = self.frameBgView.frame;
rect.origin.x += 10;
rect.size.width -= 20;
self.frameBgView.frame = rect;
rect = self.frameView.frame;
rect.origin.x -= 10;
self.frameView.frame = rect;
}
else
{
....
}
}
EDIT: some screenshots to explain the question:
oky i tried your solution but i got like this i am posting the code as well as the output how it looks, if there is any problem just comment , i am deleted the frameBgView it is not required
code
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
if (highlighted)
{
self.frameView.backgroundColor = [UIColor grayColor];
CGRect rect = self.contentView.bounds;
rect.origin.x += 15;
rect.size.width -= 30;
//rect = self.frameView.frame;
// rect.origin.x -= 10;
self.frameView.frame = rect;
}
else
{
self.frameView.backgroundColor = [UIColor whiteColor];
CGRect rect = self.contentView.bounds;
rect.origin.x += 10;
rect.size.width -= 20;
self.frameView.frame = rect;
}
}
and i am not using auto layout .. the result what i got is, before
and after highlighted,
EDIT:
this is the cell structure
as u can see in the picture, in content view i hav added frameView which is blue in colour, and within frameView i hav added imageView and also don't forget t set content mode scale to fill and also auto resizing masks for both frameView and imageView for example
autoresizing masks for content view
autoresizing masks for frameView
autoresizing masks for imageView
END EDIT
I'm rendering CGPDFPage in UIImageView but not zooming how we can zoom if any know plz let me know
PDFDocument = CGPDFDocumentCreateWithURL((__bridge CFURLRef)pdfUrl);
totalPages = (int)CGPDFDocumentGetNumberOfPages(PDFDocument);
NSLog(#"total pages %i",totalPages);
//struct CGPDFPage *page =CGPDFDocumentGetPage(PDFDocument, 1);
CGFloat width = 600.0;
// Get the page
CGPDFPageRef myPageRef = CGPDFDocumentGetPage(PDFDocument, i);
// Changed this line for the line above which is a generic line
//CGPDFPageRef page = [self getPage:page_number];
CGRect pageRect = CGPDFPageGetBoxRect(myPageRef, kCGPDFMediaBox);
CGFloat pdfScale = width/pageRect.size.width;
pageRect.size = CGSizeMake(pageRect.size.width*pdfScale, pageRect.size.height*pdfScale);
pageRect.origin = CGPointZero;
UIGraphicsBeginImageContext(pageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
// White BG
CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
CGContextFillRect(context,pageRect);
CGContextSaveGState(context);
// ***********
// Next 3 lines makes the rotations so that the page look in the right direction
// ***********
CGContextTranslateCTM(context, 0.0, pageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextConcatCTM(context, CGPDFPageGetDrawingTransform(myPageRef, kCGPDFMediaBox, pageRect, 0, true));
CGContextDrawPDFPage(context, myPageRef);
CGContextRestoreGState(context);
imageView= UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
You should add your imageView as subview of UIScrollView.
This SO answer describes how to zoom UIImageView inside UIScrollView:
Set your view controller up as a <UIScrollViewDelegate>
Draw your UIScrollView the size you want for the rectangle at the center of the view. Set the max zoom in the inspector to something bigger than 1. Like 4 or 10.
Right click on the scroll view and connect the delegate to your view controller.
Draw your UIImageView in the UIScrollView and set it up with whatever image you want. Make it the same size as the UIScrollView.
Ctrl + drag form you UIImageView to the .h of your View controller to create an IBOutlet for the UIImageView, call it something clever like imageView.
Add this code:
-(UIView *) viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
Run the app and pinch and pan til your heart's content.
Go with this
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return self.imageView;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.scrollView.minimumZoomScale=0.5;
self.scrollView.maximumZoomScale=6.0;
self.scrollView.contentSize=CGSizeMake(1280, 960);
self.scrollView.delegate=self;
}
Check Apple Documentation
Another way is to implement UITapGestureRecognizer in your viewController
- (void)viewDidLoad
{
[super viewDidLoad];
// target - what object is going to handle
// the gesture when it gets recognised
// the argument for tap: is the gesture that caused this message to be sent
UITapGestureRecognizer *tapOnce =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapOnce:)];
UITapGestureRecognizer *tapTwice =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapTwice:)];
tapOnce.numberOfTapsRequired = 1;
tapTwice.numberOfTapsRequired = 2;
//stops tapOnce from overriding tapTwice
[tapOnce requireGestureRecognizerToFail:tapTwice];
// then need to add the gesture recogniser to a view
// - this will be the view that recognises the gesture
[self.view addGestureRecognizer:tapOnce];
[self.view addGestureRecognizer:tapTwice];
}
Basically this code is saying that when a UITabGesture is registered in self.view the method tapOnce or tapTwice will be called in self depending on if its a single or double tap. You therefore need to add these tap methods to your UIViewController:
- (void)tapOnce:(UIGestureRecognizer *)gesture
{
//on a single tap, call zoomToRect in UIScrollView
[self.myScrollView zoomToRect:rectToZoomInTo animated:NO];
}
- (void)tapTwice:(UIGestureRecognizer *)gesture
{
//on a double tap, call zoomToRect in UIScrollView
[self.myScrollView zoomToRect:rectToZoomOutTo animated:NO];
}
Hope that helps
I have a UIView appearing when the user taps the screen.
After that, the UIView is dropping due to gravity by UIGravityBehavior.
Is there a way to get the UIView's position while dropping?
- (IBAction)makePoint:(UITapGestureRecognizer *)sender {
CGRect frame;
frame.origin = [sender locationInView:self.gameView];
frame.size = pointSize; //pre-defined size..
_point = [[myPoint alloc]initWithFrame:frame]; //This is UIView
[self.gameView addSubview:_point];
[self.gravity addItem:_point];
[self.collider addItem:_point];
}
I'm trying to get the CGPoint of it.
Thanks.
Use action block on the UIGravityBehavior like this,
gravityBehavior.action = ^{
CGRect frame = gravityView.frame;
CGRect otherViewFrame = otherView.frame;
if(CGRectIntersects(frame, otherViewFrame)){
// put your custom logic here
}
};
I hope that helps.
Look this https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDynamicBehavior_Class/Reference/Reference.html#//apple_ref/occ/instp/UIDynamicBehavior/action#jumpTo_3 for further detail.