Get position of UIButton in UITableViewCell - ios

I have a button in a static UITableViewCell. What I want to do is place a UIView at the buttons location.
If I just get the buttons position, it will give me a position relative to the tableviews cell, not from the top of the tableViewController. What I want is get the buttons position from the top of the tableViewController.
For example: If the button is 8px away from the top of the current
cell, but 57px away from the top of the viewController, I want to
place the UIView at 57px.
Here's my code:
- (void)viewDidLoad {
CGPoint buttonPosition = [button convertPoint:CGPointZero fromView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
CGRect rect = [myTableView rectForRowAtIndexPath:indexPath];
NSLog(#"%f, %f", rect.origin.y, rect.origin.x);
self.infoView = [[UIView alloc] initWithFrame:CGRectMake(button.frame.origin.x, button.frame.origin.y, 220, 110)];
}
The output of the nslog is 0.
I would also like to update the UIView's position whenever the iphone goes to landscape mode.

CGPoint originalPoint = button.frame.origin;
CALayer *layer = self;
CGPoint point = originalPoint;
while (layer.superlayer)
{
point = [layer convertPoint:point toLayer:layer.superlayer];
layer = layer.superlayer;
}
//When you reach this code, you should have the position in the rootView

Related

How do I calculate the correct CGRect origin on a scaled UIView subview?

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!

UIView that follows UICollectionView indicator

I want to create a custom UIView that contains a UIImageView and a UILabel, that points to the UICollectionView scroll indicator, and scrolls with it.
i am using this code :
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
// get the relative position of the indicator in the main window
CGPoint p = [verticalIndicator convertPoint:verticalIndicator.bounds.origin toView:[[UIApplication sharedApplication] keyWindow]];
// set the custom view new position
CGRect indicatorFrame = self.indicatorView.frame;
indicatorFrame.origin.y = p.y;
self.indicatorView.frame = indicatorFrame;
}
But the indicatorView does not follow the indicator exactly !!
This worked for me: Please look at the attached gif. I have added the custom uiview with blue color parallel to the scrool indicator.
I tried for the table view, but this also works for the collection view too.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
dispatch_async(dispatch_get_main_queue(), ^{
//get refrence of vertical indicator
UIImageView *verticalIndicator = ((UIImageView *)[scrollView.subviews objectAtIndex:(scrollView.subviews.count-1)]);
// get the relative position of the indicator in the main window
// CGPoint p = [verticalIndicator convertPoint:verticalIndicator.bounds.origin toView:[[UIApplication sharedApplication] keyWindow]];
CGPoint p = CGPointMake(verticalIndicator.frame.origin.x-10,
verticalIndicator.frame.origin.y - scrollView.contentOffset.y);
// set the custom view new position
CGRect indicatorFrame = CGRectMake(verticalIndicator.frame.origin.x, verticalIndicator.frame.origin.y, 10, 10);
self.indicatorView.frame = indicatorFrame;
});
}
Also maske sure you added the uiview in you view did load method.
//in viewDidLoad:
Note that I have used the sample values for the indicatorView postion.You can replace these as per your need.
indicatorView=[[UIView alloc]initWithFrame:CGRectMake( p.x, p.y , 10, 10)];
indicatorView.backgroundColor=[UIColor blueColor];
[self.tableView addSubview:indicatorView];
For me this works:
....
scrollIndicatorView = [self scrollIndicator];
[self.collectionView addSubview:scrollIndicatorView];
....
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// percentage of the content reached * height
CGFloat yPos = (self.collectionView.contentOffset.y / (self.collectionView.contentSize.height - self.collectionView.bounds.size.height)) * self.collectionView.bounds.size.height;
yPos += self.collectionView.contentOffset.y; // add content offset becasue view is inside colelctionview which content moves
CGRect indicatorFrame = CGRectMake(self.collectionView.bounds.size.width - 10,
yPos,
10,
30);
scrollIndicatorView.frame = indicatorFrame;
}

how to get a subview of UIScrollView using a CGPoint

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

How to get a UIView current position?

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.

How can I drag a UICollectionViewCell from one UICollectionView to another UICollectionView?

I am making an iPad application. On one page of this application, there is a UICollectionView on the left-hand side and another UICollectionView on the right hand side. Each UICollectionView is one column wide.
The functionality I desire is as follows:
Each UICollectionViewCell on the left hand side should be able to be dragged to the UICollectionView on the right hand side. If this is not possible, then at least a UICollectionViewCell should be able to be dragged out of the left UICollectionView and then I'll handle having it appear in the righthand UICollectionView.
Is this functionality possible? If so, how would I go about implementing it?
You would want to attach a long press gesture recognizer to the common superview of both collectionViews. The drag operation is triggered by the long-press, and the entire transaction is handled within that recognizer. Because the pan gesture is used for scrolling the collectionviews, you will run into problems in trying to use the pan recognizer.
The key thing is the gesture recognizer needs to be attached the COMMON superview, and all points and rectangles are converted to the coordinate system of the superview.
This isn't the exact code (this moves from a CV to another view) but the process would be similar (NOTE: I have tried to strip out some code that would be irrelevant to your app, so I could have messed something up in the process -- but the concept holds):
- (void) processLongPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateChanged)
{
if (!dragView)
return;
CGPoint location = [sender locationInView:self.view];
CGPoint translation;
translation.x = location.x - dragViewStartLocation.x;
translation.y = location.y - dragViewStartLocation.y;
CGAffineTransform theTransform = dragView.transform;
theTransform.tx = translation.x;
theTransform.ty = translation.y;
dragView.transform = theTransform;
[self.view bringSubviewToFront:dragView];
return;
}
if (sender.state == UIGestureRecognizerStateBegan)
{
// if point gives a valid collectionView indexPath we are doing a long press on a picture item to begin a drag
// & drop operation.
CGPoint point = [sender locationInView:collectionView];
dragViewIndexPath = [collectionView indexPathForItemAtPoint:point];
if (dragViewIndexPath) // i.e., selected item in collection view.
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:dragViewIndexPath];
dragView = [cell.contentView viewWithTag:cell.tag];
[dragView removeFromSuperview];
[self.view addSubview:dragView];
dragView.center = [collectionView convertPoint:point toView:self.view];
dragViewStartLocation = dragView.center;
[self.view bringSubviewToFront:dragView];
}
return;
}
if (sender.state == UIGestureRecognizerStateEnded)
{
if (dragView)
{
dragView.center = CGPointMake(dragView.center.x + dragView.transform.tx, dragView.center.y + dragView.transform.ty);
CGAffineTransform theTransform = dragView.transform;
theTransform.tx = 0.0f;
theTransform.ty = 0.0f;
UIView *dropTarget = [self mapDisplayModeToReceiverView]; // get drop target
CGRect convertedTargetFrame = [self.view convertRect:dropTarget.frame fromView:dropTarget.superview];
if (CGRectContainsPoint(convertedTargetFrame, dragView.center)) // if so, then drop it.
{
ImageWithAttachedLabel *i = (ImageWithAttachedLabel *) dragView;
[speakStrings addObject:[i.labelText stringByAppendingString:#". "]];
UserData *uData = (UserData *)i.userDataObject;
UIImage *image = [[UIImage alloc] initWithData:uData.image];
CGRect newFrame = CGRectMake(0.0f, 0.0f, 140.0f, 140.0f);
ImageWithAttachedLabel *newImage = [[ImageWithAttachedLabel alloc] initWithFrame:newFrame withImage:image withLabel:uData.itemName];
newImage.tag = RECEIVERVIEW_MAGIC_NUMBER;
[self.view addSubview:newImage];
newImage.center = [receiverView convertPoint:dropTarget.center toView:self.view];
[UIView animateWithDuration:0.35f animations:^{ newImage.transform = CGAffineTransformMakeScale(1.15f, 1.15f); newImage.transform = CGAffineTransformIdentity; }
completion:^(BOOL finished) { if (finished)
{
[newImage removeFromSuperview];
newImage.frame = newFrame;
[dropTarget addSubview:newImage];
[dragView removeFromSuperview];
dragView=nil; }
}];
}
else
{
[dragView removeFromSuperview];
dragView = nil;
}
[self reloadData];
return;
}
}
There's no way to actually 'pass' a cell from a collection to the other, but you can do the following:
1) Once you detect that the user dragged a cell in the other collection, delete the cell from the first collection (let's call it Collection 1). You can maybe use a nice fade animation to make the cell disappear.
2) Add a cell to the second table with a nice animation (see the UICollectionView and UICollectionViewLayout methods and delegate methods for this).

Resources