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!
Is it possible to implement above slider by subclassing UISlider?
I want slider thumb move to edges but there is some space left.
I have achieved custom position by overriding following methods but now thumb is not responding to touch events.
-(void)awakeFromNib {
[super awakeFromNib];
[self setThumbImage:[UIImage imageNamed:IMG_SLIDER_THUMB] forState:UIControlStateNormal];
}
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value {
UIImage* image = self.currentThumbImage;
CGRect thumbRect = [super thumbRectForBounds:bounds trackRect:rect value:value];
return CGRectMake(thumbRect.origin.x, rect.origin.y+2, image.size.width, image.size.height);
}
I implemented above by subclassing UISlider and for touch event issue I added height constraint to slider in storyboard to increase bounds at runtime. Following is the code to move slider to edges and custom thumb location:
//Get thumb rect for larger track rect than actual to move slider to edges
-(CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value {
UIImage *image = self.currentThumbImage;
rect.origin.x -= SLIDER_OFFSET;
rect.size.width += (SLIDER_OFFSET*2);
CGRect thumbRect = [super thumbRectForBounds:bounds trackRect:rect value:value];
return CGRectMake(thumbRect.origin.x, rect.origin.y+2, image.size.width, image.size.height);
}
//Make track rect smaller than bounds
-(CGRect)trackRectForBounds:(CGRect)bounds {
bounds.origin.x += SLIDER_OFFSET;
bounds.size.width -= (SLIDER_OFFSET*2);
CGRect trackRect = [super trackRectForBounds:bounds];
return CGRectMake(trackRect.origin.x, trackRect.origin.y, trackRect.size.width, trackRect.size.height);
}
I think the code is wrong.
First, you should get the CGRect of the custom view like this:
CGRect oldRect = [super thumbRectForBounds:bounds trackRect:rect value:value];
Than, change the oldRect.x and oldRect.y ,get NewRect
The last , return the NewRect.
I have a UIScrollView with a number of rectangular subviews lined up, of equal sizes. Then I need to be able to pass a CGPoint to that UIScrollView and I want it to give me the rectangular subview that contains the CGPoint. That's basically hitTest:event, except hitTest:event: doesn't work with UIScrollView once CGPoint goes beyond the UIScrollView bounds and doesn't look into its actual content.
What's everyone been doing about this? How to "hit test" on a UIScrollView content view?
Here's some code to illustrate the problem:
NSArray *rectangles = [self getBeautifulRectangles];
CGFloat rectangleLength;
rectangleLength = 100;
// add some rectangle subviews
for (int i = 0; i < rectangles.count; i++) {
UIView *rectangle = [rectangles objectAtIndex:i];
[rectangle setFrame:CGRectMake(i * rectangleLength, 0, rectangleLength, rectangleLength)];
[_scrollView addSubview:rectangle];
}
[_scrollView setContentSize:CGSizeMake(rectangleLength * rectangles.count, rectangleLength)];
// add scroll view to parent view
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,320, rectangleLength)];
[containerView addSubview:_scrollView];
// compute CGPoint to center of first rectangle
CGPoint number1RectanglePoint = CGPointMake(0 * rectangleLength + 50, 50);
// compute CGPoint to center of fifth rectangle
CGPoint number5RectanglePoint = CGPointMake(4 * rectangleLength + 50, 50);
UIView *firstSubview = [containerView hitTest:number1RectanglePoint withEvent:nil];
UIView *fifthSubview = [containerView hitTest:number5RectanglePoint withEvent:nil];
if (firstSubview) NSLog(#"first rectangle OK");
if (fifthSubview) NSLog(#"fifth rectangle OK");
output: first rectangle OK
You should be able to loop through the scrollview subviews
+(UIView *)touchedViewIn:(UIScrollView *)scrollView atPoint:(CGPoint)touchPoint {
CGPoint actualPoint = CGPointMake(touchPoint.x + scrollView.contentOffset.x, scrollView.contentOffset.y + touchPoint.y);
for (UIView * subView in scrollView.subviews) {
if(CGRectContainsPoint(subView.frame, actualPoint)) {
NSLog(#"THIS IS THE ONE");
return subView;
}
}
//Nothing touched
return nil;
}
I guess you pass the wrong CGPoint coordinate to the hitTest:withEvent: method causing wrong behavior if the scroll view is scrolled.
The coordinate you pass to this method must be in the target views coordinate system. I guess your coordinate is in the UIScrollView's superview's coordinate system.
You can convert the coordinate prior to using it for the hit test using CGPoint hitPoint = [scrollView convertPoint:yourPoint fromView:scrollView.superview].
In your example you let the container view perform the hit testing, but the container can only see & hit the visible portion of the scroll view and thus your hit fails.
In order to hit subviews of the scroll view which are outside of the visible area you have to perform the hit test on the scroll view directly:
UIView *firstSubview = [_scrollView hitTest:number1RectanglePoint withEvent:nil];
UIView *fifthSubview = [_scrollView hitTest:number5RectanglePoint withEvent:nil];
Using UIView's animateWithDuration:animations:completion:, I'm resizing a UIView and a subview of that UIView which is a subclass of UITableView.
The UIView resizes fine, but the UITableView doesn't. It does move around a little, but the frame does not update properly and reverts to its original state.
Edit: if I move the resizing to the completion block.... it works. What gives?
tweetTable.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[tweetTable endUpdates];
[UIView animateWithDuration:DURATION animations:^{
CGRect leftFrame = leftPane.frame;
leftFrame.size.width = self.view.frame.size.width - MARGIN;
leftPane.frame = leftFrame;
leftPaneButton.frame = leftFrame;
CGRect tweetFrame = tweetTable.frame;
tweetFrame.size.width = leftPane.frame.size.width;
NSLog(#"%f to %f", tweetTable.frame.size.width, leftPane.frame.size.width);
tweetTable.frame = tweetFrame;
tweetTable.backgroundColor = [UIColor redColor];
tweetTable.alpha = 0.5f;
sideInfo.center = CGPointMake(self.view.frame.size.width - MARGIN + (sideInfo.frame.size.width / 2), sideInfo.center.y);
rightPaneButton.center = sideInfo.center;
} completion:^(BOOL finished) {
leftExtended = TRUE;
[tweetTable beginUpdates];
}];
Check in your storyboard if the UIView has Autoresize Subviews checked. That means that it resizes all of its subviews when the view itself gets resized. That would explain why it works in the completion block.
Im building a custom UIView similar to UIPopover view , simply I subclass the UIView class and build the stuff of controls and events inside .. to show this view I assign the superView through My sub Class datasource like this
if ([dataSource respondsToSelector:#selector(containerView)])
superView = [dataSource containerView];
and to show it I have a function doing that like this
- (void) showPopOverFromRect : (CGRect) rect
{
CGSize popSize = self.frame.size;
float yPoint;
if(ntPopOverDirection == NTPopOverArrowDirectionUP)
yPoint = rect.origin.y + 10;
else
yPoint = rect.origin.y - 10;
self.frame = CGRectMake(rect.origin.x - popSize.width, yPoint , popSize.width, popSize.height);
[superView addSubview:self];
}
so My Question .. how can i dismiss this view (remove it) if the user tap AnyWhere on the superView just like the UIPopOverController ?
I suggest that you create your custom UIView to fill the entire superview or the entire screen with a clear background or a radial gradient. Then inside of this you would put another UIView that has the look and feel of the popover.
This eliminates the issue of trying to capture taps and sending notifications from other views. It will be all self contained.
You can easily add a gesture recognizer inside your custom view to close the view when the user touches the clear area.
You could place a UIButton below your new UIView that is clear. When this new button is pressed, it dismisses your view and removes itself from superview.
Something like:
- (void) showPopOverFromRect : (CGRect) rect
{
CGSize popSize = self.frame.size;
float yPoint;
if(ntPopOverDirection == NTPopOverArrowDirectionUP)
yPoint = rect.origin.y + 10;
else
yPoint = rect.origin.y - 10;
self.frame = CGRectMake(rect.origin.x - popSize.width, yPoint , popSize.width, popSize.height);
UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom];
dismissButton.backgroundColor = [UIColor clearColor];
dismissButton.frame = [[UIScreen mainScreen] bounds];
[dismissButton addTarget:self.delegate action:#selector(dismissPopover) forControlEvents:UIControlEventTouchUpInside];
[superview addSubview:dismissButton];
[superView addSubview:self];
}
You'd have to set up the view to set it's superview as a delegate that receives the message to dismiss the popover, though.